This document describes the steps required to port a single ACS 3.x module, simple survey to ACS 4.0. The total time to do the port, or at least bring the module into a useable and reasonably bug-free state, was 54.5 programmer hours. Note that the programmer in question had no prior knowledge of ACS 4.0, nor of the Oracle object model used extensively in ACS 4.0, but did have some prior knowledge of the internal workings of the simple survey module. Your mileage may vary.
Why would you want to port a module to ACS 4.0? The new system offers significant advantages over 3.x, notably packaging, a unified permissions model and an object model that simplifies some aspects of implementation.
In the case of the simple survey module I performed the tasks required for the port in the following order:
The ACS 4.0 packaging systems replaces the sometimes haphazard file layout used in earlier versions. Complete instructions can be found at http://acs40.arsdigita.com/doc/developer-guide/packages.html but in essence your file layout should be as follows:
The packages directory itself is in the server root directory, usually /web/server-name.
ACS 4.0 provides easy to use package management facilities to associate files, watch TCL libraries for changes and mount packages at arbitrary URLs on the client site. This is managed through the admin interface provided at acs-admin/apm under a standard ACS 4.0 install. The process is self-explanatory: simply click on 'Create a new package' and fill in the resulting form. You will then be presented with a list of files to become part of the package, if extraneous files (e.g. editor backup files) have been selected, simply deselect them. Providing naming conventions have been adhered to, TCL library files should be so identified and you will have the option of setting up a watch on them so that they are automatically reloaded when changed. This is useful during the development process as it negates the need to restart AOLserver every time a change is made in the TCL library.
Once a package has been installed it needs to be mounted before it is visible to users. This is done using the site administration at acs-admin. First, select the subsite on which you want to mount an instance of the package and then select Site Map. In a default ACS 4.0 installation there is only one subsite, Main Site and its site map is available at admin/site-map.
We then need to create a new URL under an existing URL - for the infoshare site I created a URL, investor_profiling_tool, under the root. Do this by clicking on the 'new sub folder' option alongisde and existing URL and entering the name for the URL - note that this does not have to be the same as the package key.
Next, we must mount the package. If the package has been mounted before, you may simply select the 'mount' option alongside the new URL and select the package. However, if this is the first time you have mounted the package, select 'new application'. You can then name the application (give it a pretty name rather than the package key) and select the package from a select box. This way, you can have several instances of a package, perhaps some unmounted and some mounted, simultaneously at different URLs on the same site.
Rewriting the data model is possibly the hardest part of performing the port, as the object system employed may seem counter-intuitive to programmers who have cut their OOP teeth on C++ or Java. The principal decision to be made is which objects in your module should be converted to the object model, and which should remain as simple tables. There is no clear answer to this, but a rule of thumb is that any table that contains a reference to the users table in ACS 3.x, i.e. any table that you will perform permissions tests on, should be converted to the object model. Additionally, any table which is to be included in the site-wide search should be made on object. In the case of simple survey, I converted the survsimp_surveys (individual surveys), survsimp_questions (questions which make up a survey) and survsimp_responses (collections of answers forming a single users response to a survey) tables to the object model.
All object types are derived from the acs_object type, so we declare a survsimp_survey type using the following PL/SQL block:
begin acs_object_type.create_type ( supertype => 'acs_object', object_type => 'survsimp_survey', pretty_name => 'Simple Survey', pretty_plural => 'Simple Surveys', table_name => 'SURVSIMP_SURVEYS', id_column => 'SURVEY_ID' ); end;
The new object type, survsimp_survey, is associated with a table, survsimp_surveys (note the pluralisation), each row of which will be an instance of the survsimp_survey type:
create table survsimp_surveys ( survey_id constraint survsimp_surveys_survey_id_fk references acs_objects (object_id) constraint survsimp_surveys_pk primary key, name varchar(100) constraint survsimp_surveys_name_nn not null, -- short, non-editable name we can identify this survey by short_name varchar(20) constraint survsimp_surveys_short_name_u unique constraint survsimp_surveys_short_name_nn not null, description varchar(4000) constraint survsimp_surveys_desc_nn not null, description_html_p char(1) constraint survsimp_surv_desc_html_p_ck check(description_html_p in ('t','f')), enabled_p char(1) constraint survsimp_surveys_enabled_p_ck check(enabled_p in ('t','f')), -- limit to one response per user single_response_p char(1) constraint survsimp_sur_single_resp_p_ck check(single_response_p in ('t','f')), single_editable_p char(1) constraint survsimp_surv_single_edit_p_ck check(single_editable_p in ('t','f')), type varchar(20) );
Compare this with the survsimp_surveys table in 3.x (items missing in the 4.0 version highlighted):
create sequence survsimp_survey_id_sequence start with 1; create table survsimp_surveys ( survey_id integer primary key, name varchar(100) not null, -- short, non-editable name we can identify this survey by short_name varchar(20) unique not null, description varchar(4000) not null, description_html_p char(1) default 'f' constraint survsimp_sur_desc_html_p_ck check(description_html_p in ('t','f')), creation_user not null references users(user_id), creation_date date default sysdate, enabled_p char(1) default 'f' check(enabled_p in ('t','f')), -- limit to one response per user single_response_p char(1) default 'f' check(single_response_p in ('t','f')), single_editable_p char(1) default 't' check(single_editable_p in ('t','f')), type varchar2(20) default 'general' );
Notice that the ACS 4.0 version has fewer columns. This is because creation_user and creation_date are inherited (kinda) from the acs_object type, and do not need to be included in the survsimp_surveys table. Additionally, the survey_id is now a reference to the object_id column of the acs_objects table and no longer has a sequence associated with it. In fact an instantiation of the survsimp_survey type should always use a survey_id generated by the acs_object_id_seq sequence, as should any type derived from the acs_object type.
Finally a constructor and destructor for the survsimp_survey type are required. These are PL/SQL procedures stored inside a package:
create or replace package survsimp_survey as function new ( survey_id in survsimp_surveys.survey_id%TYPE default null, name in survsimp_surveys.name%TYPE, short_name in survsimp_surveys.short_name%TYPE, description in survsimp_surveys.description%TYPE, description_html_p in survsimp_surveys.description_html_p%TYPE default 'f', single_response_p in survsimp_surveys.single_response_p%TYPE default 'f', single_editable_p in survsimp_surveys.single_editable_p%TYPE default 't', enabled_p in survsimp_surveys.enabled_p%TYPE default 'f', type in survsimp_surveys.type%TYPE default 'general', object_type in acs_objects.object_type%TYPE default 'survsimp_survey', creation_date in acs_objects.creation_date%TYPE default sysdate, creation_user in acs_objects.creation_user%TYPE default null, creation_ip in acs_objects.creation_ip%TYPE default null, context_id in acs_objects.context_id%TYPE default null ) return acs_objects.object_id%TYPE; procedure delete ( survey_id in survsimp_surveys.survey_id%TYPE ); end survsimp_survey; / show errors create or replace package body survsimp_survey as function new ( survey_id in survsimp_surveys.survey_id%TYPE default null, name in survsimp_surveys.name%TYPE, short_name in survsimp_surveys.short_name%TYPE, description in survsimp_surveys.description%TYPE, description_html_p in survsimp_surveys.description_html_p%TYPE default 'f', single_response_p in survsimp_surveys.single_response_p%TYPE default 'f', single_editable_p in survsimp_surveys.single_editable_p%TYPE default 't', enabled_p in survsimp_surveys.enabled_p%TYPE default 'f', type in survsimp_surveys.type%TYPE default 'general', object_type in acs_objects.object_type%TYPE default 'survsimp_survey', creation_date in acs_objects.creation_date%TYPE default sysdate, creation_user in acs_objects.creation_user%TYPE default null, creation_ip in acs_objects.creation_ip%TYPE default null, context_id in acs_objects.context_id%TYPE default null ) return acs_objects.object_id%TYPE is v_survey_id survsimp_surveys.survey_id%TYPE; begin v_survey_id := acs_object.new ( object_id => survey_id, object_type => object_type, creation_date => creation_date, creation_user => creation_user, creation_ip => creation_ip, context_id => context_id ); insert into survsimp_surveys (survey_id, name, short_name, description, description_html_p, single_response_p, single_editable_p, enabled_p, type) values (v_survey_id, new.name, new.short_name, new.description, new.description_html_p, new.single_response_p, new.single_editable_p, new.enabled_p, new.type); return v_survey_id; end new; procedure delete ( survey_id survsimp_surveys.survey_id%TYPE ) is begin delete from survsimp_surveys where survey_id = survsimp_survey.delete.survey_id; acs_object.delete(survey_id); end delete; end survsimp_survey; / show errors
The constructor, survsimp_survey.new, and destructor, survsimp_survey.delete, are declared in the create package statement and defined in the the create package body statement. The function of the constructor is to create an instance of acs_object and insert a row referencing that instance to the survsimp_surveys table. The destructor deletes both the acs_object and the survsimp_surveys.
Permissions checking under ACS 4.0 has been much simplified and is one of the most compelling reasons to migrate from 3.x. Under ACS 4.0 all permissions checks boil down to the question Can x perform action y on object z?. The actions are defined by the module writer.
The following PL/SQL statements define self explanatory actions on the simple survey package:
begin acs_privilege.create_privilege('survsimp_create_survey'); acs_privilege.create_privilege('survsimp_delete_survey'); acs_privilege.create_privilege('survsimp_modify_survey'); acs_privilege.create_privilege('survsimp_create_question'); acs_privilege.create_privilege('survsimp_delete_question'); acs_privilege.create_privilege('survsimp_modify_question'); acs_privilege.create_privilege('survsimp_take_survey'); end;
When the package is registered with the ACS 4.0 package manager, these permissions become available to the permissions manager at /permissions, which can be used assign these permissions to various users. I also created a super-privilege, survsimp_admin_survey:
begin acs_privilege.create_privilege('survsimp_admin_survey'); acs_privilege.add_child('survsimp_admin_survey','survsimp_create_survey'); acs_privilege.add_child('survsimp_admin_survey','survsimp_delete_survey'); acs_privilege.add_child('survsimp_admin_survey','survsimp_modify_survey'); acs_privilege.add_child('survsimp_admin_survey','survsimp_create_question'); acs_privilege.add_child('survsimp_admin_survey','survsimp_delete_question'); acs_privilege.add_child('survsimp_admin_survey','survsimp_modify_question'); end;
The acs_privilege.add_child function adds the privilege in the second argument to that in the first, so in this case anyone with the survsimp_admin_survey privilege has full privileges to create, modify and delete surveys and questions.
How are the new permissions used? The simplest way is to use the ad_require_permission TCL procedure. This takes two arguments, an object_id and the privilege required. If the current user does not have the required privilege, he or she is presented with an error message. For example, the page survey-simple/www/one.tcl, which presents a survey to a user who wishes to take it, contains the single line:
ad_require_permission $survey_id survsimp_take_survey
If the user has not been assigned the survsimp_take_survey privilege, he or she will not be able to proceed. A slightly more complicated case is seen in survey-simple/www/admin/question-modify-text.tcl:
ad_require_permission $question_id survsimp_modify_question
Here, the user will not be able to proceed unless he or she has the survsimp_modify_question granted on this particular question. Rather than grant privileges on every single question in a survey, the context_id parameter of the survsimp_question.new parameter is used to allow privileges granted to a user regarding the survey, to also apply to the questions. When a question is created by a call to survsimp_question.new the context_id is set to the survey_id of the survey of which the question is a member. Any user who has privileges over the survey, automatically has the same privileges over the questions. Thus, if a user is granted the survsimp_admin_survey privilege over the survey, he or she will have survsimp_admin_survey privilege, and hence the survsimp_modify_question privilege over all the questions of which that survey is comprised.
Another case is when we simply want to control access to a page rather than a particular object. For example, only users with the survsimp_admin_survey privilege should be permitted to access survey-simple/www/admin/index.tcl. In this case, we use:
ad_require_permission [ad_conn package_id] survsimp_admin_survey
This checks if the user has the correct privilege for the package, which itself is simply and instance of the acs_object type.
Finally, privileges can be used in SQL statements to restrict the rows retrieved. For example, not all users have permission to take all surveys. In survey-simple/www/index.tcl, we present the user with a list of the surveys he or she is permitted to take using the following:
db_multirow surveys survey_select { select survey_id, name from survsimp_surveys, acs_objects where object_id = survey_id and context_id = :package_id and acs_permission.permission_p(object_id, :user_id, 'survsimp_take_survey') = 't' and enabled_p = 't' order by upper(name) }