1. Richard Lawrence
  2. schoolutils

Commits

Richard Lawrence  committed 7137ec9 Merge

Merge branch 'grader_features'

  • Participants
  • Parent commits 5f6a834, cbcfa83
  • Branches master

Comments (0)

Files changed (13)

File .gitignore

View file
  • Ignore whitespace
 *~
 *.pyc
 /dist/
+/build/
 /*.egg-info
 MANIFEST

File CHANGES.txt

View file
  • Ignore whitespace
+v0.1.3, 2013-01-15 -- Add CLI options, user validators; update README
 v0.1.2, 2013-01-15 -- Add 'grade' script, README documentation 
 v0.1.1, 2013-01-14 -- setup.py: Fix installation of examples/ and add classifiers
 v0.1.0, 2013-01-14 -- Initial relase.

File README.rst

View file
  • Ignore whitespace
 It isn't necessary to configure schoolutils, but it will be faster to
 use if you do.  The command-line UI expects to find configuration
 files in the ``.schoolutils`` directory of your home directory.  You
-should create two Python modules there: ``config.py`` and
-``calculators.py``.  Sample configuration files are included in the
-``examples`` directory of the source package::
+should create three Python modules there: ``config.py``,
+``calculators.py``, and ``validators.py``.  Sample configuration files
+are included in the ``examples`` directory of the source package::
 
   $ mkdir ~/.schoolutils
   $ cp path/to/schoolutils_source/examples/*.py ~/.schoolutils
   it will (hopefully!) save you some work if you do.
   
 Grade calculation function
-  A grade calculation function is a function you define, in your
+  A grade calculation function is a function you define in your
   ``calculators.py`` module.  This function should calculate the
   calculated grades for a single student on the basis of entered
   grades.  You should define one grade calculation function per
   new key and value for each calculated grade.  For more information,
   see the example ``calculators.py`` module.
 
+Validator function
+   A validator function is a function you define in your
+   ``validators.py`` module.  It prepares data that you type into the
+   user interface to be saved to the database.  This function should
+   accept a string and either return an appropriate value or raise a
+   Python ``ValueError``.  If a validator raises a ``ValueError``, the
+   user interface asks you to re-enter the value until you type one
+   that validates. For example, the ``letter_grade`` validator ensures
+   that any string passed to it is a letter grade, so that you can't
+   save a letter grade of 'W' by mistake.
+
+   See the sample ``validators.py`` module for more information and a
+   list of the validators you can define.
 
 Command-line options
 --------------------

File bin/grade

View file
  • Ignore whitespace
 #!/usr/bin/env python
 
+# 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
 
+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()

File docs/project.org

View file
  • Ignore whitespace
        can't be in Python identifiers ('-', '.')
      + configuration must include a function that maps course rows to
        grading functions?
+   - I've gone with the naming convention for now.  Let's see how it works.
 ** DONE [#A] Support for email in students table		      :db:ui:
 ** CANCELED [#B] Support for middle name in students table		 :db:
    I've decided the right thing to do here is to leave middle names
    pip installs examples to /usr/local/share/schoolutils/examples.
    No idea if it works, or how, on Windows or other Unixes.  May need
    to revisit in the future.
-** TODO [#A] Write docs for initial release!				:doc:
+** DONE [#A] Write docs for initial release!				:doc:
 ** TODO [#D] Make compatible with Python 3			  :packaging:
-** TODO [#A] Figure out the best thing to do with user validators     :ui:db:
+** DONE [#A] Figure out the best thing to do with user validators     :ui:db:
+   useful validators include:
+   - sid
+   - course_num
+   - assignment_name
+   what's the proper behavior here?  should db module import user_validators,
+   and wrap the appropriate validator in its own version?
+   e.g.
+   from schoolutils.config import user_validators
+   def sid(s):
+       if user_validators.sid:
+          return user_validators.sid(s)
+       else:
+          # ...
 ** TODO [#B] Interfaces to edit existing assignments, courses	    :ui:edit:
-** TODO [#A] Make executable scripts with CLI options		     :ui:bin:
+** DONE [#A] Make executable scripts with CLI options		     :ui:bin:
    - bin/grade: start the grader
      options:
      + -a "Paper 1" : select assignment
      + -x : export grades
 ** TODO [#B] Interface for exporting (sorted) lists of grades	  :ui:export:
 ** TODO [#A] Test and fix nested KeyboardInterrupt loop breakout	 :ui:
+** TODO [#B] Figure out institution setup				 :ui:
+   - need an API definition for institution modules
+     + validators?
+     + csv formats?
+   - institution var in user_config
+   - import user_institution module in config? 
+** TODO [#B] Improve change_database, change_course, change_assignment, etc. :ui:
+   - edge case: should not have to retype db path if provided on CLI
+     but no database exists there yet
+   - implement current_courses option
+   - incremental search for course, assignment?
+   - calculate_all_grades interface to loop over all current courses
+   
 * Bug notes
 ** f890cef9d: grading/db.py: fix bug in select_students giving duplicate rows
    - problem was join: a student can appear in multiple courses, so

File examples/config.py

View file
  • Ignore whitespace
 #
 
 # 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"

File examples/validators.py

View file
  • Ignore whitespace
+"""
+validators.py
+
+Sample validators file for schoolutils
+"""
+# 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.
+
+# A validator is a function that accepts a string and either returns a
+# value or raises a ValueError.  Validators are used by the UI to
+# prepare user-entered data to be saved to the database.  By defining
+# your own validators, you'll ensure that the data in your grade
+# database stays consistent with your expectations.
+#
+# For example, by defining the course_number validator, you can make
+# sure that any course number saved in the database follows a
+# particular format, and that if you accidentally type in a course
+# number that doesn't follow this format, the UI will ask you to type
+# it again.
+#
+# Any function you define here will override the function in
+# schoolutils.grading.validators with the same name (provided that
+# function has been marked as overrideable).  
+# 
+# Here is an example validator function:
+def sid(s):
+    """Ensure s is a valid UC Berkeley SID"""
+    sid = s.strip()
+    if len(sid) != 8:
+        raise ValueError("SID must be 8 digits long")
+    for digit in sid:
+        if digit not in '0123456789':
+            raise ValueError("Non-numeric digit in SID: %s" % digit)
+    else:
+        return sid
+
+
+# The complete list of validators you can override is as follows:
+# grade_type
+# grade_weight
+# percentage_grade
+# four_point_grade
+# letter_grade
+# semester
+# sid
+# course_number
+# course_name
+# assignment_name
+# name
+# email

File schoolutils/config/__init__.py

View file
  • Ignore whitespace
     '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):
     try:
         user_validators = imp.load_source('user_validators',
                                           USER_VALIDATORS_FILE) 
-        # TODO: add defaults?
     except (IOError, ImportError):
-        user_validators = validators
+        user_validators = imp.new_module('user_validators')
         
     return user_config, user_calculators, user_validators
 

File schoolutils/grading/db.py

View file
  • Ignore whitespace
     return ensure_unique(
         db_connection.execute("SELECT changes()").fetchall())
 
-#
-# Constructors/validators
-#
-def number_in_range(s, constructor, mn, mx):
-    """Convert s to a number and validate that it occurs in a given range.
-       Min bound is inclusive; max bound is exclusive."""
-    n = constructor(s)
-    if not (mn <= n and n < mx):
-        raise ValueError("Number %s outside acceptable range [%s, %s)" %
-                         (n, mn, mx))
-    return n
-
-def int_in_range(s, mn, mx):
-    return number_in_range(s, int, mn, mx)
-
-def float_in_range(s, mn, mx):
-    return number_in_range(s, float, mn, mx)
-
-def year(s):
-    "Convert s to a calendar year"
-    return int_in_range(s, 1985, 2100)
-
-def month(s):
-    "Convert s to a calendar month"
-    return int_in_range(s, 1, 13)
-
-def day(s):
-    "Convert s to a calendar day"
-    return int_in_range(s, 1, 32)
-
-def semester(s):
-    "Convert s to a semester-designating string"
-    S = s.strip().title()
-    if S not in ['Fall', 'Spring', 'Summer']:
-        raise ValueError("Not a semester designation: %s" % s)
-    return S
-
-def percentage_grade(s):
-    "Convert s to a percentage grade"
-    return float_in_range(s, 0, 100.1)
-
-def four_point_grade(s):
-    "Convert s to a grade on a 4.0 scale"
-    return float_in_range(s, 0.0, 5.0)
-
-def letter_grade(s):
-    "Ensure s is a letter grade"
-    letter_grades = [
-        'A+', 'A', 'A-',
-        'B+', 'B', 'B-',
-        'C+', 'C', 'C-',
-        'D+', 'D', 'D-',
-        'F', 'I'
-    ]
-    g = s.strip().upper()
-    if g not in letter_grades:
-        raise ValueError("Not a letter grade: %s" % s)
-    return g
-    
-def date(s):
-    """Convert s to a datetime.date.
-       s is assumed to be in YYYY-MM-DD format"""
-    y, m, d = s.strip().split('-')
-    y = year(y)
-    m = month(m)
-    d = day(d)
-    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
-    """
-    return str(s)
-
-def name(s):
-    """Ensure s looks like a name"""
-    return s.strip().upper()
-
-def email(s):
-    """Ensure s looks like an email address"""
-    e = s.lower()
-    if '@' not in e:
-        raise ValueError("%s does not appear to be an email address" % s)
-    return e

File schoolutils/grading/grader.py

View file
  • Ignore whitespace
 
 import csv, os, sys, math, optparse
 
-from schoolutils.config import user_calculators
-
 #
 # Top-level interfaces
 #
 # Each interface accepts an input file handle, an output file
 # handle, and an options structure
-def csv_to_csv(in_file, out_file, options):
-    "Interface for reading and writing CSV files"        
-    fieldnames, rows = read_csv(in_file)
 
-    # TODO: user_calculators need not provide a single
-    # calculate_grade...
-    calculated_rows = [user_calculators.calculate_grade(row) for row in rows]
-    
-    if options.sort_field:
-        calculated_rows = sort_grade_list(calculated_rows, options.sort_field)
-        
-    write_csv(out_file, calculated_rows[0].keys(), calculated_rows)
+# TODO: can't import user_calculators here, because user_calculators
+# might very well be importing this module!  This code needs to move to
+# a UI module
+# def csv_to_csv(in_file, out_file, options):
+#     "Interface for reading and writing CSV files"        
+#     fieldnames, rows = read_csv(in_file)
+#
+#     calculated_rows = [user_calculators.calculate_grade(row) for row in rows]
+#    
+#     if options.sort_field:
+#         calculated_rows = sort_grade_list(calculated_rows, options.sort_field)
+#        
+#     write_csv(out_file, calculated_rows[0].keys(), calculated_rows)
 
 #
 # Grade-list operations and reporting functions

File schoolutils/grading/ui.py

View file
  • Ignore whitespace
 # 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
+from schoolutils.grading import db, validators
 
 # TODO: abstract from specific institution
 from schoolutils.institutions.ucberkeley import bspace
     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',
+                                               validators.semester)
+        self.year = self.get_config_option('current_year', validators.year)
+        self.current_courses = user_config.current_courses
+        course_num = self.get_config_option('default_course',
+                                            validators.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',
+                                                 validators.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")
         
 
             print ("Enter student data to lookup or create student. "
                    "Search uses fuzzy matching on name and email fields.\n"
                    "Use Ctrl-C to stop search and select from list.")
-            sid = typed_input("Enter SID: ", db.sid, default='')
+            sid = typed_input("Enter SID: ", validators.sid, default='')
             students = db.select_students(self.db_connection, sid=sid)
             quit_if_unique(students)
 
-            last_name = typed_input("Enter last name: ", db.name, default='')
+            last_name = typed_input("Enter last name: ", validators.name,
+                                    default='')
             students = db.select_students(self.db_connection,
                                           sid=sid,
                                           last_name=last_name,
                                           fuzzy=True)
             quit_if_unique(students)
             
-            first_name = typed_input("Enter first name: ", db.name, default='')
+            first_name = typed_input("Enter first name: ", validators.name,
+                                     default='')
             students = db.select_students(self.db_connection,
                                           sid=sid,
                                           last_name=last_name,
                                           fuzzy=True)
             quit_if_unique(students)
 
-            email = typed_input("Enter email: ", db.email, default='')
+            email = typed_input("Enter email: ", validators.email, default='')
             students = db.select_students(self.db_connection,
                                           sid=sid,
                                           last_name=last_name,
                  #self.export_grades,
                  self.exit])
 
+            # commit after successful completion of any top-level action
+            # to avoid data-loss
+            self.db_connection.commit()
+
            
     @require('db_connection', change_database,
              "A database connection is required to change the current course.")
            Lookup an existing course in the database by semester, name, or number.
         """
         print "(Press Enter to skip a given search criterion)"
-        year = typed_input("Enter year: ", db.year, default='') or None
-        semester = typed_input("Enter semester: ", db.semester, default='') or None
-        course_num = typed_input("Enter course number: ", str) or None
-        course_name = typed_input("Enter course name: ", str) or None
+        year = typed_input("Enter year: ", validators.year, default='') 
+        semester = typed_input("Enter semester: ", validators.semester,
+                               default='') 
+        course_num = typed_input("Enter course number: ",
+                                 validators.course_number) 
+        course_name = typed_input("Enter course name: ", validators.course_name)
 
         courses = db.select_courses(self.db_connection,
                                     year=year, semester=semester,
            Add a new course to the database and select it as the current
            course.
         """
-        year = typed_input("Enter year: ", db.year)
-        semester = typed_input("Enter semester: ", db.semester)
-        course_num = typed_input("Enter course number: ", str)
-        course_name = typed_input("Enter course name: ", str)
+        year = typed_input("Enter year: ", validators.year)
+        semester = typed_input("Enter semester: ", validators.semester)
+        course_num = typed_input("Enter course number: ",
+                                 validators.course_number)
+        course_name = typed_input("Enter course name: ",
+                                  validators.course_name)
 
         course_id = db.create_course(
             self.db_connection,
         """Create a new assignment.
            Add a new assignment to the database and select it as the current assignment.
         """
-        name = typed_input("Enter assignment name: ", str)
+        name = typed_input("Enter assignment name: ", validators.assignment_name)
         description = typed_input("Enter description: ", str, default='')
-        due_date = typed_input("Enter due date (YYYY-MM-DD): ", db.date)
-        grade_type = typed_input("Enter grade type: ", str)
-        weight = typed_input("Enter weight (as decimal): ", float)
+        due_date = typed_input("Enter due date (YYYY-MM-DD): ", validators.date)
+        grade_type = typed_input("Enter grade type: ", validators.grade_type)
+        weight = typed_input("Enter weight (as decimal): ",
+                             validators.grade_weight)
 
         self.assignment_id = db.create_assignment(
             self.db_connection,
         """Enter grades.
            Enter grades for the current assignment for individual students.
         """
+        # TODO: select grade validator based on assignment type
+        # _, __, = select_assignments(self.db_connection, assignment_id=self.assignment_id)
         print ""
         print "Use Control-C to finish entering grades."
         while True:
                                    deleter=lambda s: None)
 
         for s in students:
-            s.pop('full_name') # name should now be properly split into last/first
+            if 'full_name' in s:
+                s.pop('full_name') # name should now be properly split 
             try:
                 s['student_id'] = db.get_student_id(self.db_connection,
                                                     sid=s['sid'],
         else:
             prompt = "Which action? (enter a number): "
 
-        validator = lambda s: db.int_in_range(s, 0, len(actions))
+        validator = lambda s: validators.int_in_range(s, 0, len(actions))
         i = typed_input(prompt, validator,
                         default=actions.index(default) if default in actions else None)
         print "" # visually separate menu and selection 
             max_index += 1
             print menu_format.format(max_index, "None of the above")
 
-        validator = lambda s: db.int_in_range(s, 0, max_index+1)
+        validator = lambda s: validators.int_in_range(s, 0, max_index+1)
             
         idx = typed_input("Which option? (enter a number): ", validator)
         print "" # visually separate menu and selection input 
             s = s.strip().lower()
             if s.startswith('d'):
                 action = 'd'
-                idx = db.int_in_range(s[1:], 0, len(editable_rows)+1)
+                idx = validators.int_in_range(s[1:], 0, len(editable_rows)+1)
             elif s.startswith('i'):
                 action = 'i'
                 idx = None
             else:
                 action = 'e'
-                idx = db.int_in_range(s, 0, len(editable_rows)+1)
+                idx = validators.int_in_range(s, 0, len(editable_rows)+1)
             return action, idx
                 
         while True:
             raise ValueError("You may not pass both from_dict and from_row")
 
         db_fields = ['student_id', 'last_name', 'first_name', 'sid', 'email']
-        validators = {'last_name': db.name, 'first_name': db.name,
-                      'sid': db.sid, 'email': db.email}
+        vlds = {'last_name': validators.name, 'first_name': validators.name,
+                'sid': validators.sid, 'email': validators.email}
         d = {}
         for i, f in enumerate(db_fields):
             if from_row:
             else:
                 d[f] = None
                 
-        return self.edit_dict(d, validators=validators, skip=['student_id'])
+        return self.edit_dict(d, validators=vlds, skip=['student_id'])
         
     def print_course_info(self):
         "Prints information about the currently selected course"

File schoolutils/grading/validators.py

View file
  • Ignore whitespace
+"""
+validators.py
+
+Validator functions for grading utilities
+"""
+# 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.
+
+from schoolutils.config import user_validators
+
+def user_override(f):
+    "Decorator: makes a validator overrideable by user's validators.py"
+    return getattr(user_validators, f.__name__, f)
+
+# Validators:
+def number_in_range(s, constructor, mn, mx):
+    """Convert s to a number and validate that it occurs in a given range.
+       Min bound is inclusive; max bound is exclusive."""
+    n = constructor(s)
+    if not (mn <= n and n < mx):
+        raise ValueError("Number %s outside acceptable range [%s, %s)" %
+                         (n, mn, mx))
+    return n
+
+def int_in_range(s, mn, mx):
+    return number_in_range(s, int, mn, mx)
+
+def float_in_range(s, mn, mx):
+    return number_in_range(s, float, mn, mx)
+
+def year(s):
+    "Convert s to a calendar year"
+    return int_in_range(s, 1985, 2100)
+
+def month(s):
+    "Convert s to a calendar month"
+    return int_in_range(s, 1, 13)
+
+def day(s):
+    "Convert s to a calendar day"
+    return int_in_range(s, 1, 32)
+
+def date(s):
+    """Convert s to a datetime.date.
+       s is assumed to be in YYYY-MM-DD format"""
+    y, m, d = s.strip().split('-')
+    y = year(y)
+    m = month(m)
+    d = day(d)
+    return datetime.date(y, m, d)
+
+@user_override
+def grade_type(s):
+    """Ensure s is a valid grade type.
+
+       By default, converts s to lowercase and ensures it is one of
+       'letter', 'points', or 'percentage'.
+    """
+    t = s.strip().lower()
+    if t not in ['letter', 'points', 'percentage']:
+        raise ValueError("Not a grade type: %s" % s)
+
+    return t
+
+@user_override
+def grade_weight(s):
+    """Ensure s is a valid grade weight.
+
+       By default, this function is just an alias for float(); provide
+       your own in your validators.py.
+    """
+    return float(s)
+
+@user_override
+def percentage_grade(s):
+    """Convert s to a percentage grade.
+
+       By default, this function accepts any string convertable to a
+       float() value from 0.0 through (but not including) 100.1.
+    """
+    return float_in_range(s, 0, 100.1)
+
+@user_override
+def four_point_grade(s):
+    """Convert s to a grade on a 4.0 scale.
+
+       By default, this function accepts any string convertable to a
+       float() value from 0.0 through (but not including) 5.0.
+    """
+    return float_in_range(s, 0.0, 5.0)
+
+@user_override
+def letter_grade(s):
+    """Ensure s is a letter grade.
+
+       By default, this function checks that s.upper() is a standard
+       American letter grade, including 'F' (fail) and 'I' (incomplete).
+    """
+    letter_grades = [
+        'A+', 'A', 'A-',
+        'B+', 'B', 'B-',
+        'C+', 'C', 'C-',
+        'D+', 'D', 'D-',
+        'F', 'I'
+    ]
+    g = s.strip().upper()
+    if g not in letter_grades:
+        raise ValueError("Not a letter grade: %s" % s)
+    return g
+    
+@user_override
+def semester(s):
+    """Ensure s is a semester-designating string.
+
+       By default, this function converts s to title case and ensures
+       it is one of 'Fall', 'Spring', 'Summer' or 'Winter'.
+    """
+    S = s.strip().title()
+    if S not in ['Fall', 'Spring', 'Summer', 'Winter']:
+        raise ValueError("Not a semester designation: %s" % s)
+    return S
+
+@user_override
+def sid(s):
+    """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)
+
+@user_override
+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)
+
+@user_override
+def course_name(s):
+    """Ensure s is a valid course name.
+
+       By default, this function is just an alias for str(); provide
+       your own in your validators.py.
+    """
+    return str(s)
+
+@user_override
+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)
+
+@user_override
+def name(s):
+    """Ensure s looks like a name.
+
+       By default, this function strips whitespace and converts s to uppercase.
+    """
+    return s.strip().upper()
+
+@user_override
+def email(s):
+    """Ensure s looks like an email address.
+
+       By default, this function just checks that s contains '@', and converts
+       the address to lower case.
+    """
+    e = s.lower()
+    if '@' not in e:
+        raise ValueError("%s does not appear to be an email address" % s)
+    return e

File setup.py

View file
  • Ignore whitespace
 # 02110-1301, USA.
 from distutils.core import setup
 
+try:
+    readme = open('README.rst', 'r')
+    long_desc = readme.read()
+except:
+    long_desc = ''
+finally:
+    readme.close()
+    
 setup(name='schoolutils',
-      version='0.1.2',
+      version='0.1.3',
       description=('Utilities to track and manage student data, including '
                    'a grade database, grade calculators, and more'),
+      long_description=long_desc,
       url='https://bitbucket.org/wyleyr/schoolutils',
       author='Richard Lawrence',
       author_email='richard.lawrence@berkeley.edu',