# HG changeset patch # User Nattee Niparnan # Date 2015-02-06 00:47:51 # Node ID 17754ce1a3d6a7cac5511d7c7bc3607e27f7eb70 # Parent 91238b16dbc9be0d2e2b3a8e1d0af66c9df9c5e6 # Parent b530a9de9711b9f5749446b518ac24a8699a9847 Merge pull request #14 from nattee/master merge commit from nattee diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,10 @@ *~ /vendor/plugins/rails_upgrade + +#ignore public assets??? +/public/assets + +#ignore .orig and .swp +*.orig +*.swp diff --git a/Gemfile b/Gemfile --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,19 @@ # To use debugger # gem 'debugger' +# + +#in-place editor +gem 'best_in_place', '~> 3.0.1' + +# jquery addition +gem 'jquery-rails' +gem 'jquery-ui-sass-rails' +gem 'jquery-timepicker-addon-rails' +gem 'jquery-tablesorter' + +#syntax highlighter +gem 'rouge' gem 'haml' gem 'mail' diff --git a/Gemfile.lock b/Gemfile.lock --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,6 +37,9 @@ i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) arel (3.0.3) + best_in_place (3.0.2) + actionpack (>= 3.2) + railties (>= 3.2) builder (3.0.4) coffee-rails (3.2.2) coffee-script (>= 2.2.0) @@ -57,6 +60,22 @@ journey (1.0.4) json (1.8.1) mail (2.5.4) + jquery-rails (3.1.1) + railties (>= 3.0, < 5.0) + thor (>= 0.14, < 2.0) + jquery-tablesorter (1.12.7) + railties (>= 3.1, < 5) + jquery-timepicker-addon-rails (1.4.1) + railties (>= 3.1) + jquery-ui-rails (4.0.3) + jquery-rails + railties (>= 3.1.0) + jquery-ui-sass-rails (4.0.3.0) + jquery-rails + jquery-ui-rails (= 4.0.3) + railties (>= 3.1.0) + json (1.8.1) + mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.25.1) @@ -92,7 +111,8 @@ rdiscount (2.1.7.1) rdoc (3.12.2) json (~> 1.4) - rspec-collection_matchers (1.1.2) + rouge (1.6.2) + rspec-collection_matchers (1.0.0) rspec-expectations (>= 2.99.0.beta1) rspec-core (2.99.2) rspec-expectations (2.99.2) @@ -134,17 +154,24 @@ ruby DEPENDENCIES + best_in_place (~> 3.0.1) coffee-rails (~> 3.2.2) dynamic_form haml in_place_editing + jquery-rails + jquery-tablesorter + jquery-timepicker-addon-rails + jquery-ui-sass-rails mail mysql2 prototype-rails rails (= 3.2.21) rdiscount - rspec-rails (~> 2.99.0) + rouge sass-rails (~> 3.2.6) + rspec-rails (~> 2.0) + test-unit uglifier verification! diff --git a/README b/README --- a/README +++ b/README @@ -1,182 +1,10 @@ -== Welcome to Rails - -Rails is a web-application and persistence framework that includes everything -needed to create database-backed web-applications according to the -Model-View-Control pattern of separation. This pattern splits the view (also -called the presentation) into "dumb" templates that are primarily responsible -for inserting pre-built data in between HTML tags. The model contains the -"smart" domain objects (such as Account, Product, Person, Post) that holds all -the business logic and knows how to persist themselves to a database. The -controller handles the incoming requests (such as Save New Account, Update -Product, Show Post) by manipulating the model and directing data to the view. - -In Rails, the model is handled by what's called an object-relational mapping -layer entitled Active Record. This layer allows you to present the data from -database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in -link:files/vendor/rails/activerecord/README.html. - -The controller and view are handled by the Action Pack, which handles both -layers by its two parts: Action View and Action Controller. These two layers -are bundled in a single package due to their heavy interdependence. This is -unlike the relationship between the Active Record and Action Pack that is much -more separate. Each of these packages can be used independently outside of -Rails. You can read more about Action Pack in -link:files/vendor/rails/actionpack/README.html. - - -== Getting started - -1. At the command prompt, start a new rails application using the rails command - and your application name. Ex: rails myapp - (If you've downloaded rails in a complete tgz or zip, this step is already done) -2. Change directory into myapp and start the web server: script/server (run with --help for options) -3. Go to http://localhost:3000/ and get "Welcome aboard: You’re riding the Rails!" -4. Follow the guidelines to start developing your application - - -== Web Servers - -By default, Rails will try to use Mongrel and lighttpd if they are installed, otherwise -Rails will use the WEBrick, the webserver that ships with Ruby. When you run script/server, -Rails will check if Mongrel exists, then lighttpd and finally fall back to WEBrick. This ensures -that you can always get up and running quickly. +== cafe grader -Mongrel is a Ruby-based webserver with a C-component (which requires compilation) that is -suitable for development and deployment of Rails applications. If you have Ruby Gems installed, -getting up and running with mongrel is as easy as: gem install mongrel. -More info at: http://mongrel.rubyforge.org - -If Mongrel is not installed, Rails will look for lighttpd. It's considerably faster than -Mongrel and WEBrick and also suited for production use, but requires additional -installation and currently only works well on OS X/Unix (Windows users are encouraged -to start with Mongrel). We recommend version 1.4.11 and higher. You can download it from -http://www.lighttpd.net. - -And finally, if neither Mongrel or lighttpd are installed, Rails will use the built-in Ruby -web server, WEBrick. WEBrick is a small Ruby web server suitable for development, but not -for production. - -But of course its also possible to run Rails on any platform that supports FCGI. -Apache, LiteSpeed, IIS are just a few. For more information on FCGI, -please visit: http://wiki.rubyonrails.com/rails/pages/FastCGI - - -== Debugging Rails - -Have "tail -f" commands running on the server.log and development.log. Rails will -automatically display debugging and runtime information to these files. Debugging -info will also be shown in the browser on requests from 127.0.0.1. - - -== Breakpoints - -Breakpoint support is available through the script/breakpointer client. This -means that you can break out of execution at any point in the code, investigate -and change the model, AND then resume execution! Example: - - class WeblogController < ActionController::Base - def index - @posts = Post.find(:all) - breakpoint "Breaking out from the list" - end - end - -So the controller will accept the action, run the first line, then present you -with a IRB prompt in the breakpointer window. Here you can do things like: - -Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint' +cafe grader is a programming contest platform used in Thailand IOI training. +The package includes 2 repositories, jittat/cafe-grader-web and jittat/cafe-grader-judge-scripts. - >> @posts.inspect - => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, - #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" - >> @posts.first.title = "hello from a breakpoint" - => "hello from a breakpoint" - -...and even better is that you can examine how your runtime objects actually work: - - >> f = @posts.first - => #nil, "body"=>nil, "id"=>"1"}> - >> f. - Display all 152 possibilities? (y or n) - -Finally, when you're ready to resume execution, you press CTRL-D - - -== Console - -You can interact with the domain model by starting the console through script/console. -Here you'll have all parts of the application configured, just like it is when the -application is running. You can inspect domain models, change values, and save to the -database. Starting the script without arguments will launch it in the development environment. -Passing an argument will specify a different environment, like script/console production. - -To reload your controllers and models after launching the console run reload! - -To reload your controllers and models after launching the console run reload! - - - -== Description of contents - -app - Holds all the code that's specific to this particular application. - -app/controllers - Holds controllers that should be named like weblogs_controller.rb for - automated URL mapping. All controllers should descend from ApplicationController - which itself descends from ActionController::Base. - -app/models - Holds models that should be named like post.rb. - Most models will descend from ActiveRecord::Base. +=== Installation -app/views - Holds the template files for the view that should be named like - weblogs/index.rhtml for the WeblogsController#index action. All views use eRuby - syntax. - -app/views/layouts - Holds the template files for layouts to be used with views. This models the common - header/footer method of wrapping views. In your views, define a layout using the - layout :default and create a file named default.rhtml. Inside default.rhtml, - call <% yield %> to render the view using this layout. - -app/helpers - Holds view helpers that should be named like weblogs_helper.rb. These are generated - for you automatically when using script/generate for controllers. Helpers can be used to - wrap functionality for your views into methods. - -config - Configuration files for the Rails environment, the routing map, the database, and other dependencies. - -components - Self-contained mini-applications that can bundle together controllers, models, and views. +The system is tested on ubuntu 14.04 LTS. Use the installation script in +cafe-grader-judge-scripts/installer/install.sh . See http://theory.cpe.ku.ac.th/wiki/index.php/%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%95%E0%B8%B4%E0%B8%94%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87_Cafe_grader for the detail. -db - Contains the database schema in schema.rb. db/migrate contains all - the sequence of Migrations for your schema. - -doc - This directory is where your application documentation will be stored when generated - using rake doc:app - -lib - Application specific libraries. Basically, any kind of custom code that doesn't - belong under controllers, models, or helpers. This directory is in the load path. - -public - The directory available for the web server. Contains subdirectories for images, stylesheets, - and javascripts. Also contains the dispatchers and the default HTML files. This should be - set as the DOCUMENT_ROOT of your web server. - -script - Helper scripts for automation and generation. - -test - Unit and functional tests along with fixtures. When using the script/generate scripts, template - test files will be generated for you and placed in this directory. - -vendor - External libraries that the application depends on. Also includes the plugins subdirectory. - This directory is in the load path. diff --git a/app/assets/javascripts/local_jquery.js b/app/assets/javascripts/local_jquery.js new file mode 100644 --- /dev/null +++ b/app/assets/javascripts/local_jquery.js @@ -0,0 +1,14 @@ +//= require jquery +//= require jquery_ujs +//= require jquery.ui.all +//= require jquery.ui.datepicker +//= require jquery.ui.slider +//= require jquery-ui-timepicker-addon +//= require jquery-tablesorter +//= require best_in_place +//= require best_in_place.jquery-ui + +$(document).ready(function() { + /* Activating Best In Place */ + jQuery(".best_in_place").best_in_place(); +}); diff --git a/app/assets/javascripts/report.js.coffee b/app/assets/javascripts/report.js.coffee new file mode 100644 --- /dev/null +++ b/app/assets/javascripts/report.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass --- a/app/assets/stylesheets/application.css.sass +++ b/app/assets/stylesheets/application.css.sass @@ -1,3 +1,12 @@ + +@import jquery.ui.core +@import jquery.ui.theme +@import jquery.ui.datepicker +@import jquery.ui.slider +@import jquery-ui-timepicker-addon +@import jquery-tablesorter/theme.metro-dark +@import tablesorter-theme.cafe + body background: white image-url("topbg.jpg") repeat-x top center font-size: 13px @@ -290,4 +299,4 @@ h2.contest-title margin-top: 5px - margin-bottom: 5px \ No newline at end of file + margin-bottom: 5px diff --git a/app/assets/stylesheets/report.css.scss b/app/assets/stylesheets/report.css.scss new file mode 100644 --- /dev/null +++ b/app/assets/stylesheets/report.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the report controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/tablesorter-theme.cafe.css b/app/assets/stylesheets/tablesorter-theme.cafe.css new file mode 100644 --- /dev/null +++ b/app/assets/stylesheets/tablesorter-theme.cafe.css @@ -0,0 +1,197 @@ +/************* +Metro Dark Theme +*************/ +/* overall */ +.tablesorter-cafe { + // font: 12px/18px 'Segoe UI Semilight', 'Open Sans', Verdana, Arial, Helvetica, sans-serif; + color: #000; + background-color: #777; + margin: 10px 0 15px; + text-align: left; + border-collapse: collapse; + border: #555 1px solid; +} + +.tablesorter-cafe tr.dark-row th, .tablesorter-cafe tr.dark-row td { + background-color: #222; + color: #fff; + text-align: left; + font-size: 14px; +} + +/* header/footer */ +.tablesorter-cafe caption, +.tablesorter-cafe th, +.tablesorter-cafe thead td, +.tablesorter-cafe tfoot th, +.tablesorter-cafe tfoot td { + //font-weight: 300; + //font-size: 15px; + color: #fff; + background-color: #777; + padding: 2px; + border: #555 1px solid; +} + +.tablesorter-cafe .header, +.tablesorter-cafe .tablesorter-header { + background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQBAMAAADQT4M0AAAAGFBMVEUAAADu7u7u7u7u7u7u7u7u7u7u7u7u7u5jNePWAAAACHRSTlMAMxIHKwEgMWD59H4AAABSSURBVAjXY2BgYFJgAAHzYhDJ6igSAKTYBAUTgJSioKAQAwNzoaCguAFDiCAQuDIkgigxBgiA8cJAVCpQt6AgSL+JoKAzA0gjUBsQqBcBCYhFAAE/CV4zeSzxAAAAAElFTkSuQmCC); + background-position: center right; + background-repeat: no-repeat; + cursor: pointer; + white-space: normal; +} +.tablesorter-cafe .tablesorter-header-inner { + padding: 0 18px 0 4px; +} +.tablesorter-cafe thead .headerSortUp, +.tablesorter-cafe thead .tablesorter-headerSortUp, +.tablesorter-cafe thead .tablesorter-headerAsc { + background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQBAMAAADQT4M0AAAAIVBMVEUAAADu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u5meJAOAAAACnRSTlMAMwsqXt+gIBUGxGoDMAAAAFlJREFUCNctzC0SQAAUReEzGNQ3AlHRiSRZFCVZYgeswRL8hLdK7834wj3tAlGP6y7fYHpKS6w6WwbVG0I1NZVnZPG8/DYxOYlnhUYkA06R1s9ESsxR4NIdPhkPFDFYuEnMAAAAAElFTkSuQmCC); +} +.tablesorter-cafe thead .headerSortDown, +.tablesorter-cafe thead .tablesorter-headerSortDown, +.tablesorter-cafe thead .tablesorter-headerDesc { + background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQBAMAAADQT4M0AAAALVBMVEUAAADu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7i0NViAAAADnRSTlMAMiweCQITTvDctZZqaTlM310AAABcSURBVAjXY2BgYEtgAAFHERDJqigUAKSYBQUNgFSioKAYAwOLIBA4MASBKFUGQxAlzAAF+94BwWuGKBC1lIFl3rt3Lx0YGCzevWsGSjK9e6cAUlT3HKyW9wADAwDRrBiDy6bKzwAAAABJRU5ErkJggg==); +} +.tablesorter-cafe thead .sorter-false { + background-image: none; + cursor: default; + padding: 4px; +} + +/* tbody */ +.tablesorter-cafe td { + background-color: #fff; + padding: 1px 4px; + vertical-align: top; + border-style: solid; + border-color: #666; + border-collapse: collapse; + border-width: 0px 1px; + +} + +/* hovered row colors */ +.tablesorter-cafe tbody > tr:hover > td, +.tablesorter-cafe tbody > tr.even:hover > td, +.tablesorter-cafe tbody > tr.odd:hover > td { + background: #bbb; + color: #000; +} + +/* table processing indicator */ +.tablesorter-cafe .tablesorter-processing { + background-position: center center !important; + background-repeat: no-repeat !important; + /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + background-image: url(data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=) !important; +} + +/* pager */ +.tablesorter-cafe .tablesorter-pager button { + background-color: #444; + color: #eee; + border: #555 1px solid; + cursor: pointer; +} +.tablesorter-cafe .tablesorter-pager button:hover { + background-color: #555; +} + +/* Zebra Widget - row alternating colors */ +.tablesorter-cafe tr.odd td { + background-color: #eee; +} +.tablesorter-cafe tr.even td { + background-color: #fff; +} + +/* Column Widget - column sort colors */ +.tablesorter-cafe tr.odd td.primary { + background-color: #bfbfbf; +} +.tablesorter-cafe td.primary, +.tablesorter-cafe tr.even td.primary { + background-color: #d9d9d9; +} +.tablesorter-cafe tr.odd td.secondary { + background-color: #d9d9d9; +} +.tablesorter-cafe td.secondary, +.tablesorter-cafe tr.even td.secondary { + background-color: #e6e6e6; +} +.tablesorter-cafe tr.odd td.tertiary { + background-color: #e6e6e6; +} +.tablesorter-cafe td.tertiary, +.tablesorter-cafe tr.even td.tertiary { + background-color: #f2f2f2; +} + +/* filter widget */ +.tablesorter-cafe .tablesorter-filter-row td { + background: #eee; + line-height: normal; + text-align: center; /* center the input */ + -webkit-transition: line-height 0.1s ease; + -moz-transition: line-height 0.1s ease; + -o-transition: line-height 0.1s ease; + transition: line-height 0.1s ease; +} +/* optional disabled input styling */ +.tablesorter-cafe .tablesorter-filter-row .disabled { + opacity: 0.5; + filter: alpha(opacity=50); + cursor: not-allowed; +} +/* hidden filter row */ +.tablesorter-cafe .tablesorter-filter-row.hideme td { + /*** *********************************************** ***/ + /*** change this padding to modify the thickness ***/ + /*** of the closed filter row (height = padding x 2) ***/ + padding: 2px; + /*** *********************************************** ***/ + margin: 0; + line-height: 0; + cursor: pointer; +} +.tablesorter-cafe .tablesorter-filter-row.hideme .tablesorter-filter { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} +/* filters */ +.tablesorter-cafe .tablesorter-filter { + width: 95%; + height: auto; + margin: 4px; + padding: 4px; + background-color: #fff; + border: 1px solid #bbb; + color: #333; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: height 0.1s ease; + -moz-transition: height 0.1s ease; + -o-transition: height 0.1s ease; + transition: height 0.1s ease; +} +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} 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/configurations_controller.rb b/app/controllers/configurations_controller.rb --- a/app/controllers/configurations_controller.rb +++ b/app/controllers/configurations_controller.rb @@ -3,9 +3,6 @@ before_filter :authenticate before_filter { |controller| controller.authorization_by_roles(['admin'])} - in_place_edit_for :grader_configuration, :key - in_place_edit_for :grader_configuration, :type - in_place_edit_for :grader_configuration, :value def index @configurations = GraderConfiguration.find(:all, @@ -17,4 +14,15 @@ redirect_to :action => 'index' end + def update + @config = GraderConfiguration.find(params[:id]) + respond_to do |format| + if @config.update_attributes(params[:grader_configuration]) + format.json { head :ok } + else + format.json { respond_with_bip(@config) } + end + end + end + 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', @@ -23,6 +32,7 @@ :order => 'created_at DESC') @last_test_request = TestRequest.find(:first, :order => 'created_at DESC') + @submission = Submission.order("id desc").limit(20) end def clear @@ -63,6 +73,19 @@ 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 + when "php" then Rouge::Lexers::PHP.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/login_controller.rb b/app/controllers/login_controller.rb --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -22,6 +22,9 @@ end end + #save login information + Login.create(user_id: user.id, ip_address: request.remote_ip) + redirect_to :controller => 'main', :action => 'list' else flash[:notice] = 'Wrong password' diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -63,10 +63,12 @@ @submission.user = user @submission.language_id = 0 if (params['file']) and (params['file']!='') - @submission.source = params['file'].read + @submission.source = File.open(params['file'].path,'r:UTF-8',&:read) + @submission.source.encode!('UTF-8','UTF-8',invalid: :replace, replace: '') @submission.source_filename = params['file'].original_filename end @submission.submitted_at = Time.new.gmtime + @submission.ip_address = request.remote_ip if GraderConfiguration.time_limit_mode? and user.contest_finished? @submission.errors.add(:base,"The contest is over.") diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -12,7 +12,7 @@ @user = User.find(session[:user_id]) @messages = Message.find_all_sent_by_user(@user) end - + def console @user = User.find(session[:user_id]) @messages = Message.find_all_system_unreplied_messages diff --git a/app/controllers/problems_controller.rb b/app/controllers/problems_controller.rb --- a/app/controllers/problems_controller.rb +++ b/app/controllers/problems_controller.rb @@ -150,11 +150,25 @@ def stat @problem = Problem.find(params[:id]) - if !@problem.available + unless @problem.available or session[:admin] redirect_to :controller => 'main', :action => 'list' - else - @submissions = Submission.find_all_last_by_problem(params[:id]) + return end + @submissions = Submission.includes(:user).where(problem_id: params[:id]).order(:user_id,:id) + + #stat summary + range =65 + @histogram = { data: Array.new(range,0), summary: {} } + user = Hash.new(0) + @submissions.find_each do |sub| + d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60 + @histogram[:data][d.to_i] += 1 if d < range + user[sub.user_id] = [user[sub.user_id], (sub.points >= @problem.full_score) ? 1 : 0].max + end + @histogram[:summary][:max] = [@histogram[:data].max,1].max + + @summary = { attempt: user.count, solve: 0 } + user.each_value { |v| @summary[:solve] += 1 if v == 1 } end def manage @@ -164,8 +178,12 @@ def do_manage if params.has_key? 'change_date_added' change_date_added - else params.has_key? 'add_to_contest' + elsif params.has_key? 'add_to_contest' add_to_contest + elsif params.has_key? 'enable_problem' + set_available(true) + elsif params.has_key? 'disable_problem' + set_available(false) end redirect_to :action => 'manage' end @@ -234,15 +252,26 @@ end end + def set_available(avail) + problems = get_problems_from_params + problems.each do |p| + p.available = avail + p.save + end + end + def get_problems_from_params problems = [] params.keys.each do |k| if k.index('prob-')==0 - name, id = k.split('-') + name, id, order = k.split('-') problems << Problem.find(id) end end problems end + def get_problems_stat + end + end diff --git a/app/controllers/report_controller.rb b/app/controllers/report_controller.rb new file mode 100644 --- /dev/null +++ b/app/controllers/report_controller.rb @@ -0,0 +1,218 @@ +class ReportController < ApplicationController + + before_filter :admin_authorization, only: [:login_stat,:submission_stat, :stuck] + 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 + + date_and_time = '%Y-%m-%d %H:%M' + begin + md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/) + @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) + rescue + @since_time = DateTime.new(1000,1,1) + end + begin + md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/) + @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) + rescue + @until_time = DateTime.new(3000,1,1) + end + + User.all.each do |user| + @logins << { id: user.id, + login: user.login, + full_name: user.full_name, + count: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", + user.id,@since_time,@until_time) + .count(:id), + min: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", + user.id,@since_time,@until_time) + .minimum(:created_at), + max: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", + user.id,@since_time,@until_time) + .maximum(:created_at), + ip: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?", + user.id,@since_time,@until_time) + .select(:ip_address).uniq + + } + end + end + + def submission_stat + + date_and_time = '%Y-%m-%d %H:%M' + begin + @since_time = DateTime.strptime(params[:since_datetime],date_and_time) + rescue + @since_time = DateTime.new(1000,1,1) + end + begin + @until_time = DateTime.strptime(params[:until_datetime],date_and_time) + rescue + @until_time = DateTime.new(3000,1,1) + end + + @submissions = {} + + User.find_each do |user| + @submissions[user.id] = { login: user.login, full_name: user.full_name, count: 0, sub: { } } + end + + Submission.where("submitted_at >= ? AND submitted_at <= ?",@since_time,@until_time).find_each do |s| + if @submissions[s.user_id] + if not @submissions[s.user_id][:sub].has_key?(s.problem_id) + a = nil + begin + a = Problem.find(s.problem_id) + rescue + a = nil + end + @submissions[s.user_id][:sub][s.problem_id] = + { prob_name: (a ? a.full_name : '(NULL)'), + sub_ids: [s.id] } + else + @submissions[s.user_id][:sub][s.problem_id][:sub_ids] << s.id + end + @submissions[s.user_id][:count] += 1 + 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 + + return unless @problem + + @by_lang = {} #aggregrate by language + + range =65 + @histogram = { data: Array.new(range,0), summary: {} } + @summary = {count: 0, solve: 0, attempt: 0} + user = Hash.new(0) + Submission.where(problem_id: @problem.id).find_each do |sub| + #histogram + d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60 + @histogram[:data][d.to_i] += 1 if d < range + + next unless sub.points + @summary[:count] += 1 + user[sub.user_id] = [user[sub.user_id], (sub.points >= @problem.full_score) ? 1 : 0].max + + 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].clone + @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 + + @histogram[:summary][:max] = [@histogram[:data].max,1].max + @summary[:attempt] = user.count + user.each_value { |v| @summary[:solve] += 1 if v == 1 } + end + + def stuck #report struggling user,problem + # init + user,problem = nil + solve = true + tries = 0 + @struggle = Array.new + record = {} + Submission.includes(:problem,:user).order(:problem_id,:user_id).find_each do |sub| + next unless sub.problem and sub.user + if user != sub.user_id or problem != sub.problem_id + @struggle << { user: record[:user], problem: record[:problem], tries: tries } unless solve + record = {user: sub.user, problem: sub.problem} + user,problem = sub.user_id, sub.problem_id + solve = false + tries = 0 + end + if sub.points >= sub.problem.full_score + solve = true + else + tries += 1 + end + end + @struggle.sort!{|a,b| b[:tries] <=> a[:tries] } + @struggle = @struggle[0..50] + end + +end diff --git a/app/controllers/user_admin_controller.rb b/app/controllers/user_admin_controller.rb --- a/app/controllers/user_admin_controller.rb +++ b/app/controllers/user_admin_controller.rb @@ -1,3 +1,5 @@ +require 'csv' + class UserAdminController < ApplicationController include MailHelperMethods @@ -81,11 +83,17 @@ added_random_password = true end - user = User.new({:login => login, - :full_name => full_name, - :password => password, - :password_confirmation => password, - :alias => user_alias}) + user = User.find_by_login(login) + if (user) + user.full_name = full_name + user.password = password + else + user = User.new({:login => login, + :full_name => full_name, + :password => password, + :password_confirmation => password, + :alias => user_alias}) + end user.activated = true user.save @@ -122,7 +130,11 @@ end def user_stat - @problems = Problem.find_available_problems + if params[:commit] == 'download csv' + @problems = Problem.all + else + @problems = Problem.find_available_problems + end @users = User.find(:all, :include => [:contests, :contest_stat]) @scorearray = Array.new @users.each do |u| @@ -141,7 +153,11 @@ end def user_stat_max - @problems = Problem.find_available_problems + if params[:commit] == 'download csv' + @problems = Problem.all + else + @problems = Problem.find_available_problems + end @users = User.find(:all, :include => [:contests, :contest_stat]) @scorearray = Array.new #set up range from param @@ -159,6 +175,13 @@ end @scorearray << ustat end + + if params[:commit] == 'download csv' then + csv = gen_csv_from_scorearray(@scorearray,@problems) + send_data csv, filename: 'max_score.csv' + else + render template: 'user_admin/user_stat' + end end def import @@ -473,4 +496,35 @@ end return [@contest, @users] end + + def gen_csv_from_scorearray(scorearray,problem) + CSV.generate do |csv| + #add header + header = ['User','Name', 'Activated?', 'Logged in', 'Contest'] + problem.each { |p| header << p.name } + header += ['Total','Passed'] + csv << header + #add data + scorearray.each do |sc| + total = num_passed = 0 + row = Array.new + sc.each_index do |i| + if i == 0 + row << sc[i].login + row << sc[i].full_name + row << sc[i].activated + row << (sc[i].try(:contest_stat).try(:started_at)!=nil ? 'yes' : 'no') + row << sc[i].contests.collect {|c| c.name}.join(', ') + else + row << sc[i][0] + total += sc[i][0] + num_passed += 1 if sc[i][1] + end + end + row << total + row << num_passed + csv << row + end + end + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -14,6 +14,7 @@ :register, :forget, :retrieve_password] + before_filter :authenticate, :profile_authorization, only: [:profile] verify :method => :post, :only => [:chg_passwd], :redirect_to => { :action => :index } @@ -108,6 +109,30 @@ redirect_to :action => 'forget' end + def profile + @user = User.find(params[:id]) + @submission = Submission.includes(:problem).where(user_id: params[:id]) + + range = 120 + @histogram = { data: Array.new(range,0), summary: {} } + @summary = {count: 0, solve: 0, attempt: 0} + problem = Hash.new(0) + + @submission.find_each do |sub| + #histogram + d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60 + @histogram[:data][d.to_i] += 1 if d < range + + @summary[:count] += 1 + next unless sub.problem + problem[sub.problem] = [problem[sub.problem], (sub.points >= sub.problem.full_score) ? 1 : 0].max + end + + @histogram[:summary][:max] = [@histogram[:data].max,1].max + @summary[:attempt] = problem.count + problem.each_value { |v| @summary[:solve] += 1 if v == 1 } + end + protected def verify_online_registration @@ -152,5 +177,19 @@ send_mail(user.email, mail_subject, mail_body) end + + # allow viewing of regular user profile only when options allow so + # only admins can view admins profile + def profile_authorization + #if view admins' profile, allow only admin + return false unless(params[:id]) + user = User.find(params[:id]) + return false unless user + return admin_authorization if user.admin? + return true if GraderConfiguration["right.user_view_submission"] + + #finally, we allow only admin + admin_authorization + 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 @@ -13,6 +13,7 @@ append_to menu_items, '[Problems]', 'problems', 'index' append_to menu_items, '[Users]', 'user_admin', 'index' append_to menu_items, '[Results]', 'user_admin', 'user_stat' + append_to menu_items, '[Report]', 'report', 'login_stat' append_to menu_items, '[Graders]', 'graders', 'list' append_to menu_items, '[Contests]', 'contest_management', 'index' append_to menu_items, '[Sites]', 'sites', 'index' @@ -29,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/helpers/report_helper.rb b/app/helpers/report_helper.rb new file mode 100644 --- /dev/null +++ b/app/helpers/report_helper.rb @@ -0,0 +1,2 @@ +module ReportHelper +end diff --git a/app/models/login.rb b/app/models/login.rb new file mode 100644 --- /dev/null +++ b/app/models/login.rb @@ -0,0 +1,3 @@ +class Login < ActiveRecord::Base + attr_accessible :ip_address, :logged_in_at, :user_id +end diff --git a/app/models/message.rb b/app/models/message.rb --- a/app/models/message.rb +++ b/app/models/message.rb @@ -21,7 +21,7 @@ Message.build_replying_message_hierarchy messages, replied_messages return messages end - + def self.find_all_system_unreplied_messages self.find(:all, :conditions => 'ISNULL(receiver_id) ' + diff --git a/app/models/problem.rb b/app/models/problem.rb --- a/app/models/problem.rb +++ b/app/models/problem.rb @@ -14,7 +14,7 @@ DEFAULT_MEMORY_LIMIT = 32 def self.find_available_problems - Problem.available.all(:order => "date_added DESC") + Problem.available.all(:order => "date_added DESC, name ASC") end def self.create_from_import_form_params(params, old_problem=nil) @@ -43,6 +43,7 @@ if not importer.import_from_file(import_params[:file], import_params[:time_limit], import_params[:memory_limit], + import_params[:checker_name], import_to_db) problem.errors.add(:base,'Import error.') end @@ -53,6 +54,13 @@ def self.download_file_basedir return "#{Rails.root}/data/tasks" end + + def get_submission_stat + result = Hash.new + #total number of submission + result[:total_sub] = Submission.where(problem_id: self.id).count + result[:attempted_user] = Submission.where(problem_id: self.id).group_by(:user_id) + end protected @@ -90,6 +98,11 @@ problem.errors.add(:base,'No testdata file.') end + checker_name = 'text' + if ['text','float'].include? params[:checker] + checker_name = params[:checker] + end + file = params[:file] if !problem.errors.empty? @@ -106,7 +119,8 @@ return [{ :time_limit => time_limit, :memory_limit => memory_limit, - :file => file + :file => file, + :checker_name => checker_name }, problem] end diff --git a/app/models/submission.rb b/app/models/submission.rb --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -25,7 +25,7 @@ def self.find_all_last_by_problem(problem_id) # need to put in SQL command, maybe there's a better way - Submission.find_by_sql("SELECT * FROM submissions " + + Submission.includes(:user).find_by_sql("SELECT * FROM submissions " + "WHERE id = " + "(SELECT MAX(id) FROM submissions AS subs " + "WHERE subs.user_id = submissions.user_id AND " + diff --git a/app/models/user.rb b/app/models/user.rb --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,8 @@ require 'digest/sha1' +require 'net/pop' +require 'net/https' +require 'net/http' +require 'json' class User < ActiveRecord::Base @@ -61,7 +65,9 @@ def self.authenticate(login, password) user = find_by_login(login) - return user if user && user.authenticated?(password) + if user + return user if user.authenticated?(password) + end end def authenticated?(password) diff --git a/app/views/application/_bar_graph.html.haml b/app/views/application/_bar_graph.html.haml new file mode 100644 --- /dev/null +++ b/app/views/application/_bar_graph.html.haml @@ -0,0 +1,44 @@ +- param = {} unless param +- graph_height = param[:graph_height] || 100 +- bar_width = param[:bar_width] || 14 +- graph_width = (bar_width * histogram[:data].count) + 20 +:css + .hist_bar { + width: #{bar_width-1}px; + position: absolute; + background-color: lightblue; + } + .hist_fill { + width: #{bar_width-1}px; + position: absolute; + background-color: #eee; + } + .hist_text { + position: absolute; + font-size:5px; + } + +%div{style: "position: relative; width: #{graph_width}px; height: 125px; background-color:#fff;" } + //draw background + - histogram[:data].each_index do |i| + - height = histogram[:data][i] * graph_height / histogram[:summary][:max] + - top = graph_height - height + - left = graph_width - (i+1)*bar_width + %div.hist_fill{style: "top: 0px; height: #{graph_height - height}px; left: #{left}px;" } + // draw horizontal line + - line = 3 + - line.times do |i| + - top = graph_height - graph_height * (i+0.5)/ line + %div{style: "position:absolute;width: #{graph_width-21}px;height: 1px;left: 20px;top:#{top}px;background-color: #333;"} + %div.hist_text{style: "position:absolute;left: 0px;top:#{top-6}px"} + =((i+0.5) * histogram[:summary][:max] / line).to_i + // draw the actual bar and text + - @histogram[:data].each_index do |i| + - height = histogram[:data][i] * graph_height / histogram[:summary][:max] + - top = graph_height - height + - left = graph_width - (i+1)*bar_width + %div.hist_bar{style: "top: #{top}px; height: #{height}px; left: #{left}px; dae: #{histogram[:data][i]}" } + - if i % 7 == 1 + %div.hist_text{style: "top:#{graph_height + 5}px;left: #{left}px;"} #{(Time.zone.today - i.day).strftime('%-d')} + - if (Time.now.in_time_zone - i.day).day == 15 + %div.hist_text{style: "top:#{graph_height + 15}px;left: #{left}px;"} #{(Time.zone.today - i.day).strftime('%b')} diff --git a/app/views/configurations/index.html.haml b/app/views/configurations/index.html.haml --- a/app/views/configurations/index.html.haml +++ b/app/views/configurations/index.html.haml @@ -1,3 +1,6 @@ +- content_for :header do + = javascript_include_tag 'local_jquery' + %h1 System configuration %table.info @@ -14,7 +17,7 @@ %td = in_place_editor_field :grader_configuration, :value_type, {}, :rows=>1 %td - = in_place_editor_field :grader_configuration, :value, {}, :rows=>1 + = best_in_place @grader_configuration, :value, ok_button: "ok", cancel_button: "cancel" %td= conf.description - if GraderConfiguration.config_cached? diff --git a/app/views/graders/list.html.haml b/app/views/graders/list.html.haml --- a/app/views/graders/list.html.haml +++ b/app/views/graders/list.html.haml @@ -1,5 +1,6 @@ - content_for :head do = stylesheet_link_tag 'graders' + = javascript_include_tag 'local_jquery' %h1 Grader information @@ -24,28 +25,49 @@ = submit_tag 'Clear all data' %br{:style => 'clear:both'}/ -- if @last_task - Last task: - = link_to "#{@last_task.id}", :action => 'view', :id => @last_task.id, :type => 'Task' +%div{style: 'width:500px; float: left;'} + - if @last_task + Last task: + = link_to "#{@last_task.id}", :action => 'view', :id => @last_task.id, :type => 'Task' + + %br/ + + - if @last_test_request + Last test_request: + = link_to "#{@last_test_request.id}", :action => 'view', :id => @last_test_request.id, :type => 'TestRequest' + + %h2 Current graders + + = render :partial => 'grader_list', :locals => {:grader_list => @grader_processes} + + %h2 Stalled graders + + = render :partial => 'grader_list', :locals => {:grader_list => @stalled_processes} + + %h2 Terminated graders - %br/ + = form_for :clear, :url => {:action => 'clear_terminated'} do |f| + = submit_tag 'Clear data for terminated graders' -- if @last_test_request - Last test_request: - = link_to "#{@last_test_request.id}", :action => 'view', :id => @last_test_request.id, :type => 'TestRequest' + = render :partial => 'grader_list', :locals => {:grader_list => @terminated_processes} +%div{} + %h2 Last 20 submissions + %table.graders + %thead + %th ID + %th User + %th Problem + %th Submitted + %th Graded + %th Result + %tbody + - @submission.each do |sub| + %tr.inactive + %td= link_to sub.id, controller: 'graders' ,action: 'submission', id: sub.id + %td= sub.try(:user).try(:full_name) + %td= sub.try(:problem).try(:full_name) + %td= "#{time_ago_in_words(sub.submitted_at)} ago" + %td= sub.graded_at ? "#{time_ago_in_words(sub.graded_at)} ago" : " " + %td= sub.grader_comment -%h2 Current graders - -= render :partial => 'grader_list', :locals => {:grader_list => @grader_processes} - -%h2 Stalled graders - -= render :partial => 'grader_list', :locals => {:grader_list => @stalled_processes} - -%h2 Terminated graders - -= form_for :clear, :url => {:action => 'clear_terminated'} do |f| - = submit_tag 'Clear data for terminated graders' - -= render :partial => 'grader_list', :locals => {:grader_list => @terminated_processes} 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,22 +1,67 @@ +%style{type: "text/css"} + = @css_style +:css + .field { + font-weight: bold; + text-align: right; + padding: 3px; + } + + %h1= "Submission: #{@submission.id}" -%p - User: - = "#{@submission.user.login}" - %br/ - Problem: - - if @submission.problem!=nil - = "#{@submission.problem.full_name}" - - else - = "(n/a)" - %br/ - = "Number: #{@submission.number}" - %br/ - = "Submitted at: #{format_short_time(@submission.submitted_at)}" + +%h2 Stat -%b Source code (first 10kb) -%div{:style => "border: 1px solid black; background: lightgrey"} - - if @submission.source - %pre - =h truncate @submission.source, :length => 10240 +%table.info + %thead + %tr.info-head + %th Field + %th Value + %tbody + %tr{class: cycle('info-even','info-odd')} + %td.field User: + %td.value + - if @submission.user + = link_to "(#{@submission.user.login})", controller: "users", action: "profile", id: @submission.user + = @submission.user.full_name + - else + = "(n/a)" + %tr{class: cycle('info-even','info-odd')} + %td.field Problem: + %td.value + - if @submission.problem!=nil + = link_to "(#{@submission.problem.name})", controller: "problems", action: "stat", id: @submission.problem + = @submission.problem.full_name + - else + = "(n/a)" + %tr{class: cycle('info-even','info-odd')} + %td.field Tries: + %td.value= @submission.number + %tr{class: cycle('info-even','info-odd')} + %td.field Submitted: + %td.value #{time_ago_in_words(@submission.submitted_at)} ago (at #{@submission.submitted_at.to_formatted_s(:long)}) + %tr{class: cycle('info-even','info-odd')} + %td.field Graded: + %td.value #{time_ago_in_words(@submission.graded_at)} ago (at #{@submission.graded_at.to_formatted_s(:long)}) + %tr{class: cycle('info-even','info-odd')} + %td.field Points: + %td.value #{@submission.points}/#{@submission.problem.full_score} + %tr{class: cycle('info-even','info-odd')} + %td.field Comment: + %td.value #{@submission.grader_comment} + %tr{class: cycle('info-even','info-odd')} + %td.field Runtime (s): + %td.value #{@submission.max_runtime} + %tr{class: cycle('info-even','info-odd')} + %td.field Memory (kb): + %td.value #{@submission.peak_memory} + - if session[:admin] + %tr{class: cycle('info-even','info-odd')} + %td.field IP: + %td.value #{@submission.ip_address} +%h2 Source code +//%div.highlight{:style => "border: 1px solid black;"} +=@formatted_code.html_safe + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,7 +5,9 @@ <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> + <%= content_for :header %> <%= yield :head %> + diff --git a/app/views/main/_problem.html.erb b/app/views/main/_problem.html.erb --- a/app/views/main/_problem.html.erb +++ b/app/views/main/_problem.html.erb @@ -3,7 +3,10 @@ <%= "#{problem_counter+1}" %> - <%= "#{problem.full_name} (#{problem.name})" %> + <%= "#{problem.name}"%> + + + <%= "#{problem.full_name}" %> <%= link_to_description_if_any "[#{t 'main.problem_desc'}]", problem %> diff --git a/app/views/main/_submission.html.haml b/app/views/main/_submission.html.haml --- a/app/views/main/_submission.html.haml +++ b/app/views/main/_submission.html.haml @@ -2,8 +2,14 @@ %tr{:class => ((submission_counter%2==0) ? "info-even" : "info-odd")} %td.info{:align => "center"} = submission_counter+1 - %td.info= format_short_time(submission.submitted_at) %td.info{:align => "center"} + = link_to "##{submission.id}", controller: :graders, action: :submission, id: submission.id + %td.info + = l submission.submitted_at, format: :long + = "( #{time_ago_in_words(submission.submitted_at)} ago)" + %td.info{:align => "center"} + = submission.source_filename + = " (#{submission.language.pretty_name}) " = link_to('[load]',{:action => 'source', :id => submission.id}) %td.info - if submission.graded_at!=nil diff --git a/app/views/main/list.html.haml b/app/views/main/list.html.haml --- a/app/views/main/list.html.haml +++ b/app/views/main/list.html.haml @@ -25,7 +25,8 @@ %table.info %tr.info-head %th - %th Tasks + %th Tasks name + %th Full name %th # of sub(s) %th Results = render :partial => 'problem', :collection => @problems @@ -37,7 +38,8 @@ %table.info %tr.info-head %th - %th Tasks + %th Tasks name + %th Full name %th # of sub(s) %th Results = render :partial => 'problem', :collection => cp[:problems] diff --git a/app/views/main/submission.html.haml b/app/views/main/submission.html.haml --- a/app/views/main/submission.html.haml +++ b/app/views/main/submission.html.haml @@ -13,6 +13,7 @@ - if @submissions.length>0 %table.info %tr.info-head + %th.info No. %th.info # %th.info At %th.info Source diff --git a/app/views/problems/import.html.haml b/app/views/problems/import.html.haml --- a/app/views/problems/import.html.haml +++ b/app/views/problems/import.html.haml @@ -32,6 +32,19 @@ %br/ You may put task description in *.html for raw html and *.md or *.markdown for markdown. + %br/ + You may also put a pdf file for the task description + %tr + %td Checker: + %td= select_tag 'checker', options_for_select([['Text checker','text'],['Float checker','float']], 'text') + %tr + %td + %td + %span{:class => 'help'} + "Text" checker checks if the text (including numbers) is the same, ignoring any whitespace + %br/ + "Float" checker checks if all numbers is within EPSILON error using formula |a-b| < EPSILON * max(|a|,|b|) + - if @allow_test_pair_import %tr %td diff --git a/app/views/problems/manage.html.haml b/app/views/problems/manage.html.haml --- a/app/views/problems/manage.html.haml +++ b/app/views/problems/manage.html.haml @@ -1,5 +1,38 @@ - content_for :head do = stylesheet_link_tag 'problems' + = javascript_include_tag 'local_jquery' + +:javascript + $(document).ready( function() { + function shiftclick(start,stop,value) { + $('tr input').each( function(id,input) { + var $input=$(input); + var iid=parseInt($input.attr('id').split('-')[2]); + if(iid>=start&&iid<=stop){ + $input.prop('checked',value) + } + }); + } + + $('tr input').click( function(e) { + if (e.shiftKey) { + stop = parseInt($(this).attr('id').split('-')[2]); + var orig_stop = stop + if (typeof start !== 'undefined') { + if (start > stop) { + var tmp = start; + start = stop; + stop = tmp; + } + shiftclick(start,stop,$(this).is(':checked') ) + } + start = orig_stop + } else { + start = parseInt($(this).attr('id').split('-')[2]); + } + }); + }); + %h1 Manage problems @@ -7,14 +40,19 @@ = form_tag :action=>'do_manage' do .submitbox - What do you want to do? + What do you want to do to the selected problem? %br/ + (You can shift-click to select a range of problems) %ul %li Change date added to = select_date Date.current, :prefix => 'date_added'     = submit_tag 'Change', :name => 'change_date_added' + %li + Set available to + = submit_tag 'True', :name => 'enable_problem' + = submit_tag 'False', :name => 'disable_problem' - if GraderConfiguration.multicontests? %li @@ -23,19 +61,23 @@ = submit_tag 'Add', :name => 'add_to_contest' %table - %tr - %th/ + %tr{style: "text-align: left;"} + %th= check_box_tag 'select_all' %th Name %th Full name + %th Available %th Date added - if GraderConfiguration.multicontests? %th Contests + - num = 0 - for problem in @problems + - num += 1 %tr{:id => "row-prob-#{problem.id}", :name=> "prob-#{problem.id}"} - %td= check_box_tag "prob-#{problem.id}" + %td= check_box_tag "prob-#{problem.id}-#{num}" %td= problem.name %td= problem.full_name + %td= problem.available %td= problem.date_added - if GraderConfiguration.multicontests? %td diff --git a/app/views/problems/stat.html.erb b/app/views/problems/stat.html.erb deleted file mode 100644 --- a/app/views/problems/stat.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -

Problem stat: <%= @problem.name %>

- -This is just a hack. Really not efficient.

- -<% if @submissions!=nil %> - - - - - - - - - <% count = 0 %> - <% @submissions.each do |sub| %> - "> - - - - - - - <% count += 1 %> - <% end %> -
loginnamesubmitted_atpointscomment
<%= sub.user.login %><%= sub.user.full_name if sub.user %><%= sub.submitted_at.to_s %><%= sub.points %>
<%= sub.grader_comment %>
-<% else %> -No submission -<% end %> diff --git a/app/views/problems/stat.html.haml b/app/views/problems/stat.html.haml new file mode 100644 --- /dev/null +++ b/app/views/problems/stat.html.haml @@ -0,0 +1,51 @@ +:css + .fix-width { + font-family: "Consolas, Monaco, Droid Sans Mono,Mono, Monospace,Courier" + } + +%h1 Problem stat: #{@problem.name} +%h2 Overview + + +%table.info + %thead + %tr.info-head + %th Stat + %th Value + %tbody + %tr{class: cycle('info-even','info-odd')} + %td Submissions + %td= @submissions.count + %tr{class: cycle('info-even','info-odd')} + %td Solved/Attempted User + %td #{@summary[:solve]}/#{@summary[:attempt]} (#{(@summary[:solve]*100.0/@summary[:attempt]).round(1)}%) + +%h2 Submissions Count += render partial: 'application/bar_graph', locals: { histogram: @histogram } + +%h2 Submissions +- if @submissions and @submissions.count > 0 + %table.info#main_table + %thead + %tr.info-head + %th ID + %th Login + %th Name + %th Submitted_at + %th Points + %th comment + %tbody + - row_odd,curr = true,'' + - @submissions.each do |sub| + - next unless sub.user + - row_odd,curr = !row_odd, sub.user if curr != sub.user + %tr{class: row_odd ? "info-odd" : "info-even"} + %td= link_to sub.id, controller: 'graders', action: 'submission', id: sub.id + %td= link_to sub.user.login, controller: :users, action: :profile, id: sub.user.id + %td= sub.user.full_name + %td= time_ago_in_words(sub.submitted_at) + " ago" + %td= sub.points + %td.fix-width= sub.grader_comment +- else + No submission + 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/_date_range.html.haml b/app/views/report/_date_range.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/_date_range.html.haml @@ -0,0 +1,23 @@ + += form_tag({session: :url }) do + .submitbox + %table + %tr + %td{colspan: 6, style: 'font-weight: bold'}= title + %tr + %td{style: 'width: 120px; font-weight: bold'}= param_text + %td{align: 'right'} since: + %td= text_field_tag 'since_datetime' + %tr + %td + %td{align: 'right'} until: + %td= text_field_tag 'until_datetime' + %tr + %td + %td + %td Blank mean no condition + %tr + %td + %td + %td= submit_tag 'query' + diff --git a/app/views/report/_report_menu.html.haml b/app/views/report/_report_menu.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/_report_menu.html.haml @@ -0,0 +1,7 @@ + +.task-menu + Reports + %br/ + = link_to '[Hall of Fame]', :action => 'problem_hof' + = link_to '[Struggle]', :action => 'stuck' + = 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,127 @@ +- content_for :header do + = javascript_include_tag 'local_jquery' + +:javascript + $(document).ready( function() { + $("#mem_remark").hover( function() { + $("#mem_remark_box").show(); + }, function() { + $("#mem_remark_box").hide(); + }); + }); +:css + .hof_user { color: orangered; font-style: italic; } + .hof_language { color: green; font-style: italic; } + .hof_value { color: deeppink;font-style: italic; } + .info_param { font-weight: bold;text-align: right; } + .tooltip { + font-family: Verdana,sans-serif; + font-weight: normal; + text-align: left; + font-size: 1.0em; + color: black; + line-height: 1.1; + display: none; + min-width: 20em; + position: absolute; + left: 25px; + bottom: 5px; + border: 1px solid; + padding: 5px; + background-color: #FFF; + word-wrap: break-word; + z-index: 9999; + overflow: auto; + } + +%h1 (#{Problem.find(params[:id]).name}) #{Problem.find(params[:id]).full_name} + +%h2 Problem Stat +%table.info + %thead + %tr.info-head + %th Stat + %th Value + %tbody + %tr{class: cycle('info-even','info-odd')} + %td.info_param Submissions + %td= @summary[:count] + %tr{class: cycle('info-even','info-odd')} + %td.info_param Solved/Attempted User + %td #{@summary[:solve]}/#{@summary[:attempt]} (#{(@summary[:solve]*100.0/@summary[:attempt]).round(1)}%) + - if @best + %tr{class: cycle('info-even','info-odd')} + %td.info_param Best Runtime + %td + by #{link_to @best[:runtime][:user], controller:'users', action:'profile', id:@best[:memory][:user_id]} + 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]) + + %tr{class: cycle('info-even','info-odd')} + %td.info_param + Best Memory Usage + %sup{ id: "mem_remark", style: "position:relative; color: blue;"} + [?] + %span.tooltip#mem_remark_box + This counts only for submission with 100% score. + Right now, java is excluded from memory usage competition. (Because it always uses 2GB memory...) + %td + by #{link_to @best[:memory][:user], controller:'users', action:'profile', id:@best[:memory][:user_id]} + using #{@best[:memory][:lang]} + with #{number_with_delimiter(@best[:memory][:value])} kbytes + at submission + = link_to("#" + @best[:memory][:sub_id].to_s, controller: 'graders' , action: 'submission', id:@best[:memory][:sub_id]) + + %tr{class: cycle('info-even','info-odd')} + %td.info_param Shortest Code + %td + by #{link_to @best[:length][:user], controller:'users', action:'profile', id:@best[:length][:user_id]} + 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]) + + %tr{class: cycle('info-even','info-odd')} + %td.info_param First solver + %td + #{link_to @best[:first][:user], controller:'users', action:'profile', id:@best[:first][:user_id]} 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]) + +- if @best + %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 + = link_to value[:runtime][:user], controller: 'users', action: 'profile', id: value[:runtime][:user_id] + = "(#{(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 + = link_to value[:memory][:user], controller: 'users', action: 'profile', id: value[:memory][:user_id] + = "(#{number_with_delimiter(value[:memory][:value])} @" + = "#{link_to("#" + value[:memory][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:memory][:sub_id])} )".html_safe + %td + = link_to value[:length][:user], controller: 'users', action: 'profile', id: value[:length][:user_id] + = "(#{value[:length][:value]} @" + = "#{link_to("#" + value[:length][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:length][:sub_id])} )".html_safe + %td + - if value[:first][:user] != '(NULL)' #TODO: i know... this is wrong... + = link_to value[:first][:user], controller: 'users', action: 'profile', id: value[:first][:user_id] + = "(#{value[:first][:value]} @" + = "#{link_to("#" + value[:first][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:first][:sub_id])} )".html_safe + diff --git a/app/views/report/login_stat.html.haml b/app/views/report/login_stat.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/login_stat.html.haml @@ -0,0 +1,36 @@ +- content_for :header do + = stylesheet_link_tag 'tablesorter-theme.cafe' + = javascript_include_tag 'local_jquery' + +%script{:type=>"text/javascript"} + $(function () { + $('#since_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); + $('#until_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); + $('#my_table').tablesorter({widthFixed: true, widgets: ['zebra']}); + }); + +%h1 Login status + +=render partial: 'report_menu' +=render partial: 'date_range', locals: {param_text: 'Login date range:', title: 'Query login stat in the range' } + +%table.tablesorter-cafe#my_table + %thead + %tr + %th login + %th full name + %th login count + %th earliest + %th latest + %th IP + %tbody + - @logins.each do |l| + %tr{class: cycle('info-even','info-odd')} + %td= link_to l[:login], controller: 'users', action: 'profile', id: l[:id] + %td= l[:full_name] + %td= l[:count] + %td= l[:min] ? l[:min].in_time_zone.strftime('%Y-%m-%d %H:%M') : '' + %td= l[:max] ? "#{l[:max].in_time_zone.strftime('%Y-%m-%d %H:%M.%S')} (#{time_ago_in_words(l[:max].in_time_zone)} ago)" : '' + %td + - l[:ip].each do |ip| + #{ip.ip_address}
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,23 @@ + +/- 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 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' + %h2 Submission History + =render partial: 'application/bar_graph', locals: { histogram: @histogram } + diff --git a/app/views/report/stuck.html.haml b/app/views/report/stuck.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/stuck.html.haml @@ -0,0 +1,17 @@ +%table.info + %thead + %tr.info-head + %th Problem + %th User + %th tries + %tbody + - @struggle.each do |s| + %tr + %td + = link_to "(#{s[:problem].name})", controller: :problems, action: :stat, id: s[:problem] + = s[:problem].full_name + %td + = link_to "(#{s[:user].login})", controller: :users, action: :profile, id: s[:user] + = s[:user].full_name + %td + = s[:tries] diff --git a/app/views/report/submission_stat.html.haml b/app/views/report/submission_stat.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/submission_stat.html.haml @@ -0,0 +1,37 @@ +- content_for :header do + = javascript_include_tag 'local_jquery' + +%script{:type=>"text/javascript"} + $(function () { + $('#since_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); + $('#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' } + +%table.info + %thead + %tr.info-head + %th login + %th full name + %th total submissions + %th submissions + %tbody + - @submissions.each do |user_id,data| + %tr{class: cycle('info-even','info-odd')} + %td= data[:login] + %td= data[:full_name] + %td= data[:count] + %td + - data[:sub].each do |prob_id,sub_data| + = "#{sub_data[:prob_name]}: [" + - st = [] + - sub_data[:sub_ids].each do |id| + - st << link_to(id, controller: 'graders' , action: 'submission', id: id) + = raw st.join ', ' + = ']' + %br/ + diff --git a/app/views/user_admin/_form.html.erb b/app/views/user_admin/_form.html.erb --- a/app/views/user_admin/_form.html.erb +++ b/app/views/user_admin/_form.html.erb @@ -18,5 +18,8 @@


<%= text_field 'user', 'alias' %>

+ +


+<%= text_field 'user', 'remark' %>

diff --git a/app/views/user_admin/edit.html.erb b/app/views/user_admin/edit.html.erb deleted file mode 100644 --- a/app/views/user_admin/edit.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -

Editing user

- -<%= form_tag :action => 'update', :id => @user do %> - <%= render :partial => 'form' %> - <%= submit_tag 'Edit' %> -<% end %> - -<%= link_to 'Show', :action => 'show', :id => @user %> | -<%= link_to 'Back', :action => 'list' %> diff --git a/app/views/user_admin/edit.html.haml b/app/views/user_admin/edit.html.haml new file mode 100644 --- /dev/null +++ b/app/views/user_admin/edit.html.haml @@ -0,0 +1,11 @@ +%h1 Editing user + += form_tag :action => 'update', :id => @user do + = error_messages_for 'user' + = render partial: "form" + = submit_tag "Edit" + + += link_to 'Show', :action => 'show', :id => @user +| += link_to 'Back', :action => 'list' diff --git a/app/views/user_admin/list.html.erb b/app/views/user_admin/list.html.erb --- a/app/views/user_admin/list.html.erb +++ b/app/views/user_admin/list.html.erb @@ -68,8 +68,9 @@ <% for user in @users %> "> + <%= link_to user.login, controller: :users, :action => 'profile', :id => user %> <% for column in User.content_columns %> - <% if !@hidden_columns.index(column.name) %> + <% if !@hidden_columns.index(column.name) and column.name != 'login' %> <%=h user.send(column.name) %> <% end %> <% end %> diff --git a/app/views/user_admin/user_stat.html.erb b/app/views/user_admin/user_stat.html.erb deleted file mode 100644 --- a/app/views/user_admin/user_stat.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -

User grading results

-

Show scores from latest submission

- -<%= render 'submission_range' %> - -

Latest scores

- - - - - - - - -<% @problems.each do |p| %> - -<% end %> - - - -<% counter = 0 %> -<% @scorearray.each do |sc| %> - "> - <% total = 0 %> - <% num_passed = 0 %> - <% sc.each_index do |i| %> - <% if i==0 %> - - - - - - <% else %> - - <% total += sc[i][0] %> - <% num_passed += 1 if sc[i][1] %> - <% end %> - <% end %> - - - - <% counter += 1 %> -<% end %> -
UserNameActivated?Logged inContest(s)<%= p.name %>TotalPassed
<%= sc[i].login %><%= sc[i].full_name %><%= sc[i].activated %> - <%= sc[i].try(:contest_stat).try(:started_at)!=nil ? 'yes' : 'no' %> - - <%= sc[i].contests.collect {|c| c.name}.join(', ') %> - <%= sc[i][0] %><%= total %><%= num_passed %>
diff --git a/app/views/user_admin/user_stat.html.haml b/app/views/user_admin/user_stat.html.haml new file mode 100644 --- /dev/null +++ b/app/views/user_admin/user_stat.html.haml @@ -0,0 +1,59 @@ +- content_for :header do + = javascript_include_tag 'local_jquery' + = stylesheet_link_tag 'tablesorter-theme.cafe' + +%script{:type=>"text/javascript"} + $(function () { + $('#since_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); + $('#until_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); + $('#my_table').tablesorter({widgets: ['zebra']}); + }); + +%h1 User grading results +%h2= params[:action] == 'user_stat' ? "Show scores from latest submission" : "Show max scores in submission range" + + +- if @problem and @problem.errors + =error_messages_for 'problem' + += render partial: 'submission_range' + +- if params[:action] == 'user_stat' + %h3 Latest score + = link_to '[download csv with all problems]', controller: :user_admin, action: :user_stat, commit: 'download csv' +- else + %h3 Max score + = link_to '[Show only latest submissions]', controller: :user_admin, action: :user_stat + = link_to '[download csv with all problems]', controller: :user_admin, action: :user_stat_max, commit: 'download csv' + +%table.tablesorter-cafe#my_table + %thead + %tr + %th User + %th Name + %th Activated? + %th Logged in + %th Contest(s) + %th Remark + - @problems.each do |p| + %th= p.name + %th Total + %th Passed + %tbody + - @scorearray.each do |sc| + %tr{class: cycle('info-even','info-odd')} + - total,num_passed = 0,0 + - sc.each_index do |i| + - if i == 0 + %td= link_to sc[i].login, controller: 'users', action: 'profile', id: sc[i] + %td= sc[i].full_name + %td= sc[i].activated + %td= sc[i].try(:contest_stat).try(:started_at)!=nil ? 'yes' : 'no' + %td= sc[i].contests.collect {|c| c.name}.join(', ') + %td= sc[i].remark + - else + %td= sc[i][0] + - total += sc[i][0] + - num_passed += 1 if sc[i][1] + %td= total + %td= num_passed diff --git a/app/views/users/profile.html.haml b/app/views/users/profile.html.haml new file mode 100644 --- /dev/null +++ b/app/views/users/profile.html.haml @@ -0,0 +1,66 @@ +- content_for :header do + = javascript_include_tag 'local_jquery' + +:javascript + $(function () { + $('#submission_table').tablesorter({widgets: ['zebra']}); + }); + +:css + .fix-width { + font-family: Droid Sans Mono,Consolas, monospace, mono, Courier New, Courier; + } + +%h1= @user.full_name + +Login: #{@user.login}
+Full name: #{@user.full_name}
+ + +%h2 Problem Stat +%table.info + %thead + %tr.info-head + %th Stat + %th Value + %tbody + %tr{class: cycle('info-even','info-odd')} + %td.info_param Submissions + %td= @summary[:count] + %tr{class: cycle('info-even','info-odd')} + %td.info_param Solved/Attempted Problem + %td #{@summary[:solve]}/#{@summary[:attempt]} (#{(@summary[:solve]*100.0/@summary[:attempt]).round(1)}%) + +%h2 Submission History + +=render partial: 'application/bar_graph', locals: {histogram: @histogram, param: {bar_width: 7}} + + +%table.tablesorter-cafe#submission_table + %thead + %tr + %th ID + %th Problem code + %th Problem full name + %th Language + %th Submitted at + %th Result + %th Score + - if session[:admin] + %th IP + %tbody + - @submission.each do |s| + - next unless s.problem + %tr + %td= link_to "#{s.id}", controller: "graders", action: "submission", id: s.id + %td= link_to s.problem.name, controller: "problems", action: "stat", id: s.problem + %td= s.problem.full_name + %td= s.language.pretty_name + %td #{s.submitted_at.strftime('%Y-%m-%d %H:%M')} (#{time_ago_in_words(s.submitted_at)} ago) + %td.fix-width= s.grader_comment + %td= (s.points*100)/s.problem.full_score + - if session[:admin] + %td= s.ip_address + + + diff --git a/config/application.rb.SAMPLE b/config/application.rb.SAMPLE --- a/config/application.rb.SAMPLE +++ b/config/application.rb.SAMPLE @@ -60,5 +60,6 @@ config.assets.version = '1.0' config.assets.precompile += ['announcement_refresh.js','effects.js','site_update.js','graders.css','problems.css'] + config.assets.precompile += ['local_jquery.js','tablesorter-theme.cafe.css'] 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/config/routes.rb b/config/routes.rb --- a/config/routes.rb +++ b/config/routes.rb @@ -1,9 +1,13 @@ CafeGrader::Application.routes.draw do + get "report/login" + resources :contests resources :announcements resources :sites + resources :grader_configuration, controller: 'configurations' + # The priority is based upon order of creation: # first created -> highest priority. 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/migrate/20140826095949_create_logins.rb b/db/migrate/20140826095949_create_logins.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20140826095949_create_logins.rb @@ -0,0 +1,10 @@ +class CreateLogins < ActiveRecord::Migration + def change + create_table :logins do |t| + t.string :user_id + t.string :ip_address + + t.timestamps + end + end +end diff --git a/db/migrate/20140917150629_add_ip_to_submissions.rb b/db/migrate/20140917150629_add_ip_to_submissions.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20140917150629_add_ip_to_submissions.rb @@ -0,0 +1,5 @@ +class AddIpToSubmissions < ActiveRecord::Migration + def change + add_column :submissions, :ip_address, :string + end +end diff --git a/db/migrate/20150203153534_add_more_to_users.rb b/db/migrate/20150203153534_add_more_to_users.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20150203153534_add_more_to_users.rb @@ -0,0 +1,6 @@ +class AddMoreToUsers < ActiveRecord::Migration + def change + add_column :users, :enabled, :boolean, default: 1 + add_column :users, :remark, :string + end +end diff --git a/db/schema.rb b/db/schema.rb --- a/db/schema.rb +++ b/db/schema.rb @@ -11,33 +11,25 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20121001033508) do +ActiveRecord::Schema.define(:version => 20150203153534) do create_table "announcements", :force => true do |t| t.string "author" t.text "body" t.boolean "published" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.boolean "frontpage", :default => false t.boolean "contest_only", :default => false t.string "title" t.string "notes" end - create_table "codejom_statuses", :force => true do |t| - t.integer "user_id" - t.boolean "alive" - t.integer "num_problems_passed" - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "contests", :force => true do |t| t.string "title" t.boolean "enabled" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "name" end @@ -53,23 +45,23 @@ create_table "countries", :force => true do |t| t.string "name" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "descriptions", :force => true do |t| t.text "body" t.boolean "markdowned" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "grader_configurations", :force => true do |t| t.string "key" t.string "value_type" t.string "value" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.text "description" end @@ -78,8 +70,8 @@ t.integer "pid" t.string "mode" t.boolean "active" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "task_id" t.string "task_type" t.boolean "terminated" @@ -94,29 +86,34 @@ t.string "common_ext" end + create_table "logins", :force => true do |t| + t.string "user_id" + t.string "ip_address" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "messages", :force => true do |t| t.integer "sender_id" t.integer "receiver_id" t.integer "replying_message_id" t.text "body" t.boolean "replied" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "problems", :force => true do |t| - t.string "name", :limit => 30 - t.string "full_name" - t.integer "full_score" - t.date "date_added" - t.boolean "available" - t.string "url" - t.integer "description_id" - t.boolean "test_allowed" - t.boolean "output_only" - t.integer "level", :default => 0 - t.datetime "updated_at" - t.string "description_filename" + t.string "name", :limit => 30 + t.string "full_name" + t.integer "full_score" + t.date "date_added" + t.boolean "available" + t.string "url" + t.integer "description_id" + t.boolean "test_allowed" + t.boolean "output_only" + t.string "description_filename" end create_table "rights", :force => true do |t| @@ -156,21 +153,12 @@ t.string "name" t.boolean "started" t.datetime "start_time" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.integer "country_id" t.string "password" end - create_table "submission_statuses", :force => true do |t| - t.integer "user_id" - t.integer "problem_id" - t.boolean "passed" - t.integer "submission_count" - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "submissions", :force => true do |t| t.integer "user_id" t.integer "problem_id" @@ -185,6 +173,10 @@ t.text "grader_comment" t.integer "number" t.string "source_filename" + t.float "max_runtime" + t.integer "peak_memory" + t.integer "effective_code_length" + t.string "ip_address" end add_index "submissions", ["user_id", "problem_id", "number"], :name => "index_submissions_on_user_id_and_problem_id_and_number", :unique => true @@ -197,24 +189,12 @@ t.datetime "updated_at" end - create_table "test_pair_assignments", :force => true do |t| - t.integer "user_id" - t.integer "problem_id" - t.integer "test_pair_id" - t.integer "test_pair_number" - t.integer "request_number" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "submitted" - end - create_table "test_pairs", :force => true do |t| t.integer "problem_id" t.text "input", :limit => 16777215 t.text "solution", :limit => 16777215 - t.datetime "created_at" - t.datetime "updated_at" - t.integer "number" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "test_requests", :force => true do |t| @@ -225,13 +205,13 @@ t.string "output_file_name" t.string "running_stat" t.integer "status" - t.datetime "updated_at" + t.datetime "updated_at", :null => false t.datetime "submitted_at" t.datetime "compiled_at" t.text "compiler_message" t.datetime "graded_at" t.string "grader_comment" - t.datetime "created_at" + t.datetime "created_at", :null => false t.float "running_time" t.string "exit_status" t.integer "memory_usage" @@ -242,30 +222,25 @@ create_table "user_contest_stats", :force => true do |t| t.integer "user_id" t.datetime "started_at" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.boolean "forced_logout" end create_table "users", :force => true do |t| - t.string "login", :limit => 50 + t.string "login", :limit => 50 t.string "full_name" t.string "hashed_password" - t.string "salt", :limit => 5 + t.string "salt", :limit => 5 t.string "alias" t.string "email" t.integer "site_id" t.integer "country_id" - t.boolean "activated", :default => false + t.boolean "activated", :default => false t.datetime "created_at" t.datetime "updated_at" - t.string "member1_full_name" - t.string "member2_full_name" - t.string "member3_full_name" - t.boolean "high_school" - t.string "member1_school_name" - t.string "member2_school_name" - t.string "member3_school_name" + t.boolean "enabled", :default => true + t.string "remark" end add_index "users", ["login"], :name => "index_users_on_login", :unique => true 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 diff --git a/lib/grader_script.rb b/lib/grader_script.rb --- a/lib/grader_script.rb +++ b/lib/grader_script.rb @@ -29,8 +29,8 @@ end def self.start_grader(env) - GraderScript.call_grader "#{env} queue &" - GraderScript.call_grader "#{env} test_request &" + GraderScript.call_grader "#{env} queue --err-log &" + GraderScript.call_grader "#{env} test_request -err-log &" end def self.call_import_problem(problem_name, @@ -50,7 +50,7 @@ Dir.chdir(cur_dir) - return output + return "import CMD: #{cmd}\n" + output end return '' end diff --git a/lib/testdata_importer.rb b/lib/testdata_importer.rb --- a/lib/testdata_importer.rb +++ b/lib/testdata_importer.rb @@ -11,6 +11,7 @@ def import_from_file(tempfile, time_limit, memory_limit, + checker_name='text', import_to_db=false) dirname = extract(tempfile) @@ -19,7 +20,8 @@ @log_msg = GraderScript.call_import_problem(@problem.name, dirname, time_limit, - memory_limit) + memory_limit, + checker_name) else # Import test data to test pairs. diff --git a/spec/models/login_spec.rb b/spec/models/login_spec.rb new file mode 100644 --- /dev/null +++ b/spec/models/login_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Login do + pending "add some examples to (or delete) #{__FILE__}" +end