Commits

Richard Lawrence committed d91000f Merge

Merge branch 'cli' into grader_features

Comments (0)

Files changed (5)

 #!/usr/bin/env python
 
-from schoolutils.grading import ui
+# This file is part of the schoolutils package.
+# Copyright (C) 2013 Richard Lawrence <richard.lawrence@berkeley.edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
 
+# TODO: use argparse if available (2.7+) since optparse is deprecated
+from optparse import OptionValueError, OptionParser as Parser
+
+from schoolutils.grading import ui, db
+
+def make_callback(validator):
+    def callback(option, opt_str, value, parser):
+        try:
+            v = validator(value)
+            setattr(parser.values, option.dest, v)
+        except ValueError as e:
+            raise OptionValueError(
+              "Bad value for %(option)s: %(err)s" %
+              {'option': opt_str, 'err': str(e)})
+    return callback
+      
+def main():
+    desc = ("Run the schoolutils grading program.\n"
+            "Command line options override the values in your config.py module.")
+    parser = Parser(description=desc)
+    parser.add_option("-d", "--db-file",
+                      dest="gradedb_file",
+                      type="string",
+                      metavar="PATH",
+                      help="Open grade database at PATH")
+    parser.add_option("-y", "--year",
+                      dest="current_year",
+                      type="string",
+                      metavar="YEAR",
+                      help="Select YEAR as current year")
+    parser.add_option("-s", "--semester",
+                      dest="current_semester",
+                      type="string",
+                      metavar="SEMESTER",
+                      help="Select SEMESTER as current semester")
+    parser.add_option("-c", "--course",
+                      dest="default_course",
+                      type="string",
+                      metavar="NUMBER",
+                      help="Select course NUMBER as current course")
+    parser.add_option("-a", "--assignment",
+                      dest="default_assignment",
+                      metavar="NAME",
+                      help="Select assignment NAME as current assignment")
+    options, args = parser.parse_args()
+
+    u = ui.SimpleUI(options=options)
+    u.main_loop()
+ 
+    
 if __name__ == '__main__':
-  u = ui.SimpleUI()
-  u.main_loop()
-  exit(0)
+    main()

examples/config.py

 #
 
 # If you specify current_semester, current_year, and current_courses
-# (as a list of course numbers), courses matching these criteria will
-# be selectable from a list when you change courses in the grader, so
-# you won't have to search for a course in the database.  They will
-# also be used to determine the grade calculation functions for the
-# current courses.
+# (as a list of strings representing course numbers), courses matching
+# these criteria will be selectable from a list when you change
+# courses in the grading program, so you won't have to search for a
+# course in the database.
 current_semester = 'Spring' # string
-current_year = 2013 # int, not string
-current_courses = [
-    '146',
-    ]
+current_year = 2013         # int, not string
+current_courses = ['146',]  # list of strings
 
-# if you specify default_course as a (semester, year, course_number)
-# tuple, it will be selected as the current course when you start grader
-default_course = (current_semester, current_year, current_courses[0])
+# if you specify a string for default_course, it will be in
+# conjunction with current_semester and current_year to select a
+# current course when you start the grading program
+default_course = current_courses[0]
 
 # if you specify default_assignment as a string in addition to
 # default_course, it will be selected as the current assignment when
-# you start grader
-default_assignment = "Paper 1"
-
-
+# you start the grading program
+#default_assignment = "Paper 1"

schoolutils/config/__init__.py

     'current_semester': None,
     'current_year': datetime.date.today().year,
     'current_courses': [],
-    'default_course': (None, None, None), # semester, year, course_num
-    'default_assignment': None,
+    'default_course': '', 
+    'default_assignment': '',
 }
 
 def add_defaults(m, defaults):

schoolutils/grading/db.py

     return datetime.date(y, m, d)
 
 def sid(s):
-    """Ensure s is a valid SID.  This function is just an alias for
-       str(); provide your own in your validators.py
+    """Ensure s is a valid SID.
+
+       By default, this function is just an alias for str(); provide
+       your own in your validators.py
+    """
+    return str(s)
+
+def course_number(s):
+    """Ensure s is a valid course number.
+
+       By default, this function is just an alias for str(); provide
+       your own in your validators.py
+    """
+    return str(s)
+
+def assignment_name(s):
+    """Ensure s is a valid assignment name.
+
+       By default, this function is just an alias for str(); provide
+       your own in your validators.py
     """
     return str(s)
 
 def name(s):
-    """Ensure s looks like a name"""
+    """Ensure s looks like a name."""
     return s.strip().upper()
 
 def email(s):

schoolutils/grading/ui.py

 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301, USA.
 
-import os, sqlite3
+import os, sys, sqlite3
 
 from schoolutils.config import user_config, user_calculators
 from schoolutils.grading import db
     return method_factory
 
 class BaseUI(object):
-    def __init__(self):
-        self.db_file = file_path(user_config.gradedb_file)
-        if self.db_file and os.path.exists(self.db_file):
-            self.db_connection = sqlite3.connect(self.db_file)
+    def __init__(self, options=None):
+        """Initialize grading program UI.
+           options, if provided, should be an options structure produced by
+             optparse
+        """
+        if options:
+            self.cli_options = options
         else:
-            self.db_connection = None
+            self.cli_options = None
+            
         
-        self.semester = user_config.current_semester 
-        self.year = user_config.current_year 
-        self.current_courses = user_config.current_courses
-
+        self.semester = None
+        self.year = None
+        self.current_courses = []
         self.course_id = None
         self.assignment_id = None
         self.student_id = None
 
-        if user_config.default_course[0] and self.db_connection:
-            self.set_default_course()
-        if user_config.default_assignment and self.db_connection:
-            self.set_default_assignment()
+        self.initial_database_setup()
+        self.initial_course_setup()
+        self.initial_assignment_setup()
+
+        
+    def get_config_option(self, option_name, validator, default=None):
+        """Return the appropriate config value from CLI options or user config.
+           option_name should be an attribute to look for on both the options
+             object and the user_config module.  CLI options override user_config
+             values.
+           validator will be applied to the value.
+           Returns the validated value, or default if option is not
+           supplied by the user or the user-supplied value does not
+           pass validation.
+        """
+        val = (getattr(self.cli_options, option_name, None) or
+               getattr(user_config, option_name, default))
+        try:
+            return validator(val)
+        except ValueError:
+            return default
+
+    def initial_database_setup(self):
+        "Set db_file and db_connection from user config and CLI options"
+        self.db_file = self.get_config_option('gradedb_file', file_path)
+        
+        if self.db_file and os.path.exists(self.db_file):
+            self.db_connection = sqlite3.connect(self.db_file)
+        else:
+            self.db_connection = None
+
             
-    def set_default_course(self):
-        "Set course_id using user_config.default_course"
-        # TODO: course_id should also be settable via command line option
-        # TODO: fallback: if current_year and current_semester determine a unique
-        # course, use that
-        sem, yr, num = user_config.default_course
+    def initial_course_setup(self):
+        """Set semester, year, current_courses, and course_id from user config
+           and CLI options"""
+        
+        self.semester = self.get_config_option('current_semester', db.semester)
+        self.year = self.get_config_option('current_year', db.year)
+        self.current_courses = user_config.current_courses
+        course_num = self.get_config_option('default_course', db.course_number)
+        
+        if not (self.db_connection and self.semester and self.year):
+            # don't bother looking for a course without a semester and year
+            # (but try otherwise, because these might identify one uniquely)
+            return
+        
         try:
-            self.course_id = db.ensure_unique(db.select_courses(self.db_connection,
-                                                                semester=sem,
-                                                                year=yr,
-                                                                number=num))
-        except (AttributeError, db.NoRecordsFound, db.MultipleRecordsFound):
-            # AttributeError covers case where self.db_connection uninitialized
-            sys.stderr.write("Unable to locate a unique default course;"
+            self.course_id = db.ensure_unique(
+                db.select_courses(self.db_connection,
+                                  semester=self.semester,
+                                  year=self.year,
+                                  number=course_num))
+        except (db.NoRecordsFound, db.MultipleRecordsFound):
+            sys.stderr.write("Unable to locate a unique default course; "
                              "ignoring.\n")
             
             
-    def set_default_assignment(self):
-        "Set assignment_id using user_config.default_assignment"
-        # TODO: assignment_id should also be settable via command-line option
+    def initial_assignment_setup(self):
+        "Set assignment_id using user config and CLI options"
+        assignment_name = self.get_config_option('default_assignment',
+                                                 db.assignment_name)
+        if not (self.db_connection and self.course_id and assignment_name):
+            return
+        
         try:
             self.assignment_id = db.ensure_unique(
                 db.select_assignments(self.db_connection,
                                       course_id=self.course_id,
-                                      name=user_config.default_assignment))
-        except (AttributeError, db.NoRecordsFound, db.MultipleRecordsFound):
-            # AttributeError covers case where self.db_connection uninitialized
-            sys.stderr.write("Unable to locate a unique default assignment;"
+                                      name=assignment_name))
+        except (db.NoRecordsFound, db.MultipleRecordsFound):
+            sys.stderr.write("Unable to locate a unique default assignment; "
                              "ignoring.\n")