| @@ -31,7 +31,8 @@ To show all known classes, semesters, and assignments: | |||
| rake list | |||
| To load a roster for a specific semester: | |||
| To load a roster (a newline-delimited list of student identifiers) for a | |||
| specific semester: | |||
| rake roster cs123 myroster.csv semester=sp16 | |||
| @@ -26,19 +26,19 @@ module KGrader | |||
| end | |||
| def roster(course, semester, rosterfile) | |||
| course = fetch_course course | |||
| course = Course.new(@fs, course) | |||
| semester ||= course.current_semester | |||
| course.roster(semester).load rosterfile | |||
| end | |||
| def grade(course, semester, assignment, options = {}) | |||
| course = fetch_course course | |||
| course = Course.new(@fs, course) | |||
| semester ||= course.current_semester | |||
| course.task(semester, assignment).grade options | |||
| end | |||
| def commit(course, semester, assignment, options = {}) | |||
| course = fetch_course course | |||
| course = Course.new(@fs, course) | |||
| semester ||= course.current_semester | |||
| course.task(semester, assignment).commit options | |||
| end | |||
| @@ -58,12 +58,6 @@ module KGrader | |||
| end | |||
| private | |||
| def fetch_course(course) | |||
| Course.new(@fs, course) | |||
| rescue FilesystemError | |||
| KGrader::die "unknown course" | |||
| end | |||
| def reset_jail | |||
| FileUtils.rm_rf @fs.jail | |||
| FileUtils.mkdir @fs.jail | |||
| @@ -11,6 +11,8 @@ module KGrader | |||
| @config = @fs.load @fs.course_config(@name) | |||
| @rosters = {} | |||
| rescue FilesystemError | |||
| raise CourseError, "unknown or invalid course: #{name}" | |||
| end | |||
| def roster(semester) | |||
| @@ -30,12 +32,7 @@ module KGrader | |||
| end | |||
| def current_semester | |||
| case @config['semesters'] | |||
| when 'faspYY' | |||
| KGrader::season + DateTime.now.strftime('%y') | |||
| when 'faspYYYY' | |||
| KGrader::season + DateTime.now.strftime('%Y') | |||
| end | |||
| KGrader::current_semester @config['semesters'] | |||
| end | |||
| end | |||
| end | |||
| @@ -2,6 +2,18 @@ module KGrader | |||
| class KGraderError < StandardError | |||
| end | |||
| class ArgumentError < KGraderError | |||
| end | |||
| class FilesystemError < KGraderError | |||
| end | |||
| class ConfigError < KGraderError | |||
| end | |||
| class CourseError < KGraderError | |||
| end | |||
| class RosterError < KGraderError | |||
| end | |||
| end | |||
| @@ -52,10 +52,10 @@ module KGrader | |||
| when '.csv' | |||
| File.read(path).split("\n").map! { |line| line.split "," } | |||
| else | |||
| raise FilesystemError, "unknown file type" | |||
| raise FilesystemError, "unknown file type: #{path}" | |||
| end | |||
| rescue SystemCallError # Errno::ENOENT, etc. | |||
| raise FilesystemError, "can't read file" | |||
| raise FilesystemError, "can't read file: #{path}" | |||
| end | |||
| end | |||
| end | |||
| @@ -13,10 +13,14 @@ module KGrader | |||
| @students = @fs.load(filename).map! { |item| item.first } | |||
| FileUtils.mkdir_p File.dirname(rosterfile) | |||
| File.write rosterfile, @students.join("\n") | |||
| rescue FilesystemError => err | |||
| raise RosterError, err | |||
| end | |||
| def students | |||
| @students ||= @fs.load(rosterfile).map! { |item| item.first } | |||
| rescue FilesystemError | |||
| raise RosterError, "unknown semester: #{semester}" | |||
| end | |||
| private | |||
| @@ -6,23 +6,32 @@ module KGrader | |||
| @course = course | |||
| @semester = semester | |||
| @assignment = assignment | |||
| @roster = @course.roster @semester | |||
| @students = @course.roster(@semester).students | |||
| end | |||
| def grade(options = {}) | |||
| students = @roster.students | |||
| students = @students | |||
| students &= options[:students] unless options[:students].nil? | |||
| due = options.fetch(:due, Time.now) | |||
| fetch = options.fetch(:fetch, true) | |||
| regrade = options.fetch(:regrade, false) | |||
| # TODO | |||
| puts "Grading #{@course.name}:#{@semester} assignment #{@assignment}..." | |||
| puts "- options: #{options}" | |||
| puts "- students: #{students.inspect}" | |||
| puts "- due: #{due}" | |||
| puts "- fetch: #{fetch}" | |||
| puts "- regrade: #{regrade}" | |||
| end | |||
| def commit(options = {}) | |||
| students = @students | |||
| students &= options[:students] unless options[:students].nil? | |||
| # TODO | |||
| puts "Committing #{@course.name}:#{@semester} assignment #{@assignment}..." | |||
| puts "- options: #{options}" | |||
| puts "- students: #{students.inspect}" | |||
| end | |||
| end | |||
| end | |||
| @@ -1,10 +1,6 @@ | |||
| require 'date' | |||
| require 'time' | |||
| module KGrader | |||
| def self.die(error) | |||
| Kernel::abort "fatal: #{error}" | |||
| end | |||
| def self.parse_args(raw, num, keywords) | |||
| args = [] | |||
| options = {} | |||
| @@ -13,7 +9,9 @@ module KGrader | |||
| if arg.include? '=' | |||
| key, val = arg.split('=', 2) | |||
| key = key.to_sym | |||
| die "unknown keyword #{key}" unless keywords.include? key | |||
| unless keywords.include? key | |||
| raise ArgumentError, "unknown keyword: #{key}" | |||
| end | |||
| options[key] = case keywords[key] | |||
| when :string | |||
| val | |||
| @@ -21,20 +19,28 @@ module KGrader | |||
| %w(true yes 1 t y).include? val.downcase | |||
| when :array | |||
| val.split(",").map! { |x| x.strip.downcase } | |||
| when :datetime | |||
| DateTime.parse(val) | |||
| when :time | |||
| Time.parse(val) | |||
| end | |||
| else | |||
| args << arg | |||
| end | |||
| end | |||
| die "too few arguments" if args.size < num | |||
| die "too many arguments" if args.size > num | |||
| raise ArgumentError, "too few arguments" if args.size < num | |||
| raise ArgumentError, "too many arguments" if args.size > num | |||
| return args, options | |||
| end | |||
| def self.season | |||
| DateTime.now.strftime('%m').to_i <= 6 ? 'sp' : 'fa' | |||
| def self.current_semester(format) | |||
| season = Time.now.strftime('%m').to_i <= 6 ? 'sp' : 'fa' | |||
| case format | |||
| when 'faspYY' | |||
| season + Time.now.strftime('%y') | |||
| when 'faspYYYY' | |||
| season + Time.now.strftime('%Y') | |||
| else | |||
| raise ConfigError, "unknown semester format: #{format}" | |||
| end | |||
| end | |||
| end | |||
| @@ -1,13 +1,21 @@ | |||
| require_relative 'lib/kgrader' | |||
| def cli | |||
| KGrader::CLI.new Rake.application.original_dir | |||
| def die(error) | |||
| abort "fatal: [#{error.class}] #{error}" | |||
| end | |||
| def run | |||
| yield KGrader::CLI.new Rake.application.original_dir | |||
| rescue KGrader::KGraderError => err | |||
| die err | |||
| end | |||
| def parse_args(num, keywords = {}) | |||
| args, options = KGrader::parse_args ARGV.drop(1), num, keywords | |||
| args.each { |arg| task arg.to_sym {} } | |||
| args + [options] | |||
| rescue KGrader::KGraderError => err | |||
| die err | |||
| end | |||
| task :default => :help do ; end | |||
| @@ -24,31 +32,31 @@ task :help do | |||
| end | |||
| task :list do | |||
| cli.list | |||
| run { |cli| cli.list } | |||
| end | |||
| task :roster do | |||
| course, rosterfile, options = parse_args 2, { :semester => :string } | |||
| cli.roster course, options[:semester], rosterfile | |||
| run { |cli| cli.roster course, options[:semester], rosterfile } | |||
| end | |||
| task :grade do | |||
| course, assignment, options = parse_args 2, | |||
| { :semester => :string, :students => :array, :due => :datetime, | |||
| { :semester => :string, :students => :array, :due => :time, | |||
| :fetch => :bool, :regrade => :bool } | |||
| cli.grade course, options[:semester], assignment, options | |||
| run { |cli| cli.grade course, options[:semester], assignment, options } | |||
| end | |||
| task :commit do | |||
| course, assignment, options = parse_args 2, | |||
| { :semester => :string, :students => :array } | |||
| cli.commit course, options[:semester], assignment, options | |||
| run { |cli| cli.commit course, options[:semester], assignment, options } | |||
| end | |||
| task :clean do | |||
| cli.clean | |||
| run { |cli| cli.clean } | |||
| end | |||
| task :clobber do | |||
| cli.clobber | |||
| run { |cli| cli.clobber } | |||
| end | |||