Archive for the ‘Ed Tech’ Category

University Web Hosting?
Thursday, April 2nd, 2009

My department has spent a lot of time over the last couple months looking at the web hosting services that we provide and trying to determine what they should look like. Never mind the fact that the core group of us feel we should be focusing on tools in support of teaching and learning…apparently just providing and supporting Blackboard is enough in that arena. It is our job, now, to recommend and ultimately, I assume, provide a solution (think set of services/applications) to meet an ill-defined set of web content needs across the University. So, I’ve spent the majority of my time recently toying with applications like Drupal and Wordpress as we collectively try to decide what combination we’re willing to support, why and how we plan on doing so.

I’m becoming increasingly frustrated with the amount of time I’m spending administering systems and applications and discussing how to meet such a broad set of needs with a limited (read manageable) set of systems. I haven’t been doing any development, and it appears that time left for development will become less and less as I spend more time dealing with the above.

It occurs to me that web hosting providers offer, more or less, everything our stakeholders may need…beyond what we’ll ever be able to provide and manage. Why, then, am I doing backflips to support a similar, albeit lacking, environment? Wouldn’t our time be better spent working on support of the configuration, integration and extension of some common set of these solutions, and let our clients choose the solution that best meets their web hosting needs? We, then, can focus on institutional services such as learning management systems, financials, event calendars, student portal, etc.

Update: I’ve been discussing this with my colleagues and keep flip-flopping on the issue. We trade some control with local hosting for what I believe may be ease of use and administration with external hosting. Locally we have ultimate choice in architecture and range of applications, but are limited in what we can do by staffing.

The clearest path (right now, anyway) is to focus on the departments and schools most in need of web hosting support and try to meet their needs with a limited set of locally supported solutions. Beyond that, clients will need to consider securing their own resources and solutions and, possibly, looking into external web hosting providers.

Blackboard Learning Environment Connector for Sakai
Monday, February 2nd, 2009

With last week’s release of Blackboard Learn Release 9, the first release in the new Project NG series, people can begin to explore my implementation of the Learning Environment Connector API to integrate Sakai 2.5.3+ with Blackboard.

The integration consists of two parts, a Building Block portion contributed to the OSCELOT community found here
http://projects.oscelot.org/gf/project/bb-sakai-lec/ under the Apache License, Version 2.0,
and a Sakai project contributed to the Sakai community here
http://confluence.sakaiproject.org/confluence/display/BBLEC under the Educational Community License, Version 2.0. Combined, the two enhancements enable the integration of Sakai user, course and enrollment data into Blackboard and allows the users to access their Sakai course data from within the Blackboard user interface.

While the initial release of the connector implementation should still be considered beta, we’ve successfully tested and demonstrated the implementation and plan to begin further testing by the end of February. Hopefully we’ll have the integration running in production locally for the start of the next Fall semester.

For further details, including screenshots, please visit either of the links above. I’m looking forward to hearing your feedback!

Blackboard/Sakai Learning Environment Connector
Saturday, August 23rd, 2008

I haven’t posted an update in awhile since I’ve been “heads down” working on the integration between the Sakai CLE and Blackboard learning environments. It’s a lot to do in a short amount of time, but things are going well…I have good help. More updates/details regarding the project to follow once the connector is released alongside Blackboard 9, the first release of the new Project NG series.

VPN Gnome Panel Applet
Thursday, May 15th, 2008

As part of the effort to support linux users connecting to our IPSEC/L2TP based VPN, I developed my first ever gnome panel applet written in Python (another first for me). I have a functional bash script that can be used to connect to/disconnect from the VPN, but I wanted an easier way for users to interact with the script. As a result, I developed the applet to display connection status, prompt for connection credentials and to manage the connection process so users don’t have to open a terminal and work at the command line.

The vpn shell script indicates status through a /var/lock/subsys/VPN file. The applet below constantly polls status by checking for the existence of this file (see the status_timeout method). Once the user has clicked on the applet icon, and entered the appropriate credentials, if necessary, the vpn shell script is invoked accordingly using the Popen object from the subprocess module.

This is (roughly) the python script implementing the vpn gnome panel applet:


#!/usr/bin/env python
import pygtk
pygtk.require('2.0')

import gtk
import gnomeapplet
import sys
import gobject
import subprocess
import os
import time
import logging

class VPN_Applet(gnomeapplet.Applet):

    applet = None
    image_file = "/usr/local/share/vpn/applet/images/vpn.png"
    image_file_hover = "/usr/local/share/vpn/applet/images/vpn_hover.png"
    proc_file = "/var/lock/subsys/VPN"
    username = ""
    passwd = ""
    title = "VPN Applet"
    cancel = False
    logging.basicConfig(level=logging.DEBUG)

    def __init__(self, applet, iid):
        logging.debug("__init__")

        self.applet = applet
        size = self.applet.get_size() - 2
        pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(self.image_file, size, size)
        image = gtk.Image()
        image.set_from_pixbuf(pixbuf)
        tooltips = gtk.Tooltips()
        tooltips.set_tip(image, "Click to connect to VPN\nStatus: Disconnected")
        # Status update timeout (check every 3 seconds)
        gobject.timeout_add(3000, self.status_timeout, image, tooltips)
        self.applet.add(image)
        self.applet.connect("button-press-event", self.button_press)
        self.applet.connect("change-size", self.change_size, image)
        self.applet.connect("change-background", self.change_background)
        self.applet.connect("enter-notify-event", self.enter_notify, image)
        self.applet.connect("leave-notify-event", self.leave_notify, image)
        self.applet.show_all()

    def check_connected(self):
        if (os.path.exists(self.proc_file)):
            return True
        else:
            return False

    def status_timeout(self, image, tooltips):
        logging.debug("status_timeout")
        if (self.check_connected()):
            txt = "disconnect from"
            status = "Connected"
        else:
            txt = "connect to"
            status = "Disconnected"
        tooltips.set_tip(image, "Click to " + txt + " VPN\nStatus: " + status)
        return True

    def do_image(self, image_file, image):
        logging.debug("do_image")

        size = self.applet.get_size() - 2
        pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(image_file, size, size)
        image.set_from_pixbuf(pixbuf)        

    def change_size(self, applet, new_size, image):
        logging.debug("change_size")

        self.do_image(self.image_file, image)

    def change_background(self, applet, type, color, pixmap):
        logging.debug("change_background")

        applet.set_style(None)
        applet.modify_style(gtk.RcStyle())
        if (type == gnomeapplet.COLOR_BACKGROUND):
            applet.modify_bg(gtk.STATE_NORMAL, color)
        elif (type == gnomeapplet.PIXMAP_BACKGROUND):
            applet.get_style().bg_pixmap[gtk.STATE_NORMAL] = pixmap

    def enter_notify(self, applet, event, image):
        logging.debug("enter_notify")

        self.do_image(self.image_file_hover, image)

    def leave_notify(self, applet, event, image):
        logging.debug("leave_notify")

        self.do_image(self.image_file, image)

    def button_press(self, button, event):
        logging.debug("button_press")

        if (event.button == 1):
            logging.debug("create dialog")
            self.setup_dialog()
            if (not(self.cancel)):
                if (not(self.check_connected())):
                    # Prompt for username/password
                    # Continue prompting until they are non-empty or user cancels
                    while ((self.username == "" or self.passwd == "") and not(self.cancel)):
                        self.username_prompt()
                    # If the user doesn't want to cancel, complete the connection
                    if (not(self.cancel)):
                        logging.debug("gksudo vpn start")
                        args = "-n '" + self.username + "' -p '" + self.passwd + "'"
                        proc = subprocess.Popen("gksudo -D '" + self.title + "' \"vpn " + args + " start\"",
                                                stdout=subprocess.PIPE,
                                                stderr=subprocess.STDOUT,
                                                shell=True)
                        self.show_progress_bar(proc)
                    # Clear username/password here
                    self.username=""
                    self.passwd=""
                else:
                    logging.debug("gksudo vpn stop")
                    proc = subprocess.Popen("gksudo -D \"" + self.title + "\" \"vpn stop\"",
                                            stdout=subprocess.PIPE,
                                            stderr=subprocess.STDOUT,
                                            shell=True)
                    self.show_progress_bar(proc)

    def setup_dialog(self):
        logging.debug("setup_dialog")

        vpn_dialog = gtk.Dialog(title=self.title,
                                flags=(gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR),
                                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                                         gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
        # Test here for vpn connection status and prompt accordingly
        if (self.check_connected()):
            txt = "disconnect from"
        else:
            txt = "connect to"
        vpn_dialog.connect("response", self.dialog_response)
        dialog_label = gtk.Label("Would you like to " + txt + " the Virtual Private Network?")
        dialog_label.set_line_wrap(True)
        vpn_dialog.vbox.pack_start(dialog_label, padding=10)
        vpn_dialog.show_all()
        vpn_dialog.run()

    def dialog_response(self, dialog, response_id):
        logging.debug("dialog_response")

        dialog.destroy()
        if (response_id == gtk.RESPONSE_ACCEPT):
            self.cancel = False
        else:
            self.cancel = True

    def show_progress_bar(self, proc):
        logging.debug("show_progress_bar")

        if (self.check_connected()):
            txt = "Disconnecting from "
        else:
            txt = "Connecting to "
        progress_dialog = gtk.Dialog(title=self.title,
                                     flags=(gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR))
        progress_dialog.set_deletable(False)
        label = gtk.Label(txt + "VPN")
        progress_dialog.vbox.pack_start(label, padding=10)
        progress_bar = gtk.ProgressBar()
        progress_bar.set_pulse_step(0.1)
        progress_dialog.vbox.pack_start(progress_bar, padding=10)
        gobject.timeout_add(250, self.progress_bar_timeout, progress_dialog,
                            proc, progress_bar, label)
        progress_dialog.show_all()
        progress_dialog.run()

    def progress_bar_timeout(self, progress_dialog, proc, progress_bar, label):
        logging.debug("progress_bar_timeout")

        progress_bar.pulse()
        if (proc.poll() != None):
            if (self.check_connected()):
                txt = "Connected to "
            else:
                txt = "Disconnected from "
            label.set_text(txt + "VPN.  Click the 'OK' button below to continue.")
            label.set_line_wrap(True)
            progress_dialog.vbox.remove(progress_bar)
            scrolled_window = gtk.ScrolledWindow()
            text_view = gtk.TextView()
            text_view.set_editable(False)
            text_view.set_cursor_visible(False)
            text_view.set_size_request(400, 200)
            buffer = text_view.get_buffer()
            buffer.set_text(proc.stdout.read())
            scrolled_window.add(text_view)
            progress_dialog.vbox.pack_start(scrolled_window)
            progress_dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
            progress_dialog.connect("response", self.dialog_response)
            progress_dialog.show_all()
            return False
        else:
            return True

    def username_changed(self, username):
        logging.debug("username_changed")
        self.username = username.get_text()

    def passwd_changed(self, passwd):
        logging.debug("passwd_changed")
        self.passwd = passwd.get_text()

    def username_prompt_activate(self, entry, dialog):
        logging.debug("username_prompt_activate")
        dialog.response(gtk.RESPONSE_ACCEPT)

    def username_prompt(self):
        logging.debug("username_prompt")

        username_prompt = gtk.Dialog(title=self.title,
                                  flags=(gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR),
                                  buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                                          gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
        username = gtk.Entry(32)
        username.connect("changed", self.username_changed)
        username.connect("activate", self.username_prompt_activate, username_prompt)
        passwd = gtk.Entry(32)
        passwd.set_visibility(False)
        passwd.connect("changed", self.passwd_changed)
        passwd.connect("activate", self.username_prompt_activate, username_prompt)
        prompt = gtk.Label("Please enter your username and password:")
        prompt.set_line_wrap(True)
        table = gtk.Table(3, 2)
        table.attach(prompt, 0, 2, 0, 1, xpadding=5, ypadding=5)
        username_label = gtk.Label("Username (required):")
        table.attach(username_label, 0, 1, 1, 2, xpadding=5, ypadding=5)
        table.attach(username, 1, 2, 1, 2, xpadding=5, ypadding=5)
        passwd_label = gtk.Label("Password (required):")
        table.attach(passwd_label, 0, 1, 2, 3, xpadding=5, ypadding=5)
        table.attach(passwd, 1, 2, 2, 3, xpadding=5, ypadding=5)
        username_prompt.vbox.pack_start(table)
        username_prompt.connect("response", self.dialog_response)
        username_prompt.set_deletable(False)
        username_prompt.show_all()
        username_prompt.run()

gobject.type_register(VPN_Applet)

def vpn_factory(applet, iid):
    VPN_Applet(applet, iid)
    return gtk.TRUE

# run it in a gtk window
if len(sys.argv) > 1 and sys.argv[1] == "run-in-window":
    main_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    main_window.set_title("VPN Applet")
    main_window.connect("destroy", gtk.mainquit)
    main_window.set_default_size(36, 36)
    app = gnomeapplet.Applet()
    vpn_factory(app, None)
    app.reparent(main_window)
    main_window.show_all()
    gtk.main()
    sys.exit()

if __name__ == '__main__':
    gnomeapplet.bonobo_factory("OAFIID:GNOME_VPN_Applet_Factory",
                               VPN_Applet.__gtype__,
                               "VPN Applet", "0.5", vpn_factory)

While there is plenty of Python and PyGTK documentation and examples, there is very little documentation on developing gnome applets using Python. Hopefully the code above will be helpful, in addition to the following links:

GNOME Documentation Library – PanelApplet
PyGTK 2.0 Reference Manual
PyGTK 2.0 Tutorial
Gnome applets with Python
Python Tutorial
subprocess – Subprocess Management

Ubuntu IPSEC/L2TP VPN Client
Thursday, May 15th, 2008

I’ve recently been tasked on implementing a linux vpn client for our IPSEC/L2TP based vpn. This task would have been extremely difficult if not for the efforts of Jacco de Leeuw (for his “Using Linux as an L2TP/IPsec VPN client” documentation) and Scott Myron et al. from Indiana University (“Linux L2TP over IPsec VPN Script for IU”).

I’m still waiting on a public revision control system in order to share our current implementation. In the meantime, I just wanted to share a couple issues I faced with the default Ubuntu (7.10/8.04) Openswan package (2.4.6):

Bad route fix

Update – This is not necessary. See the comment added by Alucard below regarding “leftnexthop”.

After bringing up the ipsec connection

~# ipsec setup start
~# ipsec auto --up VPN

I still cannot reach the vpn server (there is no route). After further investigation the route added by this version of openswan is incorrect. So I get something like the following

~# ip route show
...
xxx.xxx.xxx.xxx dev eth0 scope link
...
default via yyy.yyy.yyy.yyy dev eth0
...
~# ping xxx.xxx.xxx.xxx
PING xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx) 56(84) bytes of data.
From zzz.zzz.zzz.zzz icmp_seq=1 Destination Host Unreachable
From zzz.zzz.zzz.zzz icmp_seq=2 Destination Host Unreachable
From zzz.zzz.zzz.zzz icmp_seq=3 Destination Host Unreachable
...

where xxx.xxx.xxx.xxx is the ip of the vpn server, yyy.yyy.yyy.yyy is the gateway ip and zzz.zzz.zzz.zzz is the local ip.
To fix this, I remove the bad route and add a new route through the gateway as follows:

~# ip route del xxx.xxx.xxx.xxx
~# route add -host xxx.xxx.xxx.xxx gw yyy.yyy.yyy.yyy

The result is

~# ip route show
...
xxx.xxx.xxx.xxx via yyy.yyy.yyy.yyy dev eth0
...
default via yyy.yyy.yyy.yyy dev eth0
...
~# ping xxx.xxx.xxx.xxx
PING xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx) 56(84) bytes of data.
64 bytes from xxx.xxx.xxx.xxx: icmp_seq=1 ttl=126 time=9.46 ms
64 bytes from xxx.xxx.xxx.xxx: icmp_seq=2 ttl=126 time=20.3 ms
...

NAT Traversal

The 2.4.6 Openswan version provided in Ubuntu (7.10/8.04) does not support NAT traversal. For more information, read Jacco’s NAT Traversal documentation.

Here are (roughly) the steps I used to build/install a patched version of Openswan 2.4.12 with NAT-T support:

1 – Download the openswan source:

~$ wget http://openswan.org/download/openswan-2.4.12.tar.gz

2 – Unpack the source:

~$ tar xzvf openswan-2.4.12.tar.gz

3 – Move into source directory:

~$ cd openswan-2.4.12/

4 – Get the patch:

~$ wget http://www.jacco2.dds.nl/networking/patches/openswan-allow_MS_bad_proposal.patch

5 – Patch the source:

~$ patch -p0 < openswan-allow_MS_bad_proposal.patch

6 – Install build dependencies:

~$ sudo apt-get -y install build-essential man2html libgmp3c2 libgmp3-dev

7 – Make the programs and install (as root)

~$ make programs
~$ sudo make install

Using the documentation referenced above, along with these workarounds, you should be able to connect an Ubuntu 7.10/8.04 client to an IPSEC/L2TP VPN. Hope that helps!

The Future of the LMS
Wednesday, February 27th, 2008

I’ve given some thought to the limitations of current enterprise learning systems (LMS, CLE, ePortfolio, etc), and thought it’d be a good time to elaborate on them after reading Mary Grush’s interview of Gary Brown in the recent Campus Technology publication [1], entitled “The Future of Web 2.0″ (perhaps more appropriately entitled “The Future of the LMS”).

Some personal observations regarding these systems:

  • Despite best intentions, tools within the systems are not the best of their kind, and, unless mandated, are not the ones chosen by students and faculty. Many of the tools are re-implementations of existing standalone content-based tools (file storage, discussion, wiki, blog, messaging, presentation). A lot of resources are wasted (IMHO, fruitlessly) trying to compete with the alternatives.
  • Content created in external systems typically needs to be recreated in or imported into the LMS.
  • A significant amount of development effort goes into the framework services provided to support the tools. This is not necessarily a bad thing if these services support the pedagogical tools I mention later.
  • The level of integration possible is much greater then the level of integration realized. Is the need realistic and does it outweigh the limitations posed by standalone components?
  • The solutions are heavy-weight; they’re generally prepackaged with several default preselected components. It requires a lot of effort to strip down and/or reconfigure to meet specific needs.

Since the LMS can’t compete with content generating alternatives, the concentration should be on tools to manage and facilitate pedagogy itself. Strip the LMS down to just those necessary parts and focus on making them extremely easy to use. I’m stepping out of my realm here, but it seems the gaps we should be filling are on management of assignments, learning outcomes, curriculum, workflow, rubrics, assessment, etc. Leave the content creation, discussion and presentation to those systems that manage it best. Rather, allow for users to submit links to their external content for assessment purposes. This is quite similar (perhaps exactly) the system that Gary Brown refers to during the interview:

Right now at WSU, one of the things we’re developing in collaboration with Microsoft is a “harvesting” gradebook. So as an instructor in an environment like this, my gradebook for you as a student has links to all the different things that are required of you in order for me to credit you for completing the work in my class. But you may have worked up one of the assignments in Flickr, another in Google Groups, another in Picasa, and another in a wiki. Maybe you’ve also made some significant contributions to Wikipedia. So, I need a gradebook where I have the link you’ve provided me, rather than a copy of the work, and the gradebook should be capable of pulling in all of these various sources.

If there’s any difference, I’m almost certain it is in my idea of the implementation. Unfortunately, WSU has chosen to “collaborate” with Microsoft on a solution, so I assume we’ll never know for sure.

Going back to early 2004, when my group at The (very soon to be defunct) Living SchoolBook was working on developing our own portfolio system, we discussed using RDF to link artifacts stored internally or externally with system-defined learning outcomes (the idea, IIRC, was Matthew Metnetsky’s). To make a long story short, feature creep and resource limitations drove us to switch to development of Goal Management within Sakai. Somewhere during that move I, regretfully, lost track of the idea of using RDF, but I think it applies now more then ever. Instead of requiring content to reside in one solution or another, we should augment the solutions with RDF capabilities to allow creation of “meaningful” relationships between content. The LMS focus then, would be management of the pedagogical parts that are semantically linked to content and providing a facility to crawl and query the resulting graphs.

Update – Just to clarify, I don’t think RDF is a solution in and of itself. There are obviously serious concerns that need to be addressed regarding privacy, authentication and authorization. I’m also not trying to say that tools for minor edits within the LMS are unnecessary/useless, and that there is no need for local copies (snapshots) of external content. The point I’m trying to make is that we need to abandon approaches that require users to use built-in content creation/organization/presentation tools. The focus needs to be on an open architecture that supports streamlined interaction (enhanced usability) with external tools integrated with the pedagogical tools that the LMS does best. Recent trends/movements towards portable data formats and open standards-based architectures to facilitate integration may support this possibility.

[1] Mary Grush, “The Future of Web 2.0,” Campus Technology, 2/27/2008, http://www.campustechnology.com/article.aspx?aid=58872

Adding Users via Web Services in Sakai
Tuesday, January 29th, 2008

We got ourselves into a little bind and had to add a few hundred users to our portfolio system in a short period of time. The alternatives were to figure out a way to script the addition of users or manually enter the students and add them to the appropriate worksite. Since I’m always up for a challenge, I decided to tackle the script…believe it or not I’ve never had the opportunity to work with perl and/or web services, so this was a great opportunity to get my hands dirty.

A quick search led me to Will Trillich’s helpful documentation over at Serensoft, which enabled me to get started pretty quickly. There was also some documentation over in the Sakai Confluence wiki that helped with the configuration. In order to use web services in sakai, you need to turn it on in the sakai.properties configuration file as follows:

# Enable web services login
webservices.allowlogin=true

If that line doesn’t exist, go ahead and add it.

Here’s the perl script I ended up with to add users from a csv file to sakai and enroll them in a specific course…play nice, it’s my first:


#!/usr/bin/perl

#
# Perl script to add a list of users to sakai and enroll
# them in a given course.  Users are read from a csv formatted
# file with lines like the following:
#
# SUID;DOB;LastName;FirstName;NETID
#

use SOAP::Lite;

# Sakai service location constants
use constant {
    LOGIN_WS => "/sakai-axis/SakaiLogin.jws?wsdl",
    SCRIPT_WS => "/sakai-axis/SakaiScript.jws?wsdl",
    SYR_ADDY => "\@syr.edu",
    DEFAULT_ROLE => "access",
};

# Initialize variables from command line parameters
my $userlist = shift;
my $host = shift;
my $id = shift;
my $password = shift;
my $siteId = shift;
my $roleId = shift;

# Routine to display some usage information
sub usage {
    print "\nUsage: useradd.pl filename host username password [siteId] [roleId]\n\n";
    print "e.g.  useradd.pl userlist.csv http://localhost:8080 admin admin_pass\n\n";
}

# Routine to connect to sakai login service
sub sakaiLogin {
    return $host.LOGIN_WS;
}

# Routine to connect to
sub sakaiScript {
    return $host.SCRIPT_WS;
}

# Routine to create an email address
sub makeEmail {
    my $username = shift;
    return $username.SYR_ADDY;
}

# Create password
sub makePassword {
    my $username = shift;
    my $dob = shift;
    ($month, $day, $year) = split(/\//, $dob);
    $month = ($month < 10) ? "0".$month : $month;
    $day = ($day < 10) ? "0".$day : $day;
    return $username.$month.$day;
}

# If either are undefined, display usage and exit
if (!$userlist ||
    !$host ||
    !$id ||
    !$password) {
    &usage;
    exit;
}

# Open the file containing the user list
open(USERLIST, $userlist) || die "Unable to open $userlist!\n";

# Initialize SOAP client
$loginClient = SOAP::Lite->new(proxy => &sakaiLogin($host));
$sessionId = $loginClient->login($id, $password)->result;

print "SESSION_ID: ".$sessionId."\n";

$scriptClient = SOAP::Lite->new(proxy => &sakaiScript($host));

while (<USERLIST>) {
    chop;
    ($suid, $dob, $lastName, $firstName, $netId) = split(/;/);
    my $email = &makeEmail($netId);
    my $password = &makePassword($netId, $dob);

    print "Attempting to add user:\n";
    print "\tEID:\t\t".$netId."\n";
    print "\tFIRST_NAME:\t".$firstName."\n";
    print "\tLAST_NAME:\t".$lastName."\n";
    print "\tEMAIL:\t\t".$email."\n";
    print "\tPASSWORD:\t".$password."\n";

    my $result = $scriptClient->addNewUser($sessionId,
					   $netId,
					   $firstName,
					   $lastName,
					   $email,
					   '',
					   $password)->result;

    print "Result: ".$result."\n";

    if ($siteId) {
	if (!$roleId) {
	    $roleId = DEFAULT_ROLE;
	}
	print "Attempting to add ".$netId." to site ".$siteId." with ".$roleId." role...";

	$result = $scriptClient->addMemberToSiteWithRole($sessionId,
							 $siteId,
							 $netId,
							 $roleId)->result;

	print $result."\n";
    }

    print "\n";
}

$loginClient->logout($sessionId);

close(USERLIST);

I was blown away how easy it was to accomplish so much with so little! In hindsight, there were several cases where I used bash scripts to do too much, whereas a simple perl script would have saved me a great deal of effort. I notice that there’s a pretty big dispute going on over which is better, perl or python…maybe next time I’ll give python a shot and let you know what I think.

Finding Kudos Like Mice Sniff Out Cheese
Wednesday, January 23rd, 2008

Yessssssssss! Found one on page 2 here:

Rethinking Accountability: Response to “The ePortfolio Hijacked”

Indirect, but nice nonetheless.  Thanks Joe!

BTW, mice, apparently, prefer cereal.

Course Merge Building Block
Thursday, January 10th, 2008

I wrote a Blackboard building block that we use to facilitate processing of course merges. In the past, faculty would have to contact our support technicians to request that cross-listed courses and/or courses with multiple sections be merged into a single course. The technician was responsible for manually modifying an “exception” file that is read and processed by the ERP side that provides our Blackboard installation with it’s data feed (as part of the snapshot process).

The building block presents an interface to faculty that allows them to select courses to be merged from those available. The result is then written to the exception file by the course merge building block. By automating the process by which faculty can request course merges, we have reduced the number of merge request support calls for technicians.

I am sharing the building block source (licensed under the ECL version 2) in the hopes that it will help other developers get started, reference as an example, or to customize for their own purposes. Since the exception file format and back-end processing are custom to our Blackboard implementation, this building block is probably not immediately useful and/or generally applicable to meet the needs of other institutions.

I have a pending request to setup and expose portions of our subversion installation to the public for sharing of code. In the meantime, I have bundled a copy of the latest version of the building block source here:

Download Course Merge Building Block

Our wiki containing the building block documentation is not, currently, publicly available, but the source archive contains a README.txt file that is a copy of the same information.

The building block is written using the Spring Framework along with Spring Web MVC for the UI. It uses Maven 2 as the build tool and can be imported as a project into the Eclipse IDE.

Powered by Laughing Squid