#!/bin/sh # (C) 2011 by Eugeniy Mikhailov, # vim:set ft=tcl: \ exec tclsh "$0" "$@" package require Tcl 8.5 package require try ;# Tcllib. package require cmdline 1.5 ;# First version with proper error-codes. package require json::write package require sqlite3 package require md5 source ./GradeBook_lib.tcl set options { {w "Write to database, disabled by default"} {v "Verbose"} {d "Debug write DB operations"} } set usage " Usage: $argv0 \[options] gradebook.sqlitedb blackboard.sqlitedb Import a foreign DB to gradebook format. Expect both DB files to be sqlite tables. Imported DB should have a single table 'export_table' Example: $argv0 2020_Fall_Phys251 blackboard.db Options:" try { array set params [::cmdline::getoptions argv $options $usage] #parray params # Note: argv is modified now. The recognized options are # removed from it, leaving the non-option arguments behind. if { [llength $argv] < 2 } { throw {CMDLINE USAGE} [::cmdline::usage $options $usage] } } trap {CMDLINE USAGE} {msg o} { # Trap the usage signal, print the message, and exit the application. # Note: Other errors are not caught and passed through to higher levels! puts $msg exit 1 } ################# Config ############################################ set categories2import [list FinalExam HomeWork LabReport] #set categories2import [list FinalExam] set commonInfoCol {{First Name} {Last Name} {Student ID} {Last Access} Username Availability } set infoColumsMarkers {{ - Lateness \(H:M:S\)} { - Max Points} { - Submission Time} {Total Lateness \(H:M:S\)} {Current Weighted} {Total \[} } set skipCreationCol [concat $commonInfoCol $infoColumsMarkers] ###################################################################### # if DRYRUN is true the database will not be modified set DRYRUN true set DRYRUN [expr {!$params(w)}] if { $DRYRUN} { puts "DRYRUN: DB will not be modified" } set VERBOSE [expr {$params(v)}] set DBDEBUG [expr {$params(d)}] set classDB [lindex $argv 0] set blackboardDB [lindex $argv 1] puts [list Categories to import: $categories2import] sqlite3 db $classDB sqlite3 bdb $blackboardDB proc iferror { err errStat {eval_str {""} }} { #return; # comment out when debugging if { $err } { puts "Error: $errStat" if { $eval_str ne "" } { puts "For query: $eval_str" } } } proc dbg { msg } { global VERBOSE if { $VERBOSE } { puts $msg } } proc dbdbg { msg } { global DBDEBUG if { $DBDEBUG } { puts -nonewline $msg } } proc getColListFromAnyTable {db table} { set all_column_names "" set eval_str [concat SELECT * FROM \'$table\' LIMIT 1] set err [catch { $db eval $eval_str v { set all_column_names $v(*) } } errStat ] iferror $err $errStat if { $err } { return false } return $all_column_names } proc getBlackboardUsernames { db } { set username_list {} set eval_str [concat SELECT Username FROM export_table] set err [catch { $db eval $eval_str v { lappend username_list $v(Username) } } errStat ] iferror $err $errStat if { $err } { return false } return $username_list } proc getBlackboardUserGrade { db uname col } { set eval_str [concat SELECT \"$col\" FROM export_table where Username=='$uname'] set val [$db onecolumn $eval_str] return $val } proc isInList { col listCol} { foreach sCol $listCol { set result [regexp $sCol $col match] if { $result } { return true } } return false } proc trimColName { col } { set shortCol $col set category Note set maxScore 0 set type Score set result [regexp -nocase {(.*) (\[Total Pts:.*)} $col match shortCol scoreStr] if { $result} { set result [regexp -nocase {\[Total Pts: (\d+)} $scoreStr match maxScore] set result [regexp -nocase {\[Total Pts: (\d+) Percentage} $scoreStr match] if { $result } { set type Percentage } set number {} set name $shortCol set result [regexp -nocase {(\D+)(\d+)} $shortCol match name number] set result [regexp -nocase {(\S+)\s+$} $name match name] switch $name { HW { set name Homework ; set category HomeWork } Lab { set name Lab ; set category LabReport} Design { set name "Final Project Design" ; set category FinalExam } Report { set name "Final Project Report" ; set category FinalExam } Precision { set name "Final Project Precision" ; set category FinalExam } Hardware { set name "Final Project Hardware" ; set category FinalExam } "Extra Credit" { set name "Final Project Bonus" ; set category FinalExam; set maxScore 0 } default { } } if { $number eq "" } { set shortCol "$name" } else { set shortCol "$name $number" } } #puts [list $col "--->" $shortCol $category $maxScore $type] return [list $shortCol $category $maxScore $type] } proc dbRequest {script} { # verbose evaluation of dbRequest with dry run capability global DRYRUN set cmd "" foreach line [split $script \n] { if {$line eq ""} {continue} append cmd $line\n if { [info complete $cmd] } { if { ![info exists DRYRUN] || $DRYRUN} { dbdbg "DRYRUN: $cmd" } else { dbdbg "EXECUTING: $cmd" uplevel 1 $cmd } set cmd "" } } } ## adding students if they do not exist proc addStudentsFromBlackboardDB { db } { # blackboard does not provide the following info set id unknownID set section unknownSection puts "Blackboard does not provide Student ID and Section number, skipping user addition" #return set fname [list First Name] set lname [list Last Name] set eval_str [concat SELECT * FROM 'export_table'] set err [catch { $db eval $eval_str v { set username [blackboardUsername2local $v(Username)] dbRequest [list AddUserNonWeb $v($fname) $v($lname) $username student $id $section] } } errStat ] iferror $err $errStat } proc blackboardUsername2local { uname } { return "$uname@email.wm.edu" } proc updateGrade { col locUname grade } { set oldGrade [SelectColValue4User $col $locUname ] #puts "$locUname: $col oldGrade \{$oldGrade\} --> \{$grade\}" if { $oldGrade == $grade } { return } if { [regexp -nocase -- {excuse} $oldGrade] } { # remote system do not handle excuses # so local system take precedence dbg [list "not updating \"excused\" grade:" $locUname $col $oldGrade "-->" $grade] return } if { $oldGrade != $grade } { #puts "$locUname: $col oldGrade \{$oldGrade\} --> \{$grade\}" puts [list $locUname $col $oldGrade "-->" $grade] dbRequest [list UpdateColValue4UserNameNonWeb $col $locUname $grade] } } proc pickCols2import { cols2import skipCreationCol categories2import } { set reduced_list {} foreach col $cols2import { if { [isInList $col $skipCreationCol] } { dbg "skipping column $col" continue } set colInfo [trimColName $col] set category [lindex $colInfo 1] if { $category ni $categories2import } { dbg [list skipping $col in $category, permitted categories: $categories2import] continue } lappend reduced_list $col } return $reduced_list } proc normilizeBlackboardGrade { grade colInfo } { set shortCol [lindex $colInfo 0] set category [lindex $colInfo 1] set maxScore [lindex $colInfo 2] set scoreType [lindex $colInfo 3] if { "" ne $grade } { if { $scoreType eq "Percentage" } { # change percentage to score set grade [ expr {$grade/100*$maxScore} ] } } return $grade } proc alias {alias target} { # makes alias to a proc, borrowed from # https://wiki.tcl-lang.org/page/proc+alias set fulltarget [uplevel [list namespace which $target]] if {$fulltarget eq {}} { return -code error [list {no such command} $target] } set save [namespace eval [namespace qualifiers $fulltarget] { namespace export}] namespace eval [namespace qualifiers $fulltarget] {namespace export *} while {[namespace exists [ set tmpns [namespace current]::[info cmdcount]]]} {} set code [catch {set newcmd [namespace eval $tmpns [ string map [list @{fulltarget} [list $fulltarget]] { namespace import @{fulltarget} }]]} cres copts] namespace eval [namespace qualifiers $fulltarget] [ list namespace export {*}$save] if {$code} { return -options $copts $cres } uplevel [list rename ${tmpns}::[namespace tail $target] $alias] namespace delete $tmpns return [uplevel [list namespace which $alias]] } ######################### START of the execution ######################### alias foreignUsername2local blackboardUsername2local alias addStudentsFromForeignDB addStudentsFromBlackboardDB alias getForeignDBUsernames getBlackboardUsernames alias getForeignDBUserGrade getBlackboardUserGrade alias normilizeGrade normilizeBlackboardGrade addStudentsFromForeignDB bdb # now for every user add their grades set username_list [getForeignDBUsernames bdb] set cols2import [getColListFromAnyTable bdb export_table] set cols2import [ pickCols2import $cols2import $skipCreationCol $categories2import] foreach col $cols2import { set colInfo [trimColName $col] set shortCol [lindex $colInfo 0] if { ![doesColumnExists $shortCol GradesTable] } { dbRequest [list AddColumnNonWeb $shortCol $category $maxScore] } foreach uname $username_list { set locUname [foreignUsername2local $uname] set grade [getForeignDBUserGrade bdb $uname $col ] set grade [normilizeGrade $grade $colInfo] updateGrade $shortCol $locUname $grade } }