LP1207396 Patron self-registration web form
authorBill Erickson <berick@esilibrary.com>
Thu, 1 Aug 2013 20:52:18 +0000 (16:52 -0400)
committerDan Wells <dbw2@calvin.edu>
Fri, 23 Aug 2013 23:16:14 +0000 (19:16 -0400)
Web form for allowing patrons to submit thier own library registration
requests.  The form collects various bits of user data and creates a
pending user account, which has no privileges.  These pending accounts
must be approved and completed by staff from the staff client "Pending
Patrons" action in the Cirulation menu.

Control of whether to show a field, treat a field as required, show
example text, and validate the field format is managed with existing org
unit settings.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Dan Wells <dbw2@calvin.edu>
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Register.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/parts/footer.tt2
Open-ILS/src/templates/opac/parts/org_selector.tt2
Open-ILS/src/templates/opac/register.tt2 [new file with mode: 0644]

index 210a3c0..48931e8 100644 (file)
@@ -24,6 +24,7 @@ use OpenILS::WWW::EGCatLoader::Search;
 use OpenILS::WWW::EGCatLoader::Record;
 use OpenILS::WWW::EGCatLoader::Container;
 use OpenILS::WWW::EGCatLoader::SMS;
+use OpenILS::WWW::EGCatLoader::Register;
 
 my $U = 'OpenILS::Application::AppUtils';
 
@@ -140,6 +141,7 @@ sub load {
     return $self->redirect_ssl unless $self->cgi->https;
     return $self->load_password_reset if $path =~ m|opac/password_reset|;
     return $self->load_logout if $path =~ m|opac/logout|;
+    return $self->load_patron_reg if $path =~ m|opac/register|;
 
     if($path =~ m|opac/login|) {
         return $self->load_login unless $self->editor->requestor; # already logged in?
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Register.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Register.pm
new file mode 100644 (file)
index 0000000..cdbe9a4
--- /dev/null
@@ -0,0 +1,242 @@
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK FORBIDDEN HTTP_INTERNAL_SERVER_ERROR);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Event;
+use Data::Dumper;
+$Data::Dumper::Indent = 0;
+my $U = 'OpenILS::Application::AppUtils';
+
+sub load_patron_reg {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $cgi = $self->cgi;
+    $ctx->{register} = {};
+    $self->collect_register_validation_settings;
+    $self->collect_requestor_info;
+
+    # in the home org unit selector, we only want to present 
+    # org units to the patron which support self-registration.
+    # all other org units will be disabled
+    $ctx->{register}{valid_orgs} = 
+        $self->setting_is_true_for_orgs('opac.allow_pending_user');
+
+    # just loading the form
+    return Apache2::Const::OK
+        unless $cgi->request_method eq 'POST';
+
+    my $user = Fieldmapper::staging::user_stage->new;
+    my $addr = Fieldmapper::staging::mailing_address_stage->new;
+
+    # user
+    foreach (grep /^stgu\./, $cgi->param) {
+        my $val = $cgi->param($_);
+        $self->inspect_register_value($_, $val);
+        s/^stgu\.//g;
+        $user->$_($val);
+    }
+
+    # requestor is logged in, capture who is making this request
+    $user->requesting_usr($ctx->{user}->id) if $ctx->{user};
+
+    # make sure the selected home org unit is in the list 
+    # of valid orgs.  This can happen if the selector 
+    # defaults to CONS, for example.
+    $ctx->{register}{invalid}{bad_home_ou} = 1 unless
+        grep {$_ eq $user->home_ou} @{$ctx->{register}{valid_orgs}};
+
+    # address
+    my $has_addr = 0;
+    foreach (grep /^stgma\./, $cgi->param) {
+        my $val = $cgi->param($_);
+        $self->inspect_register_value($_, $val);
+        s/^stgma\.//g;
+        $addr->$_($val);
+        $has_addr = 1;
+    }
+
+    # if the form contains no address fields, do not 
+    # attempt to create a pending address
+    $addr = undef unless $has_addr;
+
+    # At least one value was invalid. Exit early and re-render.
+    return Apache2::Const::OK if $ctx->{register}{invalid};
+
+    $self->test_requested_username($user);
+
+    # user.stage.create will generate a temporary usrname and 
+    # link the user and address objects via this username in the DB.
+    my $resp = $U->simplereq(
+        'open-ils.actor', 
+        'open-ils.actor.user.stage.create',
+        $user, $addr
+    );
+
+    if (!$resp or ref $resp) {
+
+        $logger->warn("Patron self-reg failed ".Dumper($resp));
+        $ctx->{register}{error} = 1;
+
+    } else {
+
+        $logger->info("Patron self-reg success; usrname $resp");
+        $ctx->{register}{success} = 1;
+    }
+
+    return Apache2::Const::OK;
+}
+
+# if the pending account is requested by an existing user account,
+# load the existing user's data to pre-populate some fields.
+sub collect_requestor_info {
+    my $self = shift;
+    return unless $self->ctx->{user};
+
+    my $user = $self->editor->retrieve_actor_user([
+        $self->ctx->{user}->id,
+        {flesh => 1, flesh_fields => {
+            au => [qw/mailing_address billing_address/]}
+        }
+    ]);
+
+
+    my $vhash = $self->ctx->{register}{values} = {};
+    my $addr = $user->mailing_address || $user->billing_address;
+    $vhash->{stgu}{home_ou} = $user->home_ou;
+
+    if ($addr) {
+        $vhash->{stgma}{city} = $addr->city;
+        $vhash->{stgma}{county} = $addr->county;
+        $vhash->{stgma}{state} = $addr->state;
+        $vhash->{stgma}{post_code} = $addr->post_code;
+    }
+}
+
+# if the username is in use by an actor.usr OR a 
+# pending user treat it as taken and warn the user.
+sub test_requested_username {
+    my ($self, $user) = @_;
+    my $uname = $user->usrname || return;
+    my $e = $self->editor;
+
+    my $taken = $e->search_actor_user(
+        {usrname => $uname, deleted => 'f'}, 
+        {idlist => 1}
+    )->[0];
+
+    $taken = $e->search_staging_user_stage(
+        {usrname => $uname}, 
+        {idlist => 1}
+    )->[0] unless $taken;
+
+    if ($taken) {
+        $self->ctx->{register}{username_taken} = 1;
+        $user->clear_usrname;
+    }
+}
+
+sub collect_register_validation_settings {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $e = new_editor();
+    my $ctx_org = $ctx->{physical_loc} || $self->_get_search_lib;
+    my $shash = $self->{register}{settings} = {};
+
+    # retrieve the org unit setting types and values
+    # that are relevant to our validation tasks.
+
+    my $settings = $e->json_query({
+        select => {coust => ['name']},
+        from => 'coust',
+        where => {name => {like => 'ui.patron.edit.%.%.%'}}
+    });
+
+    # load org setting values for all of the regex, 
+    # example, show, and require settings
+    for my $set (@$settings) {
+        $set = $set->{name};
+        next unless $set =~ /regex$|show$|require$|example$/;
+
+        my $val = $ctx->{get_org_setting}->($ctx_org, $set);
+        next unless $val; # no configured org setting
+
+        # extract the field class, name, and 
+        # setting type from the setting name
+        my (undef, undef, undef, $cls, $field, $type) = split(/\./, $set);
+
+        # translate classes into stage classes
+        my $scls = ($cls eq 'au') ? 'stgu' : 'stgma';
+
+        $shash->{$scls}{$field}{$type} = $val;
+    }
+
+    # use the generic phone settings where none are provided for day_phone.
+
+    $shash->{stgu}{day_phone}{example} =
+        $ctx->{get_org_setting}->($ctx_org, 'ui.patron.edit.phone.example')
+        unless $shash->{stgu}{day_phone}{example};
+
+    $shash->{stgu}{day_phone}{regex} =
+        $ctx->{get_org_setting}->($ctx_org, 'ui.patron.edit.phone.regex')
+        unless $shash->{stgu}{day_phone}{regex};
+
+    # some fields are assumed to be visible / required even without the            
+    # presence of org unit settings.  E.g. we obviously want the user to 
+    # enter a name, since a name is required for ultimately creating a user 
+    # account.  We can mimic that by forcing some org unit setting values
+    
+    $shash->{stgu}{first_given_name}{require} = 1
+        unless defined $shash->{stgu}{first_given_name}{require};
+    $shash->{stgu}{second_given_name}{show} = 1
+        unless defined $shash->{stgu}{second_given_name}{show};
+    $shash->{stgu}{family_name}{require} = 1
+        unless defined $shash->{stgu}{family_name}{require};
+    $shash->{stgma}{street1}{require} = 1
+        unless defined $shash->{stgma}{street1}{require};
+    $shash->{stgma}{street2}{show} = 1
+        unless defined $shash->{stgma}{street2}{show};
+    $shash->{stgma}{city}{require} = 1
+        unless defined $shash->{stgma}{city}{require};
+    $shash->{stgma}{post_code}{require} = 1
+        unless defined $shash->{stgma}{post_code}{require};
+    $shash->{stgu}{usrname}{show} = 1
+        unless defined $shash->{stgu}{usrname}{show};
+
+    $ctx->{register}{settings} = $shash;
+}
+
+# inspects each value and determines, based on org unit settings, 
+# if the value is invalid.  Invalid is defined as not providing 
+# a value when one is required or not matching the configured regex.
+sub inspect_register_value {
+    my ($self, $field_path, $value) = @_;
+    my $ctx = $self->ctx;
+    my ($scls, $field) = split(/\./, $field_path);
+
+    if (!$value) {
+
+        if ($self->{register}{settings}{$scls}{$field}{require}) {
+            $ctx->{register}{invalid}{$scls}{$field}{require} = 1;
+
+            $logger->info("patron register field $field ".
+                "requires a value, but none was entered");
+        }
+        return;
+    }
+
+    my $regex = $self->{register}{settings}{$scls}{$field}{regex};
+    return if !$regex or $value =~ /$regex/; # field is valid
+
+    $logger->info("invalid value was provided for patron ".
+        "register field=$field; pattern=$regex; value=$value");
+
+    $ctx->{register}{invalid}{$scls}{$field}{regex} = 1;
+
+    return;
+}
+
+
+
index 6382210..142f537 100644 (file)
@@ -753,7 +753,24 @@ sub load_org_util_funcs {
 
 }
 
+# returns the list of org unit IDs for which the 
+# selected org unit setting returned a true value
+sub setting_is_true_for_orgs {
+    my ($self, $setting) = @_;
+    my $ctx = $self->ctx;
+    my @valid_orgs;
+
+    my $test_org;
+    $test_org = sub {
+        my $org = shift;
+        push (@valid_orgs, $org->id) if
+            $ctx->{get_org_setting}->($org->id, $setting);
+        $test_org->($_) for @{$org->children};
+    };
 
+    $test_org->($ctx->{aou_tree}->());
+    return \@valid_orgs;
+}
     
 
 
index 39cd573..3d827d5 100644 (file)
@@ -1584,3 +1584,28 @@ a.preflib_change {
 .bib_peer_type {
     font-weight: bold;
 }
+
+#main-content-register {
+    margin-left: 40px;
+    font-size: 120%;
+}
+
+#main-content-register table { 
+    padding: 20px; 
+    margin-top: 18px; 
+    border-collapse: collapse;
+}
+
+#main-content-register td {
+    text-align: left;
+}
+
+#main-content-register td:not(:first-child) {
+    padding-left: 20px;
+}
+
+.patron-reg-invalid {
+    font-weight: bold;
+    color: red;
+    padding-right: 10px;
+}
index 2368055..cf43f7c 100644 (file)
@@ -1,8 +1,13 @@
 <div id="footer-wrap">
 <div id="footer">
     <a href="/">[% l('Dynamic catalog') %]</a> &nbsp;|&nbsp;
-    <a href="http://example.com">[% l('Bottom Link 2') %]</a> &nbsp;|&nbsp;
+    [% IF ctx.get_org_setting(
+        ctx.physical_loc || ctx.aou_tree.id, 'opac.allow_pending_user') %]
+    <a href="[% mkurl(ctx.opac_root _ '/register') %]">[% 
+        l('Request Library Card') %]</a> &nbsp;|&nbsp;
+    [% ELSE %]
     <a href="http://example.com">[% l('Bottom Link 3') %]</a> &nbsp;|&nbsp;
+    [% END %]
     <a href="http://example.com">[% l('Bottom Link 4') %]</a> &nbsp;|&nbsp;
     <a href="http://example.com">[% l('Bottom Link 5') %]</a>
     [% IF ctx.timing %]
index d82933d..262fe40 100644 (file)
@@ -2,6 +2,7 @@
 # Org Unit Selector Widget :
 #   INCLUDE build_org_selector id='selector-id' name='selector-name' 
 #       value=org_id show_loc_groups=1/0 can_have_vols_only=1/0
+#       can_have_users_only=1/0
 #
 # NOTE: DO NOT USE PROCESS
 # Use of PROCESS results in internal variables, such as value or org_unit, to "leak" out
@@ -90,6 +91,12 @@ BLOCK build_org_selector;
                 selected = 'selected="selected"';
             END; 
 
+            IF can_have_users_only AND org_unit.ou_type.can_have_users != 't';
+                disabled = 'disabled="disabled"';
+            ELSIF node_value == value;
+                selected = 'selected="selected"';
+            END; 
+
             pad_depth = 0;
 
             # copy loc groups appear as children of the owning org unit
diff --git a/Open-ILS/src/templates/opac/register.tt2 b/Open-ILS/src/templates/opac/register.tt2
new file mode 100644 (file)
index 0000000..a1c7b0e
--- /dev/null
@@ -0,0 +1,177 @@
+[%- PROCESS "opac/parts/header.tt2";
+    PROCESS "opac/parts/org_selector.tt2";
+    WRAPPER "opac/parts/base.tt2";
+    INCLUDE "opac/parts/topnav.tt2";
+    ctx.page_title = l("Request Library Card");
+
+# some useful variables and MACROs for display, 
+# field validation, and added info display
+
+ctx_org = ctx.physical_loc || ctx.search_ou || ctx.aou_tree.id;
+
+# list of the registration fields to (potentially) 
+# display in the order they should be shown
+
+# post_code is the only field below that is required in the database and
+# post_code is only required if an address is created.
+# To prevent any of these fields from showing locally, regardless org unit
+# settings, simply remove the fields from this list.  In the case of 
+# addresses, if all address fields are removed, no attempt at creating
+# an address will be made (and post_code will no longer be required).
+
+register_fields = [
+    {class => 'stgu',  name = 'first_given_name', label => l('First Name')},
+    {class => 'stgu',  name = 'second_given_name', label => l('Middle Name')},
+    {class => 'stgu',  name = 'family_name', label => l('Last Name')},
+    {class => 'stgma', name = 'street1', label => l('Street Address')},
+    {class => 'stgma', name = 'street2', label => l('Street Address (2)')},
+    {class => 'stgma', name = 'city', label => l('City')},
+    {class => 'stgma', name = 'county', label => l('County')},
+    {class => 'stgma', name = 'state', label => l('State')},
+    {class => 'stgma', name = 'post_code', label => l('Zip Code')},
+    {class => 'stgu',  name = 'dob', label => l('Date of Birth')},
+    {class => 'stgu',  name = 'day_phone', label => l('Phone Number')},
+    {class => 'stgu',  name = 'email', label => l('Email Address')}
+    {class => 'stgu',  name = 'usrname', label => l('Requested Username')}
+];
+%]
+
+<div id="content-wrapper">
+    <div id="main-content-register">
+        <div class="common-full-pad"></div>
+        <h1>[% l('Request a Library Card')%]</h1>
+        <hr/>
+
+        [% IF ctx.register.success %]
+            <h3>[% l('Registration successful!') %]<h3>
+            <h4>[% l('Please see library staff to complete your registration.') %]</h4>
+
+            [% IF ctx.register.username_taken %]
+            <p>
+                [% |l %]
+                Note: The selected username may be in use by another patron.  
+                You may select another username when finalizing your 
+                registration or in the online catalog.
+                [% END %]
+            </p>
+            [% END %]
+
+            <br/>
+            <p>
+                <a href="[% ctx.opac_root %]/home" 
+                    class="opac-button">[% l('Return to the Catalog') %]</a>
+            </p>
+
+        [% ELSIF ctx.register.error %]
+            <h3>[% l('A registration error has occurred') %]</h3>
+            <h4>[% l('Please see library staff to complete your registration.') %]</h4>
+
+            <br/>
+            <p>
+                <a href="[% ctx.opac_root %]/home" 
+                    class="opac-button">[% l('Return to the Catalog') %]</a>
+            </p>
+
+        [% ELSE %]
+
+        [% IF ctx.user %]
+            <!-- if the user is logged in, make it 
+                clear we are tracking the requestor -->
+            <h4>[% l('New account requested by [_1] [_2] [_3] [_4] [_5]',
+                    ctx.user.prefix, ctx.user.first_given_name,
+                    ctx.user.second_given_name, ctx.user.family_name,
+                    ctx.user.suffix
+                ) | html %]</h4>
+        [% END %]
+
+        <form method='POST'>
+            <table>
+                <tr>
+                    <td>
+                        <label for='stgu.home_ou'>[% l('Home Library') %]</label>
+                    </td>
+                    <td>[% INCLUDE build_org_selector 
+                            name='stgu.home_ou' 
+                            value=value || ctx_org
+                            can_have_users_only=1
+                            valid_org_list=ctx.register.valid_orgs
+                        %]
+                    </td>
+                    <td>
+                        [% IF ctx.register.invalid.bad_home_ou %]
+                        <span class='patron-reg-invalid'>
+                            [% l('Please select a valid library') %]
+                        </span>
+                        [% END %]
+                </tr>
+[%
+# <=== shifting code left for readability
+
+# render the table row for each of the register fields
+FOR field_def IN register_fields;
+    fclass = field_def.class;
+    fname = field_def.name;
+    field_path = fclass _ "." _ fname;
+
+    show = ctx.register.settings.$fclass.$fname.show;
+    require = ctx.register.settings.$fclass.$fname.require;
+    example = ctx.register.settings.$fclass.$fname.example;
+    value = ctx.register.values.$fclass.$fname;
+
+    invalid_require = ctx.register.invalid.$fclass.$fname.require;
+    invalid_regex = ctx.register.invalid.$fclass.$fname.regex;
+
+    NEXT UNLESS require OR show;
+%]
+<tr>
+    <td>
+        <label for='[% field_path %]'>[% field_def.label | html %]</label>
+    </td>
+    <td>
+        <input 
+            type='text' 
+            name='[% field_path %]'
+            value='[% value || CGI.param(field_path) | html %]'/>
+        [% IF require %]
+        <span class='patron-reg-invalid'>*</span>
+        [% END %]
+    </td>
+    <td>
+
+    <!-- display errors and example text -->
+
+    [% IF invalid_require %]
+        <span class='patron-reg-invalid'>
+            [% l('This field is required') %]
+        </span>
+    [% ELSIF invalid_regex %]
+        <span class='patron-reg-invalid'>
+            [% l('The value entered does not have the correct format') %]
+        </span>
+    [% END %]
+    [% IF example %]
+        <span class='patron-reg-extra'>
+            [% l('(Example: [_1])', example) %]
+        </span>
+    [% END %]
+
+    </td>
+</tr>
+[% END %]
+<!-- ====> shifting the code back to the right for context -->
+                    <tr>
+                        <td colspan='3'>
+                            <a href="[% ctx.opac_root %]/home" 
+                                class="opac-button">[% l('Go Back') %]</a>
+                            <input type="submit" 
+                                value="[% l('Submit Registration') %]" 
+                                class="opac-button" />
+                        </td>
+                    </tr>
+                </table>
+            </form>
+            [% END %]
+            <div class="common-full-pad"></div>        
+        </div>
+    </div>
+[%- END %]