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 %>
-
-
- login
- name
- submitted_at
- points
- comment
-
- <% count = 0 %>
- <% @submissions.each do |sub| %>
- ">
- <%= sub.user.login %>
- <%= sub.user.full_name if sub.user %>
- <%= sub.submitted_at.to_s %>
- <%= sub.points %>
- <%= sub.grader_comment %>
-
- <% count += 1 %>
- <% end %>
-
-<% 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 @@
Alias
<%= text_field 'user', 'alias' %>
+
+Remark
+<%= 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
-
-
-
- User
- Name
- Activated?
- Logged in
- Contest(s)
-<% @problems.each do |p| %>
- <%= p.name %>
-<% end %>
-Total
-Passed
-
-<% counter = 0 %>
-<% @scorearray.each do |sc| %>
- ">
- <% total = 0 %>
- <% num_passed = 0 %>
- <% sc.each_index do |i| %>
- <% if i==0 %>
- <%= 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(', ') %>
-
- <% else %>
- <%= sc[i][0] %>
- <% total += sc[i][0] %>
- <% num_passed += 1 if sc[i][1] %>
- <% end %>
- <% end %>
- <%= total %>
- <%= num_passed %>
-
- <% counter += 1 %>
-<% end %>
-
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