This feature supplies the ability to create alternate templates for Action Triggers
authorRogan Hamby <rogan.hamby@gmail.com>
Wed, 23 Feb 2022 18:55:01 +0000 (13:55 -0500)
committerJane Sandberg <sandbergja@gmail.com>
Mon, 28 Mar 2022 12:33:11 +0000 (05:33 -0700)
that will generate locale specific out for Action Triggers.  If you send notices in
multiple languages, we recommend putting some words to that effect in your notice
templates.  The template, message and message title can all be localized.  To use the
feature the following new UI elements have been added:

- When you double-click on an Event Definition under Notifications / Action Triggers
  to edit it there will be a tab option for Edit Alternate Template if the reactor is
  ProcessTemplate, SendEmail or SendSMS.
- In the Patron Registration and Patron Editor screens staff members may now select a
  locale for a patron and edit it in the Patron Preferred Language field.
- Patrons may set their own locale in the My Account interface off the OPAC by going to
  Preferences -> Personal Information and setting the Preferred Language field.

The templates used on the Edit Definition tab are the defaults that are used if there are
no alternate templates available that match the preferred language.  If alternate templates
are available the system will use a locale that is an exact match and then if failing that
use one where the language code matches and then fall back to the default one.

For example, if a patron has a locale of fr-CA and there are templates for both fr-CA and
fr-FR it will use the fr-CA.  If the fr-CA template was deleted it would fall back on using
the fr-FR for the patron since it at least shares the same base language.

Valid locales are the codes defined in the i18n_locale table in the config schema.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Jane Sandberg <sandbergja@gmail.com>

19 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/local/triggers/trigger-edit.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/triggers/trigger-edit.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
Open-ILS/src/sql/Pg/upgrade/xxxx.schema.preferred_locale_and_alternate_at_templates.sql [new file with mode: 0644]
Open-ILS/src/templates-bootstrap/opac/css/style.css.tt2
Open-ILS/src/templates-bootstrap/opac/myopac/prefs.tt2
Open-ILS/src/templates-bootstrap/opac/myopac/update_locale.tt2 [new file with mode: 0755]
Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
docs/RELEASE_NOTES_NEXT/Administration/localized_action_triggers.adoc [new file with mode: 0644]

index e3bdba8..92abe15 100644 (file)
@@ -1333,6 +1333,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Is Error" name="is_error" reporter:datatype="bool"/>
                        <field reporter:label="Events" name="events" oils_persist:virtual="true"  reporter:datatype="link"/>
                        <field reporter:label="Error Events" name="error_events" oils_persist:virtual="true"  reporter:datatype="link"/>
+            <field reporter:label="Output Locale" name="locale" reporter:datatype="text"/>
                </fields>
                <links>
             <link field="events" reltype="has_many" key="template_output" map="" class="atev"/>
@@ -1508,6 +1509,39 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                </permacrud>
        </class>
 
+    <class id="atevalt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action_trigger::alternate_template" oils_persist:tablename="action_trigger.alternate_template" reporter:label="Alternate Action Trigger Templates">
+        <fields oils_persist:primary="id" oils_persist:sequence="action_trigger.alternate_template_id_seq">
+            <field reporter:label="Alternate Template ID" name="id" reporter:datatype="id"/>
+            <field reporter:label="Enabled" name="active" reporter:datatype="bool"/>
+            <field reporter:label="Template" name="template"  reporter:datatype="text"/>
+            <field reporter:label="Template Locale" name="locale" reporter:datatype="link" oils_obj:required="true"/>
+            <field reporter:label="Message Title" name="message_title" reporter:datatype="text"/>
+            <field reporter:label="Message Template" name="message_template" reporter:datatype="text"/>
+            <field reporter:label="Event Definition" name="event_def" reporter:datatype="link"/>
+        </fields>
+        <links>
+            <link field="event_def" reltype="has_a" key="id" map="" class="atevdef"/>
+            <link field="locale" relteype="has_a" key="code" map="" class="i18n_l"/>
+         </links>
+         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+             <actions>
+                 <create permission="ADMIN_TRIGGER_EVENT_DEF CREATE_TRIGGER_EVENT_DEF">
+                     <context link="event_def" field="owner"/>
+                 </create>
+                 <retrieve permission="ADMIN_TRIGGER_EVENT_DEF VIEW_TRIGGER_EVENT_DEF">
+                     <context link="event_def" field="owner"/>
+                 </retrieve>
+                 <update permission="ADMIN_TRIGGER_EVENT_DEF UPDATE_TRIGGER_EVENT_DEF">
+                     <context link="event_def" field="owner"/>
+                 </update>
+                 <delete permission="ADMIN_TRIGGER_EVENT_DEF DELETE_TRIGGER_EVENT_DEF">
+                     <context link="event_def" field="owner"/>
+                 </delete>
+            </actions>
+        </permacrud>
+    </class>
+
+
        <class id="atevdefg" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action_trigger::event_def_group" oils_persist:tablename="action_trigger.event_def_group" reporter:label="Trigger Event Definition Group" oils_persist:restrict_primary="100">
                <fields oils_persist:primary="id" oils_persist:sequence="action_trigger.event_def_group_id_seq">
                        <field reporter:label="Group ID" name="id" reporter:datatype="id"/>
@@ -4118,6 +4152,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Reservations" name="reservations" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="User Activity Entries" name="usr_activity" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="User/Working Location Map" name="usr_work_ou_map" oils_persist:virtual="true" reporter:datatype="link"/>
+            <field reporter:label="Patron Preferred Language" name="locale" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="demographic" reltype="might_have" key="id" map="" class="rud"/>
@@ -4152,6 +4187,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="reservations" reltype="has_many" key="usr" map="" class="bresv"/>
                        <link field="usr_activity" reltype="has_many" key="usr" map="" class="auact"/>
                        <link field="usr_work_ou_map" reltype="has_many" key="usr" map="" class="puwoum"/>
+            <link field="locale" reltype="has_a" key="code" map="" class="i18n_l"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
index cee9987..34a66a8 100644 (file)
             <div class="text-right">
                 <button class="btn btn-outline-dark mr-3" (click)="back()">&#8592; Back to Notifications/Action Triggers</button>
             </div>
-       </ng-template>
+    </ng-template>
+    </li>
+    <li ngbNavItem="'alt'" *ngIf=this.evtAltEligible>
+        <a ngbNavLink i18n>Edit Alternate Template</a>
+        <ng-template ngbNavContent>
+            <ng-template #textAreaTemplate let-field="field" let-record="record">
+                <textarea class="form-control" name="{{field.name}}"
+                    [readonly]="field.readOnly" [required]="field.isRequired()"
+                    [ngModel]="record[field.name]()"
+                    (ngModelChange)="record[field.name]($event)" style="height: 600px;">
+                </textarea>
+            </ng-template>
+            <h3 class="mb-3">Alternate Templates</h3>
+            <eg-grid #altTemplateGrid idlClass="atevalt" [dataSource]="altTemplateDataSource"
+                showFields="active,locale" persistKey="admin.local.triggers.atevalt"
+                (onRowActivate)="editSelected([$event])">
+                <eg-grid-toolbar-button label="New Template" i18n-label
+                    [action]="createNewAltTemplate"></eg-grid-toolbar-button>
+                <eg-grid-toolbar-action label="Edit Template" i18n-label
+                    [action]="editSelected"></eg-grid-toolbar-action>
+                <eg-grid-toolbar-action label="Delete Selected" i18n-label
+                    (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
+            </eg-grid>
+            <eg-fm-record-editor #altTemplateDialog idlClass="atevalt"
+                 [fieldOptions]="{template:{customTemplate:{template:textAreaTemplate}},message_template:{customTemplate:{template:textAreaTemplate}}}"
+                 fieldOrder="active,locale,template,message_title,message_template"
+                 [preloadLinkedValues]="true"
+                 hiddenFields="event_def,id"></eg-fm-record-editor>
+        </ng-template>
     </li>
     <li ngbNavItem="'env'">
         <a ngbNavLink i18n>Edit Environment</a>
index 9b665b6..f41dc02 100644 (file)
@@ -21,22 +21,27 @@ export class EditEventDefinitionComponent implements OnInit {
 
     evtDefId: number;
     evtDefName: String;
+    evtReactor: string;
+    evtAltEligible: Boolean = false;
 
     testErr1: String = '';
     testErr2: String = '';
     testResult: String = '';
     testDone: Boolean = false;
 
+    altTemplateDataSource: GridDataSource = new GridDataSource();
     envDataSource: GridDataSource = new GridDataSource();
     paramDataSource: GridDataSource = new GridDataSource();
 
-    editTab: 'def' | 'env' | 'param' | 'test' = 'def';
+    editTab: 'def' | 'alt' | 'env' | 'param' | 'test' = 'def';
 
     @ViewChild('paramDialog') paramDialog: FmRecordEditorComponent;
     @ViewChild('envDialog') envDialog: FmRecordEditorComponent;
+    @ViewChild('altTemplateDialog') altTemplateDialog: FmRecordEditorComponent;
 
     @ViewChild('envGrid') envGrid: GridComponent;
     @ViewChild('paramGrid') paramGrid: GridComponent;
+    @ViewChild('altTemplateGrid') altTemplateGrid: GridComponent;
 
     @ViewChild('updateSuccessString') updateSuccessString: StringComponent;
     @ViewChild('updateFailedString') updateFailedString: StringComponent;
@@ -69,11 +74,24 @@ export class EditEventDefinitionComponent implements OnInit {
             this.evtDefName = rec.name();
         });
 
+        // get current event def reactor to decide if the alt template tab should show
+        this.pcrud.search('atevdef',
+            {id: this.evtDefId}, {}).toPromise().then(rec => {
+            this.evtReactor = rec.reactor();
+            if ('ProcessTemplate SendEmail SendSMS'.indexOf(this.evtReactor) > -1)
+                { this.evtAltEligible = true; }
+        });
+
         this.envDataSource.getRows = (pager: Pager, sort: any[]) => {
             return this.pcrud.search('atenv',
                 {event_def: this.evtDefId}, {});
         };
 
+        this.altTemplateDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.pcrud.search('atevalt',
+                {event_def: this.evtDefId}, {});
+        };
+
         this.paramDataSource.getRows = (pager: Pager, sort: any[]) => {
             return this.pcrud.search('atevparam',
                 {event_def: this.evtDefId}, {});
@@ -88,6 +106,10 @@ export class EditEventDefinitionComponent implements OnInit {
         this.createNewThing(this.envDialog, this.envGrid, 'atenv');
     }
 
+    createNewAltTemplate = () => {
+        this.createNewThing(this.altTemplateDialog, this.altTemplateGrid, 'atevalt');
+    }
+
     createNewParam = () => {
         this.createNewThing(this.paramDialog, this.paramGrid, 'atevparam');
     }
@@ -118,6 +140,8 @@ export class EditEventDefinitionComponent implements OnInit {
         let currentGrid;
         if (idlThings[0].classname === 'atenv') {
             currentGrid = this.envGrid;
+        } else if (idlThings[0].classname === 'atevalt') {
+            currentGrid = this.altTemplateGrid;
         } else {
             currentGrid = this.paramGrid;
         }
@@ -157,6 +181,9 @@ export class EditEventDefinitionComponent implements OnInit {
         if (selectedRecord.classname === 'atenv') {
             currentDialog = this.envDialog;
             currentGrid = this.envGrid;
+        } else if (selectedRecord.classname === 'atevalt') {
+            currentDialog = this.altTemplateDialog;
+            currentGrid = this.altTemplateGrid;
         } else {
             currentDialog = this.paramDialog;
             currentGrid = this.paramGrid;
index faf723b..c0d9851 100644 (file)
@@ -1654,6 +1654,20 @@ __PACKAGE__->register_method(
     }
 );
 
+__PACKAGE__->register_method(
+    method    => "update_passwd",
+    api_name  => "open-ils.actor.user.locale.update",
+    signature => {
+        desc   => "Update the operator's i18n locale",
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'New locale',           type => 'string' },
+            { desc => 'Current password',     type => 'string' }
+        ],
+        return => {desc => '1 on success, Event on error or incorrect current password'}
+    }
+);
+
 sub update_passwd {
     my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
     my $e = new_editor(xact=>1, authtoken=>$auth);
@@ -1696,6 +1710,10 @@ sub update_passwd {
         } elsif( $api =~ /email/o ) {
             $db_user->email($new_val);
             $at_event++;
+
+        } elsif( $api =~ /locale/o ) {
+            $db_user->locale($new_val);
+            $at_event++;
         }
     }
 
index 419bd7f..bd068e5 100644 (file)
@@ -492,6 +492,57 @@ sub build_environment {
         $self->environment->{usr_message}{title} = $self->event->event_def->message_title;
         $self->environment->{user_data} = $self->user_data;
 
+        my ($usr_locale, $alt_templates, $query, $query_result, $new_template_id);
+        my $reactor = $self->environment->{event}->event_def->reactor;
+        $query = {
+            select   => { atevalt => ['id', 'locale'] },
+            from     => 'atevalt',
+            where    => {
+                event_def => $self->environment->{event}->event_def->id,
+                active => 't'
+            }
+        };
+        my $e = new_editor(xact=>1);
+        if ($reactor) {
+            if (     $reactor eq 'SendEmail' 
+                  or $reactor eq 'ProcessTemplate' 
+                  or $reactor eq 'SendSMS') {
+                $query_result = $e->json_query($query);
+                $alt_templates = $query_result;
+                $query = {
+                    select => { au => ['locale'] },
+                    from   => 'au',
+                    where  => { id => $self->environment->{event}->target }
+                };
+                $query_result = $e->json_query($query);
+                $usr_locale = @$query_result[0]->{locale};
+                if ($alt_templates and @$alt_templates and $usr_locale) {
+                    foreach (@$alt_templates) {
+                        if ($_->{locale} eq $usr_locale) { 
+                            $new_template_id = $_->{id};
+                            $self->environment->{tt_locale} = $_->{locale};
+                            last;
+                        } else { #attempt a lanuage if not locale match
+                            if ((split /\p{Dash}/,$_->{locale})[0] eq (split /\p{Dash}/,$usr_locale)[0]) {
+                                $new_template_id = $_->{id};
+                                $self->environment->{tt_locale} = $_->{locale};
+                            }
+                        }
+                    }
+                }
+                if ($new_template_id) {
+                    $query = {
+                        select => { atevalt => ['template','message_template','message_title'] }, 
+                        from   => 'atevalt',
+                        where  => { id => $new_template_id }
+                    };
+                    $query_result = $e->json_query($query);
+                    $self->environment->{template} = @$query_result[0]->{template};
+                    $self->environment->{usr_message}{template} = @$query_result[0]->{message_template};
+                    $self->environment->{usr_message}{title} = @$query_result[0]->{message_title};
+                }
+            }
+        }
         $current_environment = $self->environment;
 
         $self->environment->{params}{ $_->param } = $compartment->reval($_->value) for ( @{$self->event->event_def->params} );
index cb7d5b8..74255e4 100644 (file)
@@ -553,6 +553,7 @@ sub run_TT {
         my $t_o = Fieldmapper::action_trigger::event_output->new;
         $t_o->data( ($error) ? $error : $output );
         $t_o->is_error( ($error) ? 't' : 'f' );
+        $t_o->locale($env->{tt_locale}); 
         $logger->info("trigger: writing " . length($t_o->data) . " bytes to template output");
 
         $env->{EventProcessor}->editor->xact_begin;
index 6d5432e..0f8557b 100644 (file)
@@ -268,6 +268,7 @@ sub load {
     return $self->load_myopac_update_email if $path =~ m|opac/myopac/update_email|;
     return $self->load_myopac_update_password if $path =~ m|opac/myopac/update_password|;
     return $self->load_myopac_update_username if $path =~ m|opac/myopac/update_username|;
+    return $self->load_myopac_update_locale if $path =~ m|opac/myopac/update_locale|;
     return $self->load_myopac_bookbags if $path =~ m|opac/myopac/lists|;
     return $self->load_myopac_bookbag_print if $path =~ m|opac/myopac/list/print|;
     return $self->load_myopac_bookbag_update if $path =~ m|opac/myopac/list/update|;
index 084aecc..dcc2f14 100644 (file)
@@ -37,7 +37,7 @@ sub prepare_extended_user_info {
         {
             flesh => 2,
             flesh_fields => {
-                au => [qw/card home_ou addresses ident_type billing_address waiver_entries/, @extra_flesh],
+                au => [qw/card home_ou addresses ident_type locale billing_address waiver_entries/, @extra_flesh],
                 "aou" => ["billing_address"]
             }
         }
@@ -2766,6 +2766,46 @@ sub load_myopac_update_username {
     return $self->generic_redirect($url);
 }
 
+sub load_myopac_update_locale {
+    my $self = shift;
+    my $e = $self->editor;
+    my $ctx = $self->ctx;
+    my $lang = $self->cgi->param('pref_lang') || '';
+    my $current_pw = $self->cgi->param('current_pw') || '';
+
+    $self->prepare_extended_user_info;
+
+    my $locs = $U->simplereq(
+        'open-ils.cstore',
+        "open-ils.cstore.direct.config.i18n_locale.search.atomic", 
+       { "code" => { "!=" => undef } }
+    );
+
+    my %user_locales;
+    foreach my $l (@$locs) { $user_locales{$l->code} = $l->name; }
+    $self->ctx->{i18n_locales} = \%user_locales; 
+
+    return Apache2::Const::OK
+        unless $self->cgi->request_method eq 'POST';
+
+    if($lang ne $e->requestor->locale) {
+        my $evt = $U->simplereq(
+            'open-ils.actor',
+            'open-ils.actor.user.locale.update',
+            $e->authtoken, $lang, $current_pw);
+
+        if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
+            $ctx->{password_incorrect} = 1;
+            return Apache2::Const::OK;
+        }
+    }
+
+    my $url = $self->apache->unparsed_uri;
+    $url =~ s/update_locale/prefs/;
+
+    return $self->generic_redirect($url);
+}
+
 sub load_myopac_update_password {
     my $self = shift;
     my $e = $self->editor;
index 9a42e05..b18a33c 100644 (file)
@@ -71,7 +71,8 @@ CREATE TABLE actor.usr (
        create_date             TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT now(),
        expire_date             TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT (now() + '3 years'::INTERVAL),
        claims_never_checked_out_count  INT         NOT NULL DEFAULT 0,
-    last_update_time    TIMESTAMP WITH TIME ZONE
+    last_update_time    TIMESTAMP WITH TIME ZONE,
+    locale                  TEXT REFERENCES config.i18n_locale(code) INITIALLY DEFERRED
 );
 COMMENT ON TABLE actor.usr IS $$
 User objects
index 3a0816a..05a9af9 100644 (file)
@@ -212,6 +212,17 @@ CREATE TABLE action_trigger.event_definition (
     CONSTRAINT ev_def_name_owner_once UNIQUE (owner, name)
 );
 
+CREATE TABLE action_trigger.alternate_template (
+    id               SERIAL,
+    event_def        INTEGER REFERENCES action_trigger.event_definition(id) INITIALLY DEFERRED,
+    template         TEXT,
+    active           BOOLEAN DEFAULT TRUE,
+    locale           TEXT REFERENCES config.i18n_locale(code) INITIALLY DEFERRED,
+    message_title    TEXT,
+    message_template TEXT,
+    UNIQUE (event_def,locale)
+);
+
 CREATE OR REPLACE FUNCTION action_trigger.check_valid_retention_interval() 
     RETURNS TRIGGER AS $_$
 BEGIN
@@ -263,7 +274,8 @@ CREATE TABLE action_trigger.event_output (
     id              BIGSERIAL   PRIMARY KEY,
     create_time     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
     is_error        BOOLEAN     NOT NULL DEFAULT FALSE,
-    data            TEXT        NOT NULL
+    data            TEXT        NOT NULL,
+    locale          TEXT
 );
 
 CREATE TABLE action_trigger.event (
diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.preferred_locale_and_alternate_at_templates.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.preferred_locale_and_alternate_at_templates.sql
new file mode 100644 (file)
index 0000000..d70fd2f
--- /dev/null
@@ -0,0 +1,20 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version);
+
+CREATE TABLE action_trigger.alternate_template (
+      id               SERIAL,
+      event_def        INTEGER REFERENCES action_trigger.event_definition(id) INITIALLY DEFERRED,
+      template         TEXT,
+      active           BOOLEAN DEFAULT TRUE,
+      message_title    TEXT,
+      message_template TEXT,
+      locale           TEXT REFERENCES config.i18n_locale(code) INITIALLY DEFERRED,
+      UNIQUE (event_def,locale)
+);
+
+ALTER TABLE actor.usr ADD COLUMN locale TEXT REFERENCES config.i18n_locale(code) INITIALLY DEFERRED;
+
+ALTER TABLE action_trigger.event_output ADD COLUMN locale TEXT;
+
+COMMIT;
index a40c8eb..49a989d 100755 (executable)
@@ -373,6 +373,10 @@ Novelist Styling
     max-width: 100px !important;
 }
 
+.mod-control{
+    max-width: 150px !important;
+}
+
 .card-body:empty{
     display:none;
 }
index bf9f116..74bb68c 100755 (executable)
             </tr>
 
             <tr>
+                <td class='color_4 light_border'>[% l("Preferred Language") %]</td>
+                <td class='light_border'>[% ctx.user.locale.name | html %]</td>
+                <td>
+                <span class='light_border'><a class="btn btn-sm btn-action" href='update_locale'
+                    title="[% l('Update Preferred Language') %]"><i class="fas fa-user-cog"></i> [% l('Change') %]</a></span>
+                </td>
+            </tr>
+
+            <tr>
                 <td class='color_4 light_border'>[% l("Home Library") %]</td>
  <td class='light_border'>
                     [% ctx.get_aou(ctx.user.home_ou.parent_ou).name %]<br/>
diff --git a/Open-ILS/src/templates-bootstrap/opac/myopac/update_locale.tt2 b/Open-ILS/src/templates-bootstrap/opac/myopac/update_locale.tt2
new file mode 100755 (executable)
index 0000000..b09f1a0
--- /dev/null
@@ -0,0 +1,38 @@
+[%  PROCESS "opac/parts/header.tt2";
+    PROCESS "opac/parts/misc_util.tt2";
+    WRAPPER "opac/parts/myopac/base.tt2";
+    myopac_page = "prefs"  %]
+<h3 class="sr-only">[% l('Update Preferred Language') %]</h3>
+<div id='myopac_summary_div' style="padding:0px;">
+
+    <div class="header_middle">
+        <span class="float-left">[% l('Update Preferred Language') %]</span>
+    </div>
+       
+[% IF ctx.password_incorrect %]
+    <div id='account-update-email-error'>
+        [% |l %]Your current password was not correct.[% END %]
+    </div>
+[% END %]
+
+<form method='post' id='account-update-email' autocomplete='off'> 
+    [% IF CGI.param("return_to_referer") %]
+    <input type="hidden" name="redirect_to" value="[% ctx.referer | html %]" />
+    [% END %]
+    <table> 
+        <tr><td>[% l('Current Preferred Language') %]</td><td>[% ctx.user.locale.name | html %]</td></tr>
+        <tr><td>[% l('Current Password') %]</td><td><input type='password' name='current_pw'/></td></tr>
+        <tr><td>[% l('New Preferred Language') %]</td>
+            <td class="px-3">
+                <select class="d-inline-block form-control mod-control" name="pref_lang" id="pref_lang">
+                    [% FOREACH i18n IN ctx.i18n_locales %]
+                        <option value='[% i18n.key | html %]'>[% l(i18n.value) %]
+                    [% END %]
+                </select>
+            </td>
+        </tr>
+    </table>
+    <button class="btn btn-confirm m-2" type='submit'><i class="fas fa-save"></i>Save Changes</button>
+</form>
+
+[% END %]
index e9a166b..ebafd08 100644 (file)
@@ -479,6 +479,20 @@ within the "form" by name for validation.
   </div>
 </div>
 
+<!-- LOCALE -->
+<div class="row reg-field-row" ng-show="show_field('au.locale')">
+  [% draw_field_label('au', 'locale') %]
+  <div class="col-md-3 reg-field-input">
+    <select
+      class="form-control"
+      aria-labelledby="{{idl_fields.au.locale.name}}"
+      ng-model="patron.locale"
+      ng-blur="handle_field_changed(patron, 'locale')"
+      ng-options="loc.name() for loc in locales track by loc.code()">
+    </select>
+  </div>
+</div>
+
 <!-- EMAIL -->
 <div class="row reg-field-row" ng-show="show_field('au.email')">
   [% draw_field_label('au', 'email') %]
index af98a66..af63520 100644 (file)
       <div class="col-md-7">{{patron().ident_value2()}}</div>
     </div>
     <div class="row">
+      <div class="col-md-5">[% l('Pref Language') %]&nbsp;<span ng-if="hasLocaleName" class="locale"></span></div>
+      <div class="col-md-7">{{patron().locale().name()}}</div>
+    </div>
+    <div class="row">
       <div class="col-md-5">[% l('Legal Name') %]</div>
       <div class="col-md-7">
         [% l('[_1] [_2], [_3] [_4] [_5]',
index 127d86c..37247ef 100644 (file)
@@ -60,6 +60,7 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
                 'net_access_level',
                 'ident_type',
                 'ident_type2',
+                'locale',
                 'cards',
                 'groups'
             ]);
@@ -311,9 +312,15 @@ function($scope,  $q , $location , $filter , egCore , egNet , egUser , egAlertDi
             .then(function() {return patronSvc.checkAlerts()})
             .then(redirectToAlertPanel)
             .then(function(){
-                $scope.ident_type_name = $scope.patron().ident_type().name()
+                if ($scope.patron().locale() !== null) { 
+                    $scope.locale_name = $scope.patron().locale().name();
+                    $scope.hasLocaleName = $scope.locale_name.length > 0;
+                }
+            })
+            .then(function(){
+                $scope.ident_type_name = $scope.patron().ident_type().name();
                 $scope.hasIdentTypeName = $scope.ident_type_name.length > 0;
-            });
+    });
         } else {
             // No patron, use the tab name as the page title.
             egCore.strings.setPageTitle(
index ffcb7a4..31bc4fe 100644 (file)
@@ -21,6 +21,7 @@ angular.module('egCoreMod')
         stat_cats : [],
         stat_cat_entry_maps : {},   // cat.id to selected value
         virt_id : -1,               // virtual ID for new objects
+        locales : [],
         init_done : false           // have we loaded our initialization data?
     };
 
@@ -42,6 +43,7 @@ angular.module('egCoreMod')
                 service.get_perm_groups(),
                 service.get_perm_group_entries(),
                 service.get_ident_types(),
+                service.get_locales(),
                 service.get_org_settings(),
                 service.get_stat_cats(),
                 service.get_surveys(),
@@ -49,7 +51,6 @@ angular.module('egCoreMod')
             ];
             service.init_done = true;
         }
-
         return $q.all(common_data.concat(page_data));
     };
 
@@ -470,6 +471,19 @@ angular.module('egCoreMod')
         }
     };
 
+    service.get_locales = function() {
+        if (egCore.env.i18n_l) {
+            service.locales = egCore.env.i18n_l.list;
+            return $q.when();
+        } else {
+            return egCore.pcrud.retrieveAll('i18n_l', {}, {atomic : true})
+            .then(function(locales) {
+                egCore.env.absorbList(locales, 'i18n_l')
+                service.locales = locales
+           });
+        }
+    };
+
     service.get_net_access_levels = function() {
         if (egCore.env.cnal) {
             service.net_access_levels = egCore.env.cnal.list;
@@ -771,7 +785,6 @@ angular.module('egCoreMod')
         service.existing_patron = current;
 
         var patron = egCore.idl.toHash(current);
-
         patron.home_ou = egCore.org.get(patron.home_ou.id);
         patron.expire_date = new Date(Date.parse(patron.expire_date));
         patron.dob = service.parse_dob(patron.dob);
@@ -779,6 +792,7 @@ angular.module('egCoreMod')
         patron.net_access_level = current.net_access_level();
         patron.ident_type = current.ident_type();
         patron.ident_type2 = current.ident_type2();
+        patron.locale = current.locale();
         patron.groups = current.groups(); // pre-hash
 
         angular.forEach(
@@ -907,6 +921,8 @@ angular.module('egCoreMod')
             user.ident_type = egCore.env.cit.map[user.ident_type];
         if (user.ident_type2)
             user.ident_type2 = egCore.env.cit.map[user.ident_type2];
+       if (user.locale) 
+           user.locale = egCore.env.i18n_l.map[user.locale];
         user.dob = service.parse_dob(user.dob);
 
         // Clear the usrname if it looks like a UUID
@@ -1095,6 +1111,8 @@ angular.module('egCoreMod')
             patron.dob(patron.dob().toISOString().replace(/T.*/,''));
         if (patron.ident_type()) 
             patron.ident_type(patron.ident_type().id());
+        if (patron.locale())
+            patron.locale(patron.locale().code());
         if (patron.net_access_level())
             patron.net_access_level(patron.net_access_level().id());
 
@@ -1393,6 +1411,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         $scope.edit_profiles = prs.edit_profiles;
         $scope.edit_profile_entries = prs.edit_profile_entries;
         $scope.ident_types = prs.ident_types;
+        $scope.locales = prs.locales;
         $scope.net_access_levels = prs.net_access_levels;
         $scope.user_setting_types = prs.user_setting_types;
         $scope.opt_in_setting_types = prs.opt_in_setting_types;
@@ -1531,6 +1550,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         'au.ident_type' : 3,
         'au.ident_type2' : 2,
         'au.photo_url' : 2,
+        'au.locale' : 2,
         'au.home_ou' : 3,
         'au.profile' : 3,
         'au.expire_date' : 3,
diff --git a/docs/RELEASE_NOTES_NEXT/Administration/localized_action_triggers.adoc b/docs/RELEASE_NOTES_NEXT/Administration/localized_action_triggers.adoc
new file mode 100644 (file)
index 0000000..0472e1f
--- /dev/null
@@ -0,0 +1,26 @@
+== Localized Templates Available for Action Triggers ==
+
+This feature supplies the ability to create alternate templates for Action Triggers 
+that will generate locale specific out for Action Triggers.  If you send notices in 
+multiple languages, we recommend putting some words to that effect in your notice 
+templates.  The template, message and message title can all be localized.  To use the 
+feature the following new UI elements have been added:
+
+- When you double-click on an Event Definition under Notifications / Action Triggers 
+  to edit it there will be a tab option for Edit Alternate Template if the reactor is 
+  ProcessTemplate, SendEmail or SendSMS.
+- In the Patron Registration and Patron Editor screens staff members may now select a 
+  locale for a patron and edit it in the Patron Preferred Language field.
+- Patrons may set their own locale in the My Account interface off the OPAC by going to 
+  Preferences -> Personal Information and setting the Preferred Language field.
+
+The templates used on the Edit Definition tab are the defaults that are used if there are 
+no alternate templates available that match the preferred language.  If alternate templates 
+are available the system will use a locale that is an exact match and then if failing that 
+use one where the language code matches and then fall back to the default one.
+
+For example, if a patron has a locale of fr-CA and there are templates for both fr-CA and 
+fr-FR it will use the fr-CA.  If the fr-CA template was deleted it would fall back on using 
+the fr-FR for the patron since it at least shares the same base language.  
+
+Valid locales are the codes defined in the i18n_locale table in the config schema.