Student Study Material Access 2008
This page outlines the process for developing the kludge web application that will allow CQU students in T1, 2008 to access PDF copies of their study material. This is being done a short-term cover for the late delivery of material. Not as a replacement for print material or as a long term solution.
This page also serves as an example/tutorial of building a wf application. It draws on/extends some existing documentation on wf application
- An overview of what wf is
- An example of writing a wf application
This document uses the steps outlined in the writing a wf application page.
Contents |
Requirements specification
Users
Only CQU students will use this system, and initially only students enrolled in courses in T1, 2008. The main stories are
- ShowStudyMaterials
A valid student will be shown a list of the courses they are enrolled in for T1, 2008. For each they will see a list of the study materials available for that course and some indication of when/if that material has been dispatched. - AccessStudyMaterial
A student actually attempts to download a study material file. - AccessError
If the student is not enrolled in a T1, 2008 course they will be shown an appropriate error page.
AccessStudyMaterials will not actually be part of this wf application. It will be implemented as a simple web link with an access handler that restricts access to just the enrolled students.
Finite state machine
Really boring, the student number is passed in as username
- ShowStudyMaterials - If enrolled in T1, 2008
- AccessError - If not enrolled in T1, 2008 or not a student
Develop the prototype
The template application mentioned in the other wf documents doesn't appear to exist anymore. I generally take a recent application, copy it and then edit it. BamRegister is a recent one I've worked on.
So, I'll copy it to ~/webfuse/lib/Objects/ShowStudyMaterials.pm and do some initial modifications to get the following code
#
# FILE: ShowStudyMaterials.pm
# PURPOSE: Allow students to view study materials (PDFs from
# UPU) for students enrolled in T1, 2008
#
# VIEWS DESCRIPTION
# default Display the students courses and the available study
# material
# AccessError Report an error for not a student or not enrolled
#
# AUTHOR:
# HISTORY: 13 Feb 2008 Work started (DJ)
#
# TO DO:
use strict;
package ShowStudyMaterials;
use webfuse::lib::WebfuseObject;
use webfuse::lib::Webfuse::Content;
use webfuse::lib::View;
#-- Normal User screens
use webfuse::lib::StudyMaterial::ShowStudentMaterials;
use webfuse::lib::StudyMaterial::ShowStudentMaterials_View;
@ShowStudyMaterials::ISA = qw( WebfuseObject );
use CGI;
#-------------
# Object specific globals
my $CGI_URL;
use webfuse::lib::WebfuseConfig;
my $WEBFUSE_HOME=$WebfuseConfig::WEBFUSE_HOME;
my $DATA_DIR="$WebfuseConfig::WEBFUSE_DATA/databases";
my $DEFAULT_TEMPLATE="/";
#-- specify default template
my $TEMPLATE_FAMILY = "$WebfuseConfig::TEMPLATE_FAMILY";
my $TEMPLATE_STYLE = "$WebfuseConfig::TEMPLATE_STYLE";
my $VIEW_FAMILY = "$WebfuseConfig::VIEW_FAMILY";
#-------------
# %states
# - a hash of hashes which define the valid states for different
# classes of user
# - $states->{admin}->{menu} == TRUE means that
# users in admin can view the state menu, if false they can't
my $states =
{
"normal" =>
{
default => 1
}
};
#-----------------------------------------------------
# new( $query )
# - takes CGI reference and creates the class
sub new
{
my $proto = shift;
my $class = ref( $proto ) || $proto;
my $self = new WebfuseObject( shift );
bless( $self, $class );
$self->{STATES} = $states;
$CGI_URL = $self->{QUERY}->url;
$self->GetContent( QUICK => 1 );
$self->{CONTENT}->{NOLOCATION} = 1;
return $self;
}
#----------------------------------------------------
# default
sub default
{
my $self = shift;
if ( $self->checkPermissions() )
{
my $model = StudyMaterial::ShowStudyMaterials->new(
STUD => $self->{USER}->{USERNAME} );
my $family = "default";
my $view = StudyMaterial::ShowStudyMaterials->new(
MODEL => $model, FAMILY => $family );
my $string = $view->Display( $self->{QUERY}, FAMILY => $family );
$self->{CONTENT}->{HEADERS}->{TITLE} = "Show T1, 2008 Study Materials";
$string = $self->{CONTENT}->DisplayString( $string );
$string =~ s/{LOCATION}/Show T1, 2008 Study Materials/g;
$string =~ s/{AUTHOR}/ShowStudyMaterials/g;
print $string;
}
}
#-------------------------------------------------------
########## SUPPORT FUNCTIONS ##########################
#-----------------------------------------------------------
# checkPermissions
# - check that user is a coordinator for the given offering
# - add the offering details into self
sub checkPermissions
{
my $self = shift;
my %args = @_;
#-- get user details
$self->{USERNAME} = $self->{QUERY}->remote_user;
if ( ! exists $self->{USERNAME} )
{
$self->NoPermission( "No valid username found" );
return 0;
}
$self->{USER} = new AccountFactory( USERNAME=> $self->{USERNAME},
DETAILS => { USER => 1, GROUPS => 1, LOOKUPS => 0 } );
if ( $self->{USER}->Errors() )
{
$self->NoPermission( "Error accessing your account details" );
return 0;
}
#-- user must be enrolled in a T1, 2008 course
###### CODE HERE #####
return 1;
}
#-------------------------------------------------
# NoPermission( $issue )
# - display a no access page
sub NoPermission
{
my $self = shift;
my $issue = shift;
my $message =<<"EOF";
<h3>
Unable to grant access to BAM Configuration Screen
</h3>
<p>
Due to the following issue
</p>
<p>
<blockquote>
$issue
</blockquote>
</p>
<p>
It's not possible to provide access to the BAM configuration screen.
</p>
<p>
Please email
<a href="mailto:$WebfuseConfig::WEBFUSE_MASTER">$WebfuseConfig::WEBFUSE_MASTER</a>
if you have any queries or believe you've reached this page in error.
</p>
EOF
$self->Message( $message );
return 0;
}
#-------------------------------------------------
# Message( $string )
# - display a full page message
sub Message
{
my $self = shift;
my $string = shift;
my $view = Template_View->new( FAMILY => $TEMPLATE_FAMILY,
STYLE => $TEMPLATE_STYLE );
my $page = $view->Display;
$page =~ s/{BODY}/$string/g;
$page =~ s/{LOCATION}/Staff MyCQU/g;
$page =~ s/{TITLE}/BAM Register Error/g;
print $page;
}
1;
The tasks left to make this application a working prototype are
- Further modify checkPermissions so that it errors if the user is not a student with a T1, 2008 course
- Add the AccessError method to display the appropriate error message if not a T1, 2008 student
- Create and test the StudyMaterial::ShowStudyMaterials model and view
Will leave the first two until last. Time to get a look up and going.
Develop the prototype
Create the model and the view
Copy BAM::Register::Start model and view as the model and view for this app (StudyMaterial::ShowStudyMaterials model and view) and cut out the pecific BAM stuff to get the following
StudyMaterial::ShowStudyMaterials
#
# FILE: StudyMaterial::ShowStudyMaterials.pm
# PURPOSE: Model holding information for showing T1 study materials
#
# new( STUD => )
# - get details for the study materials for the student
# - get their personal details
#
# AUTHOR: David Jones
# HISTORY: 13 Feb 2008 Started
#
# TO DO:
#
package StudyMaterial::ShowStudyMaterials;
$VERSION = '0.5';
use strict;
use Data::Dumper;
use CGI qw/:standard/;
use webfuse::lib::NewModel;
@StudyMaterial::ShowStudyMaterials::ISA = ( "NewModel" );
#--------------------
# Globals
use webfuse::lib::WebfuseConfig;
my $WEBFUSE_HOME=$WebfuseConfig::WEBFUSE_HOME;
my $DATA_DIR="$WebfuseConfig::WEBFUSE_DATA/databases";
my $CONFIG="$DATA_DIR/EAST.txt";
# CONDITIONS is constructed in handle_params based on parameters
# passed in
my $defaults = {
};
my @REQUIRED_PARAMETERS = ( qw/ STUD / );
#-------------------------------------------------------------
# new( )
sub new
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {};
my %args = @_;
bless( $self, $class );
return $self if ( ! $self->HandleParams( %args ) );
$self->AddData();
return $self;
}
#---------------------------------------------------------------------------
# HandleParams
# - check the parameters passed in
sub HandleParams
{
my $self = shift;
my %args = @_;
#-- setup the defaults
%{$self->{DEFAULTS}} = %$defaults;
@{$self->{REQUIRED_PARAMETERS}} = @REQUIRED_PARAMETERS;
$self->{CONFIG} = $args{CONFIG} || $CONFIG;
foreach my $key ( @{$self->{REQUIRED_PARAMETERS}} )
{
$self->{KEYS}->{$key} = $args{$key};
}
return $self;
}
#-----------------------------------------------------------------
# AddData
# - add the various other data models
sub AddData
{
my $self = shift;
}
1;
StudyMaterial::ShowStudyMaterials_View
#
# FILE: ShowStudyMaterials_View.pm
# PURPOSE: View class for StudyMaterial::Details screen
#
# AUTHOR: David Jones
# HISTORY: 13 Feb 2008
#
# TO DO:
#
package StudyMaterial::ShowStudyMaterials_View;
$VERSION = '0.5';
use strict;
use Carp;
use HTML::Template;
use webfuse::lib::View;
@StudyMaterial::ShowStudyMaterials_View::ISA = ( qw/ View / );
use webfuse::lib::WebfuseConfig;
my $WEBFUSE_HOME=$WebfuseConfig::WEBFUSE_HOME;
#-------------------------------------------------------------
# new( TYPE => $type, VIEW => $view_name, MODEL =>$model, FAMILY =>
# ACCOUNT => $account, DIRECTORY => dir, FILE => file )
# - normally will get the HTML file
# DIRECTORY/TYPE/VIEW.FAMILY
# and replace {VARIABLE} with the contents of the model
# - this action can/might be modified based on the ACCOUNT if
# passed THIS IS NOT IMPLEMENTED yet
# - this can be over ridden by specifying a direct FILE
sub new
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {};
$self->{ERRORS} = [];
my %args = @_;
bless( $self, $class );
$self->{MODEL} = $args{MODEL} || undef;
$self->{FAMILY} = $args{FAMILY} || "default";
$self->{VIEW} = $args{VIEW} || "ShowStudyMaterial";
$self->{TYPE} = $args{TYPE} || "BAM";
$self->{TYPE} =~ s/^(.*)\/$//;
$self->{DIRECTORY} = $args{DIRECTORY} || "$WEBFUSE_HOME/lib/StudyMaterial/Views/ShowStudyMaterials";
$self->{DIRECTORY} =~ s/^(.*)\/$/$1/;
$self->{FULL_PAGE} = 1;
$self->{FULL_PAGE} = $args{FULL_PAGE} if ( exists $args{FULL_PAGE} );
$self->{QUERY} = $args{QUERY} || undef;
$self->{TEMPLATE} = $args{TEMPLATE} || "template.default";
$self->{FILE} = $args{FILE} || "";
return $self;
}
#---------------------------------------------------
# Display
#
sub Display
{
my $self = shift;
my $string;
return $self->DumpErrors()
if ( ! $self->GetHTMLTemplate(
"$self->{DIRECTORY}/$self->{VIEW}.$self->{FAMILY}" ) );
#-- modifications here
return $self->{TEMPLATE}->output();
}
1;
Doing some debug
At this stage the wf application should be able to run, it won't do anything useful, but it should run. One of the aims of this approach is to get working code as quickly as possible.
The wf application should be able to be run from the command line. So at this stage a first check is to run the controller, then the model and the view.
This shows up a typo in the wf object/controller above. The model and view are called ShowStudentMaterials rather than ShowStudyMaterials. Fix that up.
Then run the model and the view. All run with no errors.
Can run from the web browser - get an empty page. Which is as expected.
Develop the prototype HTML
The aim here is to generate some example HTML which is fully hard-coded. Usually the aim in doing this is to show the potential users/systems owners so they can see something in action and give feedback.
In the absence of this purpose this step is still useful because it provides an opportunity to put some real data into a HTML file and learn some lessons about the application. Essentially it tells us what data is required.
The HTML for this application will be located in ~/webfuse/lib/StudyMaterial/Views/ShowStudyMaterials as per the following line from the View above
$self->{DIRECTORY} = $args{DIRECTORY} || "$WEBFUSE_HOME/lib/StudyMaterial/Views/ShowStudyMaterials";
So, I'll create a a file ShowStudyMaterials.html in that directory. Normally, this would be where we get Rolley involved but no time or requirement for this application. So I will just use the historical Webfuse look and feel. This gives the following look.
Implement/identify the support classes
At this stage we attempt to identify the data and services required to implement the prototype. For this type of application it is simply what data we need and what classes can give it to us.
For this app we will need
- Student information
Given the student number we need to get their name and the list of courses they are enrolled in. - Course information
For each course we need to know its name. - Study material information
For the courses the student is enrolled in we need to know what study materials are available for the courses, how big they are and where they are (so we can link to them).
Student information
Is taken from Peoplesoft and all classes for Peoplesoft are in ~/webfuse/lib/People. Relevant classes include
- People::StudentInfo - which gets student name, email etc
- People::CurrentStudentUnits - which given a period/year gets the course the student is enrolled in
We'll use these two.
Actually, we need CurrentStudentUnits in the controller to make sure that students are enrolled in T1, 2008 courses. We'll be able to pass that in as a parameter to the model.
Study Materials
There's no existing class that does this. We'll need to write a new one which will probably have to use the //chef//UPU-rdos share and also perhaps some connection to Axapta if we want to include information about dispatch.
We'll create a new one here called ~/webfuse/lib/StudyMaterial/StudyMaterials.pm
Iterative updating of application
Because we're using some existing courses I'm going to make some mods to the application before completing the other components. The changes to be made here are
- Add CurrentStudentUnits to the controller and implement the error message if the student is not enrolled in T1, 2008
- Pass CurrentStudentUnits to the model
- Modify the model to create StudentInfo
- Modify the view to include the information from model into the HTML
Add CurrentStudentUnits to the controller
Changes required include
- include the module
use webfuse::lib::People::CurrentStudentUnits;
- create an object of that module in CheckPermissions
#-- only allow access if the student is enrolled in T1, 2008
$self->{USER} = new AccountFactory( USERNAME=> $self->{USERNAME},
DETAILS => { USER => 1, GROUPS => 1, LOOKUPS => 0 } );
if ( $self->{USER}->Errors() )
{
$self->NoPermission( "Error accessing your account details" );
return 0;
}
#-- get the current student units
$self->{CURRENT_UNITS} = People::CurrentStudentUnits->new(
STUD => $self->{USERNAME}, PERIOD => "T1", YEAR => "2008");
if ( $self->{CURRENT_UNITS}->Errors )
{
$self->NoPermission( "Error getting list of current courses" ); return 0;
}
if ( $self->{CURRENT_UNITS}->NumberOfRows() == 0 )
{
$self->NoPermission( "You are not enrolled as a student in any T1, 2008 cours
es" );
return 0;
}
Test the code from the command line. Test it from the web. This means I can no longer use the application logged in as me. It generates an error. The error screen needs a few updates as well as it is showing its origins with BAM Register.
I have to start using a student login account (appropriately modified).
The student I chose/created was enrolled in 2081 courses, but had dropped them. The application ran appropriately with an error. When I re-enrolled the student it worked.
Pass CurrentStudentUnits to the model
The model needs this information. We could either pass it in from the controller or have the model create a different object with the same information.
The constructor for the model becomes
my $model = StudyMaterial::ShowStudyMaterials->new(
STUD => $self->{USER}->{USERNAME},
UNITS => $self->{CURRENT_UNITS} );
Modify model to create StudentInfo
sub AddData
{
my $self = shift;
#-- create student information object to get student name etc
$self->{STUDENT_INFO} = People::StudentInfo->new( STUD =>
$self->{KEYS}->{STUD} );
if ( $self->{STUDENT_INFO}->Errors() )
{
$self->AddError( "ShowStudyMaterials.pm - AddDate error getting StudInfo" );
$self->AddErrors( $self->{STUDENT_INFO}->{ERRORS} );
}
}
Modify the View to use this data
Two basic steps
- Modify the template to use the variables
- Modify the View module to create the necessary data structures
Modify the template
The student details are the easiest. The hard-coded student details in the original template gets replaced with some simple HTML::Template variables
Before |
After |
|
<tr class="color" align="right">
<th>
<small>Student Number:</small>
</th>
<td>
<small>S000000</small>
</td>
</tr>
<tr class="alt_color" align="right">
<th>
<small>Name:</small>
</th>
<td>
<small>Joe Blogs</small>
</td>
</tr>
|
<tr class="color" align="right">
<th>
<small>Student Number:</small>
</th>
<td>
<small><TMPL_VAR NAME="EMPLID"></small>
</td>
</tr>
<tr class="alt_color" align="right">
<th>
<small>Name:</small>
</th>
<td>
<small><TMPL_VAR NAME="FIRST_NAME"> <TMPL_VAR NAME="LAST_NAME"></small>
</td>
</tr>
|
The "names" of the HTML::Template variables match the field names in the database, which matches the hash keys in the StudentInfo hash.
Modify the view
#-- Add the student data
foreach my $key ( keys %{$self->{MODEL}->{STUDENT_INFO}->{DATA}} )
{
$self->{TEMPLATE}->param( $key
$self->{MODEL}->{STUDENT_INFO}->{DATA}->[0]->{KEY} );
}
Modify the template (current units)
This is a bit more difficult. Needs a loop to show the same content for each course. At this stage we haven't added in the details for the study materials for each course. So will leave that until later, leave it hard-coded again.
<TMPL_LOOP NAME="COURSES">
<tr class="table_banner">
<th colspan="2">
<small><TMPL_VAR NAME="SUBJECT"><TMPL_VAR NAME="CATALOG_NBR">,
<TMPL_VAR NAME="DESCR"></small>
</th>
</tr>
<tr class="alt_color">
<th align="left" >
<small>Material</small>
</th>
<th align="left" >
<small>Size</small>
</th>
</tr>
<tr class="color">
<td >
<small><a href="somewhere">Study guide</a></small>
</td>
<td >
<small>4.5Mb</small>
</td>
</tr>
</TMPL_LOOP>
Modify the View
To use the HTML::Template loop we need to create an array of hashes. Each element in the array is a hash containing the information for each element in the loop. At this stage the DATA array in from CurrentUnits is in the right format.
$self->{TEMPLATE}->param( "COURSES" =>
$self->{MODEL}->{CURRENT_UNITS}->{DATA} );
Adding the StudyMaterials Information
Modify the template
Need to add in an inner loop to show each of the materials for the course.
<TMPL_LOOP NAME="COURSE_MATERIALS">
<tr class="color">
<td >
<small><a href="<TMPL_VAR NAME="LINK">">
<TMPL_VAR NAME="MATERIAL_TYPE"></a></small>
</td>
<td >
<small><TMPL_VAR NAME="SIZE"></small>
</td>
</tr>
</TMPL_LOOP>
Modify the view
Need to get the data about the actual PDF files of StudyMaterials. There is an new model StudyMaterial::StudyMaterials that will gather inforamtion about all the PDF files currently on the UPU drive for a particular period/year.
This model creates a hash called MATERIALS. The key for this hash is the course code. The value is a hash ref that contains a range of information about the actual file. e.g.
'COIS20026' => {
'CP' => {
'NLink' => 1,
'ATime' => 1202456714,
'CTime' => 1202456658,
'Ino' => 3249325,
'Dev' => 234881026,
'MTime' => 1202099849,
'Type' => 'f',
'Size' => '491248',
'BlkSize' => 4096,
'Blocks' => 960,
'Path' => 'DOCROOT/2008-1/Course Profiles/COIS-20026-CP-1-08-B37344.pdf',
'Mode' => 33216,
'Gid' => 501,
'Uid' => 501,
'RDev' => 0
},
'SG' => {
'NLink' => 1,
'ATime' => 1202456676,
'CTime' => 1202456635,
'Ino' => 3249281,
'Dev' => 234881026,
'MTime' => 1201563439,
'Type' => 'f',
'Size' => '347766',
'BlkSize' => 4096,
'Blocks' => 680,
'Path' => 'DOCROOT/2008-1/COIS/SG/COIS-20026-SG-1-08-B36090.pdf',
'Mode' => 33216,
'Gid' => 501,
'Uid' => 501,
'RDev' => 0
}
}
For the student view the information we need is
- the link to the file;
- the type of file (study guide, resource materials etc)
- the size of the file
Need to add a hash for each type of file to a loop for each course. i.e. an entry COURSE_MATERIALS to the CURRENT_UNITS.
First, need to add the StudyMaterial model into the ShowStudyMaterialsModel.




