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