From 0f99fc4f3fdfd10ee364dc10cbc4bb4bc009091f Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 15 Apr 2019 18:11:46 -0400 Subject: [PATCH] server-driven print templates POC / WIP Signed-off-by: Bill Erickson --- Open-ILS/examples/apache_24/eg_startup.in | 3 + Open-ILS/examples/apache_24/eg_vhost.conf.in | 8 ++ Open-ILS/examples/fm_IDL.xml | 28 +++++ .../src/eg2/src/app/share/print/print.service.ts | 48 +++++++- .../src/app/staff/sandbox/sandbox.component.html | 3 + .../eg2/src/app/staff/sandbox/sandbox.component.ts | 38 +++++++ .../src/perlmods/lib/OpenILS/Application/Actor.pm | 1 + .../src/perlmods/lib/OpenILS/WWW/PrintTemplate.pm | 123 +++++++++++++++++++++ .../upgrade/XXXX.schema.server-print-templates.sql | 62 +++++++++++ 9 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/PrintTemplate.pm create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.server-print-templates.sql diff --git a/Open-ILS/examples/apache_24/eg_startup.in b/Open-ILS/examples/apache_24/eg_startup.in index 855159e16f..316034a424 100755 --- a/Open-ILS/examples/apache_24/eg_startup.in +++ b/Open-ILS/examples/apache_24/eg_startup.in @@ -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 diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in index 95d0702b6d..0d32754c9c 100644 --- a/Open-ILS/examples/apache_24/eg_vhost.conf.in +++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in @@ -773,6 +773,14 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT] + + SetHandler perl-script + PerlHandler OpenILS::WWW::PrintTemplate + Options +ExecCGI + PerlSendHeader On + Require all granted + + diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 1f510735fb..6a8348612a 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -12813,6 +12813,34 @@ SELECT usr, + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/share/print/print.service.ts b/Open-ILS/src/eg2/src/app/share/print/print.service.ts index 5ae6844dfd..54dcd9c3a4 100644 --- a/Open-ILS/src/eg2/src/app/share/print/print.service.ts +++ b/Open-ILS/src/eg2/src/app/share/print/print.service.ts @@ -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; + 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; - constructor(private store: StoreService) { + constructor( + private locale: LocaleService, + private auth: AuthService, + private store: StoreService + ) { this.onPrintRequest$ = new EventEmitter(); } @@ -37,5 +54,34 @@ export class PrintService { this.print(req); } } + + compileRemoteTemplate(printReq: PrintRequest): Promise { + + 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); + }); + + } } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index 84e127e3c0..e78b6cb237 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -152,3 +152,6 @@

PCRUD auto flesh and FormatService detection

Fingerprint: {{aMetarecord}}
+

Test Server Print Template

+ + diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index 9b058cd5b1..8eb7f92989 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -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 + }); + } + ); + } } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm index 219f611b6b..d9174a86c9 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm @@ -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 index 0000000000..0be2c69782 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/PrintTemplate.pm @@ -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 index 0000000000..e02e09cadc --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.server-print-templates.sql @@ -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; +-%] +
+
+ [% patron.first_given_name %] + [% patron.second_given_name %] + [% patron.family_name %] +
+
[% addr.street1 %]
+ [% IF addr.street2 %]
[% addr.street2 %]
[% END %] +
+ [% addr.city %], [% addr.state %] [% addr.post_code %] +
+
+$TEMPLATE$ +); + +-- TODO: add print template permission + +-- Allow for 1k stock templates +SELECT SETVAL('config.print_template_id_seq'::TEXT, 1000); + +COMMIT; + -- 2.11.0