Quicklinks: The Toolbox > CD&DU Feeds

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

  1. An overview of what wf is
  2. 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

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

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

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.

Image:Materials.png

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

Is taken from Peoplesoft and all classes for Peoplesoft are in ~/webfuse/lib/People. Relevant classes include

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

Changes required include

use webfuse::lib::People::CurrentStudentUnits;
  #-- 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

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

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.

 
 
 

toolbox

What links here | Related changes | Upload file | Special pages | Permanent link

 
 

Our Talk

 
 

Our Links