server-driven print templates POC / WIP
authorBill Erickson <berickxx@gmail.com>
Mon, 15 Apr 2019 22:11:46 +0000 (18:11 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 19 Apr 2019 17:31:22 +0000 (13:31 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/examples/apache_24/eg_startup.in
Open-ILS/examples/apache_24/eg_vhost.conf.in
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/share/print/print.service.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/PrintTemplate.pm [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.server-print-templates.sql [new file with mode: 0644]

index 855159e..316034a 100755 (executable)
@@ -15,6 +15,9 @@ use OpenILS::WWW::IDL2js ('@sysconfdir@/opensrf_core.xml');
 use OpenILS::WWW::FlatFielder;
 use OpenILS::WWW::PhoneList ('@sysconfdir@/opensrf_core.xml');
 
+# Pass second argument of '1' to disable template caching.                     
+use OpenILS::WWW::PrintTemplate ('/openils/conf/opensrf_core.xml', 0);  
+
 # - Uncomment the following 2 lines to make use of the IP redirection code
 # - The IP file should to contain a map with the following format:
 # - actor.org_unit.shortname <start_ip> <end_ip>
index 95d0702..0d32754 100644 (file)
@@ -773,6 +773,14 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT]
     </LocationMatch>
 </IfModule>
 
+<Location /print_template>
+    SetHandler perl-script
+    PerlHandler OpenILS::WWW::PrintTemplate
+    Options +ExecCGI
+    PerlSendHeader On
+    Require all granted 
+</Location>
+
 
 <Location /IDL2js>
 
index 1f51073..6a83486 100644 (file)
@@ -12813,6 +12813,34 @@ SELECT  usr,
                </links>
        </class>
 
+       <class id="cpt" controller="open-ils.cstore open-ils.pcrud"
+               oils_obj:fieldmapper="config::print_template" 
+               oils_persist:tablename="config.print_template" 
+               reporter:label="Workstation Setting Type">
+               <fields oils_persist:primary="name">
+                       <field name="id" reporter:datatype="id"  reporter:selector="label"/>
+                       <field name="name" reporter:datatype="text" oils_obj:required="true"/>
+                       <field name="label" reporter:datatype="text" oils_obj:required="true" oils_persist:i18n="true"/>
+                       <field reporter:label="Owner" name="owner" oils_obj:required="true" reporter:datatype="link"/>
+                       <field reporter:label="Locale" name="locale" reporter:datatype="link"/>
+                       <field name="content_type" reporter:datatype="text"/>
+                       <field name="template" reporter:datatype="text" oils_obj:required="true"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="locale" reltype="has_a" key="id" map="" class="i18n_l"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_PRINT_TEMPLATE" context_field="owner"/>
+                               <retrieve permission="STAFF_LOGIN" context_field="owner"/>
+                               <update permission="ADMIN_PRINT_TEMPLATE" context_field="owner"/>
+                               <delete permission="ADMIN_PRINT_TEMPLATE" context_field="owner"/>
+                       </actions>
+               </permacrud>
+       </class>
+
+
        <!-- ********************************************************************************************************************* -->
 </IDL>
 
index 5ae6844..54dcd9c 100644 (file)
@@ -1,8 +1,16 @@
 import {Injectable, EventEmitter, TemplateRef} from '@angular/core';
+import {tap} from 'rxjs/operators';
 import {StoreService} from '@eg/core/store.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {AuthService} from '@eg/core/auth.service';
+
+declare var js2JSON: (jsThing: any) => string;
+
+const PRINT_TEMPLATE_PATH = '/print_template';
 
 export interface PrintRequest {
     template?: TemplateRef<any>;
+    templateName?: string;
     contextData?: any;
     text?: string;
     printContext: string;
@@ -10,12 +18,21 @@ export interface PrintRequest {
     showDialog?: boolean;
 }
 
+export interface PrintTemplateResponse {
+    contentType: string;
+    content: string;
+}
+
 @Injectable()
 export class PrintService {
 
     onPrintRequest$: EventEmitter<PrintRequest>;
 
-    constructor(private store: StoreService) {
+    constructor(
+        private locale: LocaleService,
+        private auth: AuthService,
+        private store: StoreService
+    ) {
         this.onPrintRequest$ = new EventEmitter<PrintRequest>();
     }
 
@@ -37,5 +54,34 @@ export class PrintService {
             this.print(req);
         }
     }
+
+    compileRemoteTemplate(printReq: PrintRequest): Promise<PrintTemplateResponse> {
+
+        const formData: FormData = new FormData();
+
+        formData.append('ses', this.auth.token());
+        formData.append('template_name', printReq.templateName);
+        formData.append('template_data', js2JSON(printReq.contextData));
+        formData.append('locale', this.locale.currentLocaleCode());
+
+        return new Promise((resolve, reject) => {
+            const xhttp = new XMLHttpRequest();
+            xhttp.onreadystatechange = function() {
+                if (this.readyState === 4) {
+                    if (this.status === 200) {
+                        resolve({
+                            content: xhttp.responseText,
+                            contentType: this.getResponseHeader('content-type')
+                        });
+                    } else {
+                        reject('Error compiling print template');
+                    }
+                }
+            };
+            xhttp.open('POST', PRINT_TEMPLATE_PATH, true);
+            xhttp.send(formData);
+        });
+
+    }
 }
 
index 84e127e..e78b6cb 100644 (file)
 <h4>PCRUD auto flesh and FormatService detection</h4>
 <div *ngIf="aMetarecord">Fingerprint: {{aMetarecord}}</div>
 
+<h4>Test Server Print Template</h4>
+<button class="btn btn-info" (click)="testServerPrint()">GO</button>
+
index 9b058cd..8eb7f92 100644 (file)
@@ -223,6 +223,44 @@ export class SandboxComponent implements OnInit {
                 .then(txt => this.toast.success(txt));
         }, 4000);
     }
+
+    testServerPrint() {
+
+        // Note these values can be IDL objects or plain hashes.
+        const patron = this.idl.create('au');
+        const address = this.idl.create('aua');
+        patron.first_given_name('Crosby');
+        patron.second_given_name('Stills');
+        patron.family_name('Nash');
+        address.street1('123 Pineapple Road');
+        address.street2('Apt #4');
+        address.city('Bahama');
+        address.state('NC');
+        address.post_code('555444');
+
+        const templateData = {
+            patron: patron,
+            address: address
+        }
+
+        // NOTE: eventually this will be baked into the print service.
+        this.printer.compileRemoteTemplate({
+            templateName: 'address-label',
+            contextData: templateData,
+            printContext: 'default'
+        }).then(
+            response => {
+                console.log(response.contentType);
+                console.log(response.content);
+                this.printer.print({
+                    printContext: 'default',
+                    contentType: response.contentType,
+                    text: response.content,
+                    showDialog: true
+                });
+            }
+        );
+    }
 }
 
 
index 219f611..d9174a8 100644 (file)
@@ -32,6 +32,7 @@ use OpenILS::Application::Actor::UserGroups;
 use OpenILS::Application::Actor::Friends;
 use OpenILS::Application::Actor::Stage;
 use OpenILS::Application::Actor::Settings;
+use OpenILS::Application::Actor::PrintTemplate;
 
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Penalty;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/PrintTemplate.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/PrintTemplate.pm
new file mode 100644 (file)
index 0000000..0be2c69
--- /dev/null
@@ -0,0 +1,123 @@
+package OpenILS::WWW::PrintTemplate;
+use strict; use warnings;
+use Apache2::Const -compile => 
+    qw(OK FORBIDDEN NOT_FOUND HTTP_INTERNAL_SERVER_ERROR HTTP_BAD_REQUEST);
+use Apache2::RequestRec;
+use CGI;
+use OpenSRF::Utils::JSON;
+use OpenSRF::System;
+use OpenSRF::Utils::SettingsClient;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+my $bs_config;
+my $disable_cache;
+sub import {
+    $bs_config = shift;
+    $disable_cache = shift;
+}
+
+my $init_complete = 0;
+sub child_init {
+    $init_complete = 1;
+
+    OpenSRF::System->bootstrap_client(config_file => $bs_config);
+    OpenILS::Utils::CStoreEditor->init; # just in case
+    return Apache2::Const::OK;
+}
+
+
+sub handler {
+    my $r = shift;
+    my $cgi = CGI->new;
+
+    child_init() unless $init_complete;
+
+    my $auth = $cgi->param('ses') || 
+        $cgi->cookie('eg.auth.token') || $cgi->cookie('ses');
+
+    my $e = new_editor(authtoken => $auth);
+
+    # Requires staff login
+    return Apache2::Const::FORBIDDEN 
+        unless $e->checkauth && $e->requestor->wsid;
+
+    # Let pcrud handle the authz
+    $e->personality('open-ils.pcrud');
+
+    my $owner = $e->requestor->ws_ou;
+    my $locale = $cgi->param('locale') || 'en-US';
+    my $template_name = $cgi->param('template_name');
+    my $template_data = $cgi->param('template_data');
+
+    return Apache2::Const::FORBIDDEN unless $template_name;
+
+    my $template = find_template($e, $template_name, $locale, $owner)
+        or return Apache2::Const::NOT_FOUND;
+
+    my $output = '';
+    my $tt = Template->new;
+    my $tmpl = $template->template;
+    my $data;
+
+    eval { $data = OpenSRF::Utils::JSON->JSON2perl($template_data); };
+    if ($@) {
+        $logger->error("Invalid JSON in template compilation: $template_data");
+        return Apache2::Const::HTTP_BAD_REQUEST;
+    }
+
+    my $stat = $tt->process(\$tmpl, {template_data => $data}, \$output);
+
+    if ($stat) { # OK
+
+        $r->content_type($template->content_type . '; encoding=utf8');
+        $r->print($output);
+        return Apache2::Const::OK;
+
+    } else {
+
+        (my $error = $tt->error) =~ s/\n/ /og;
+        $logger->error("Error processing Trigger template: $error");
+        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+    }
+}
+
+# Find the template closest to the specific org unit owner.
+my %template_cache;
+sub find_template {
+    my ($e, $name, $locale, $owner) = @_;
+
+    return  $template_cache{$owner}{$name}{$locale}
+        if  $template_cache{$owner} && 
+            $template_cache{$owner}{$name} &&
+            $template_cache{$owner}{$name}{$locale};
+
+    while ($owner) {
+        my ($org) = $U->fetch_org_unit($owner); # internally cached
+        
+        my $template = $e->search_config_print_template({
+            name => $name, 
+            locale => $locale, 
+            owner => $org->id
+        })->[0];
+
+        if ($template) {
+            if (!$disable_cache) {
+                $template_cache{$owner} = {} unless $template_cache{$owner};
+                $template_cache{$owner}{$name} = {} 
+                    unless $template_cache{$owner}{$name};
+                $template_cache{$owner}{$name}{$locale} = $template;
+            }
+
+            return $template;
+        }
+
+        $owner = $org->parent_ou;
+    }
+
+    return undef;
+}
+
+1;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.server-print-templates.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.server-print-templates.sql
new file mode 100644 (file)
index 0000000..e02e09c
--- /dev/null
@@ -0,0 +1,62 @@
+
+BEGIN;
+
+/*
+No longer have to deliver print templates to the client or store as workstatoin settings
+-- trade-off is print data is now sent to the server
+Can define per-locale templates.
+immune to future technology changes
+Angular template modification requires recompiling the Angular build.
+https://metacpan.org/pod/HTML::Restrict
+security concerns of staff modifing templates directly since
+they execute in the borwser context.
+offline caveat
+*/
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+CREATE TABLE config.print_template (
+    id           SERIAL PRIMARY KEY,
+    name         TEXT NOT NULL, -- programatic name
+    label        TEXT NOT NULL, -- i18n
+    owner        INT NOT NULL REFERENCES actor.org_unit (id),
+    locale       TEXT REFERENCES config.i18n_locale(code) 
+                 ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    content_type TEXT NOT NULL DEFAULT 'text/html',
+    template     TEXT NOT NULL,
+       CONSTRAINT   name_once_per_lib UNIQUE (owner, name),
+       CONSTRAINT   label_once_per_lib UNIQUE (owner, label)
+);
+
+INSERT INTO config.print_template (id, name, locale, owner, label, template) 
+VALUES (
+    1, 'address-label', 'en-US',
+    (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
+    oils_i18n_gettext(1, 'Test Template', 'cpt', 'label'),
+$TEMPLATE$
+[%-
+    SET patron = template_data.patron;
+    SET addr = template_data.address;
+-%]
+<div style="font-size:.7em;">
+  <div>
+    [% patron.first_given_name %] 
+    [% patron.second_given_name %] 
+    [% patron.family_name %]
+  </div>
+  <div>[% addr.street1 %]</div>
+  [% IF addr.street2 %]<div>[% addr.street2 %]</div>[% END %]
+  <div>
+    [% addr.city %], [% addr.state %] [% addr.post_code %]
+  </div>
+</div>
+$TEMPLATE$
+);
+
+-- TODO: add print template permission
+
+-- Allow for 1k stock templates
+SELECT SETVAL('config.print_template_id_seq'::TEXT, 1000);
+
+COMMIT;
+