Skip to content

WeBWorK3 and Perl Dancer

Peter Staab edited this page Jun 10, 2014 · 2 revisions

#WeBWorK 3 and Perl Dancer

This explain in further depth how perl dancer is used in the WeBWorK 3 project. Perl Dancer is a perl-based library to make developing web applications simpler.

##Perl Dancer and Routes

Although not required, we have decided to use a RESTful API in that the URL is in the form of an object in webwork. For a WeBWorK example, a user with name jdoe will have the URL /users/jdoe The problem set named HW1 in course math1 is /courses/math1/sets/HW1

In addition, each routes has one or more verbs associated with the http requests. The verbs are write (POST), read (GET), update (PUT), and delete (DEL).

If you want to create, read, update or delete the set in the course math1, the routes are

POST /courses/math1/sets/HW1
GET /courses/math1/sets/HW1
PUT /courses/math1/sets/HW1
DELETE /courses/math1/sets/HW1

Dancer uses the terms "post", "get", "put", and "del" (since delete is a reserved word) for these verbs and the headers are written:

post '/courses/:course_id/sets/:set_id' => sub {  };
get '/courses/:course_id/sets/:set_id' => sub {  };
put '/courses/:course_id/sets/:set_id' => sub {  };
del '/courses/:course_id/sets/:set_id' => sub {  };

Where the terms :course_id and :set_id are called tokens and any string between /courses/ and /sets/ will match this (except a string containing "/"). In the route, /courses/math1/sets/HW1 will set the token :course_id to "math1" and :set_id to "HW1". These can be accessed via the params variable. For example:

params->{set_id} # returns "HW1"

###Details of a Dancer route

Let's dive into the details of a dancer route. We'll start with the post (CREATE), which creates a new homework set in the course. Here's the entire route and below we'll go through this line by line.

post '/courses/:course_id/sets/:set_id' => sub {
    checkPermissions(10);

    # call validator directly instead

    send_error("The set name must only contain A-Za-z0-9_-.",403) if (params->{set_id} !~ /^[\w\_.-]+$/); 

    send_error("The set name: " . param('set_id'). " already exists.",404) if (vars->{db}->existsGlobalSet(param('set_id')));

    my $set = vars->{db}->newGlobalSet();
    for my $key (@set_props) {
        $set->{$key} = params->{$key} if defined(params->{$key});
    }

    vars->{db}->addGlobalSet($set);

    for my $user(@{params->{assigned_users}}){
        addUserSet($user,params->{set_id});
    }

    addGlobalProblems(params->{set_id},params->{problems});
    addUserProblems(params->{set_id},params->{problems},params->{assigned_users});

    

    my $returnSet = convertObjectToHash($set);

    my @globalProblems = vars->{db}->getAllGlobalProblems(params->{set_id});
    $returnSet->{problems} = convertArrayOfObjectsToHash(\@globalProblems);
    $returnSet->{assigned_users} = params->{assigned_users};
    $returnSet->{_id} = params->{set_id};

    return $returnSet;


};

####Line by Line Analysis of this route

checkPermissions(10);

This method is in Routes::Authentication to check the permissions. The number indicates the level of the user (10=instructor) that can access the route.

send_error("The set name must only contain A-Za-z0-9_-.",403) if (params->{set_id} !~ /^[\w\_.-]+$/);

This checks if the set name has the right form. If not, send a 403 error indicating a forbidden error. This can then be handled from the client in an appropriate manner.

send_error("The set name: " . param('set_id'). " already exists.",404) if (vars->{db}->existsGlobalSet(param('set_id')));

This checks if the set name already exists. If so, it sends a 404 error.

my $set = vars->{db}->newGlobalSet();
for my $key (@set_props) {
    $set->{$key} = params->{$key} if defined(params->{$key});
}

This creates a new global problem set and sets the fields that were sent via the POST. The @set_props is defined at the top of this file to ensure that only known properties are attempted to be set.

Also, the vars->{db} is where the reference to the database is stored. All methods on the database object are located in the DB.pm file

vars->{db}->addGlobalSet($set);

Adds the set created and defined above to the database.

for my $user(@{params->{assigned_users}}){
    addUserSet($user,params->{set_id});
}

The users that are assigned to the set are contained in parameter assigned_users (an array of login names or user_id's). This creates (adds) a UserSet for each user.

addGlobalProblems(params->{set_id},params->{problems});
addUserProblems(params->{set_id},params->{problems},params->{assigned_users});

These are defined in Utils::ProblemSets and adds both Global and User problems. The problems are in the array params->{problems}.

my $returnSet = convertObjectToHash($set);

The last few steps sets up a hash to return to the client so that the client and the server data is in sync. This line converts the object in the problem set $returnSet to a hash. This method is defined in Utils::Convert

my @globalProblems = vars->{db}->getAllGlobalProblems(params->{set_id});
$returnSet->{problems} = convertArrayOfObjectsToHash(\@globalProblems);

and this returns an array of the global problems for the set and converts it to an array of hashes. This method is also defined in Utils::Convert.

$returnSet->{assigned_users} = params->{assigned_users};

and this adds the array of assigned_users to the set.

$returnSet->{_id} = params->{set_id};

and lastly this gives a value for _id. This is very helpful for Backbone models to understand if an object on the client side is new or old, mainly for the purpose of sending to the server a PUT or a POST request. Now that a set has a _id field, Backbone will treat this as an old object. See Backbone.js idAttribute for more explanation.

###Adding the Course Environment to a route

Fundamental to the way that WeBWorK operates is the CourseEnvironment. This contains all information about a course including settings, the database object, etc. In many of the routes for WW3, the course environment is needed. You may notice, however, that from the example above, that it was not defined at all.

Perl Dancer has a wonderful feature in that if you would like common code to be in every route that as long as the route information is processed before another one that it will match first and thus be executed. For example in Routes::Authentication, the following line is toward the top.

any ['get','put','post','delete'] => '/courses/*/**' => sub {
	my ($courseID) = splat;
	setCourseEnvironment($courseID);
	pass;
};

This routes matches any route that starts with /courses/. The line

my ($courseID) = splat;

pulls the courseID (course name) from the route. The term splat is a dancer keyword to handle route matching. See the Dancer documentation for more information.

The line

setCourseEnvironment($courseID);

is a method in Routes::Authentication that creates a CourseEnvironment object which gets stored in vars->{ce} and since the database object is very important that is stored in vars->{db}, which you saw in the example route above.

Lastly the line pass in the route then passes the route to the next matched route. For example if a POST /courses/math1/sets/HW1 is made, then first it will be matched to the route above, which sets the course environment, then will be passed to the next route, which is the one discussed above.