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>
</LocationMatch>
</IfModule>
+<Location /print_template>
+ SetHandler perl-script
+ PerlHandler OpenILS::WWW::PrintTemplate
+ Options +ExecCGI
+ PerlSendHeader On
+ Require all granted
+</Location>
+
<Location /IDL2js>
</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="Print Templates">
+ <fields oils_persist:primary="id">
+ <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="Active" name="active" reporter:datatype="bool"/>
+ <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>
}
return null;
}
+
+ toHash(obj: any, flatten?: boolean): any {
+
+ if (typeof obj !== 'object' || obj === null) {
+ return obj;
+ }
+
+ if (Array.isArray(obj)) {
+ return obj.map(item => this.toHash(item));
+ }
+
+ const fieldNames = obj._isfieldmapper ?
+ Object.keys(this.classes[obj.classname].field_map) :
+ Object.keys(obj);
+
+ const hash: any = {};
+ fieldNames.forEach(field => {
+
+ var val = this.toHash(
+ typeof obj[field] === 'function' ? obj[field]() : obj[field],
+ flatten
+ );
+
+ if (val === undefined) { return; }
+
+ if (flatten && val !== null &&
+ typeof val === 'object' && !Array.isArray(val)) {
+
+ Object.keys(val).forEach(key => {
+ let fname = field + '.' + key;
+ hash[fname] = val[key];
+ });
+
+ } else {
+ hash[field] = val;
+ }
+ });
+
+ return hash;
+ }
}
<div class="modal-body">
<form #fmEditForm="ngForm" role="form" class="form-validated common-form striped-odd">
<div class="form-group row" *ngFor="let field of fields">
- <div class="col-lg-3 offset-lg-1">
+ <div class="col-lg-3">
<label for="{{idPrefix}}-{{field.name}}">{{field.label}}</label>
</div>
- <div class="col-lg-7">
+ <div class="col-lg-9">
<ng-container [ngSwitch]="inputType(field)">
context?: {[fields: string]: any};
}
-interface CustomFieldContext {
+export interface CustomFieldContext {
// Current create/edit/view record
record: IdlObject;
this.isPrinting = true;
- this.applyTemplate(printReq);
-
- // Give templates a chance to render before printing
- setTimeout(() => {
- this.dispatchPrint(printReq);
- this.reset();
+ this.applyTemplate(printReq).then(() => {
+ // Give templates a chance to render before printing
+ setTimeout(() => {
+ this.dispatchPrint(printReq);
+ this.reset();
+ });
});
}
- applyTemplate(printReq: PrintRequest) {
+ applyTemplate(printReq: PrintRequest): Promise<any> {
if (printReq.template) {
- // Inline template. Let Angular do the interpolationwork.
+ // Local Angular template.
this.template = printReq.template;
this.context = {$implicit: printReq.contextData};
- return;
+ return Promise.resolve();
+ }
+
+ let promise;
+
+ // Precompiled text
+ if (printReq.text) {
+ promise = Promise.resolve();
+
+ } else if (printReq.templateName || printReq.templateId) {
+
+ promise = this.printer.compileRemoteTemplate(printReq).then(
+ response => {
+ printReq.text = response.content;
+ printReq.contentType = response.contentType;
+ },
+ err => {
+ console.error("Error compiling template", printReq);
+ return Promise.reject(new Error(
+ 'Error compiling server-hosted print template'));
+ }
+ );
+
+ } else {
+ console.error("Cannot find template", printReq);
+ return Promise.reject(new Error("Cannot find print template"));
}
- if (printReq.text && true /* !this.hatch.isActive */) {
- // Insert HTML into the browser DOM for in-browser printing only.
+ return promise.then(() => {
- if (printReq.contentType === 'text/plain') {
- // Wrap text/plain content in pre's to prevent
- // unintended html formatting.
+ if (printReq.contentType === 'text/plain' && true /* this.hatch.isActive */) {
+ // When adding text output to DOM for rendering, wrap in
+ // pre to avoid unintended HTML formatting.
printReq.text = `<pre>${printReq.text}</pre>`;
}
this.htmlContainer.innerHTML = printReq.text;
- }
+ });
}
// Clear the print data
printViaHatch(printReq: PrintRequest) {
// Send a full HTML document to Hatch
- const html = `<html><body>${printReq.text}</body></html>`;
+ let html = printReq.text;
+ if (printReq.contentType === 'text/html') {
+ html = `<html><body>${printReq.text}</body></html>`;
+ }
/*
this.hatch.print({
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;
+ templateOwner?: number; // org unit ID, follows ancestors
+ templateId?: number; // useful for testing templates
contextData?: any;
text?: string;
printContext: string;
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>();
}
this.print(req);
}
}
+
+ compileRemoteTemplate(printReq: PrintRequest): Promise<PrintTemplateResponse> {
+
+ const formData: FormData = new FormData();
+
+ formData.append('ses', this.auth.token());
+ if (printReq.templateName) {
+ formData.append('template_name', printReq.templateName);
+ }
+ if (printReq.templateId) {
+ formData.append('template_id', '' + printReq.templateId);
+ }
+ if (printReq.templateOwner) {
+ formData.append('owner', '' + printReq.templateOwner);
+ }
+ 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);
+ });
+
+ }
}
--- /dev/null
+import {Injectable} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+
+/** Service for generating sample data for testing, demo, etc. */
+
+// TODO: I could also imagine this coming from a web service or
+// even a flat file of web-served JSON.
+
+// Copied from sample of Concerto data set
+const DATA = {
+ au: [
+ {first_given_name: 'Vincent', second_given_name: 'Kenneth', family_name: 'Moran'},
+ {first_given_name: 'Gregory', second_given_name: 'Adam', family_name: 'Jones'},
+ {first_given_name: 'Brittany', second_given_name: 'Geraldine', family_name: 'Walker'},
+ {first_given_name: 'Ernesto', second_given_name: 'Robert', family_name: 'Miller'},
+ {first_given_name: 'Robert', second_given_name: 'Louis', family_name: 'Hill'},
+ {first_given_name: 'Edward', second_given_name: 'Robert', family_name: 'Lopez'},
+ {first_given_name: 'Andrew', second_given_name: 'Alberto', family_name: 'Bell'},
+ {first_given_name: 'Jennifer', second_given_name: 'Dorothy', family_name: 'Mitchell'},
+ {first_given_name: 'Jo', second_given_name: 'Mai', family_name: 'Madden'},
+ {first_given_name: 'Maomi', second_given_name: 'Julie', family_name: 'Harding'}
+ ],
+ aua: [
+ {street1: '1809 Target Way', city: 'Vero beach', state: 'FL', post_code: 32961},
+ {street1: '3481 Facility Island', city: 'Campton', state: 'KY', post_code: 41301},
+ {street1: '5150 Dinner Expressway', city: 'Dodge center', state: 'MN', post_code: 55927},
+ {street1: '8496 Random Trust Points', city: 'Berryville', state: 'VA', post_code: 22611},
+ {street1: '7626 Secret Institute Courts', city: 'Anchorage', state: 'AK', post_code: 99502},
+ {street1: '7044 Regular Index Path', city: 'Livingston', state: 'KY', post_code: 40445},
+ {street1: '3403 Thundering Heat Meadows', city: 'Miami', state: 'FL', post_code: 33157},
+ {street1: '759 Doubtful Government Extension', city: 'Sellersville', state: 'PA', post_code: 18960},
+ {street1: '5431 Japanese Work Rapid', city: 'Society hill', state: 'SC', post_code: 29593},
+ {street1: '5253 Agricultural Exhibition Stravenue', city: 'La place', state: 'IL', post_code: 61936}
+ ]
+};
+
+
+@Injectable()
+export class SampleDataService {
+
+ constructor(private idl: IdlService) {}
+
+ randomValue(list: any[], field: string): string {
+ return list[Math.floor(Math.random() * list.length)][field];
+ }
+
+ listOfThings(idlClass: string, count: number = 1): IdlObject[] {
+ if (!(idlClass in DATA)) {
+ throw new Error(`No sample data for class ${idlClass}"`);
+ }
+
+ const things: IdlObject[] = [];
+ for (let i = 0; i < count; i++) {
+ const thing = this.idl.create(idlClass);
+ Object.keys(DATA[idlClass][0]).forEach(field =>
+ thing[field](this.randomValue(DATA[idlClass], field))
+ );
+ things.push(thing);
+ }
+
+ return things;
+ }
+}
+
+
url="/eg/staff/admin/server/legacy/permission/grp_tree"></eg-link-table-link>
<eg-link-table-link i18n-label label="Permissions"
routerLink="/staff/admin/server/permission/perm_list"></eg-link-table-link>
+ <!-- Probably should move this to local admin once it's migrated -->
+ <eg-link-table-link i18n-label label="Print Templates"
+ routerLink="/staff/admin/server/config/print_template"></eg-link-table-link>
<eg-link-table-link i18n-label label="Remote Accounts"
routerLink="/staff/admin/server/config/remote_account"></eg-link-table-link>
<eg-link-table-link i18n-label label="SMS Carriers"
import {AdminServerRoutingModule} from './routing.module';
import {AdminCommonModule} from '@eg/staff/admin/common.module';
import {AdminServerSplashComponent} from './admin-server-splash.component';
+import {PrintTemplateComponent} from './print-template.component';
+import {SampleDataService} from '@eg/share/util/sample-data.service';
@NgModule({
declarations: [
- AdminServerSplashComponent
+ AdminServerSplashComponent,
+ PrintTemplateComponent
],
imports: [
AdminCommonModule,
exports: [
],
providers: [
+ SampleDataService
]
})
--- /dev/null
+
+<eg-title i18n-prefix prefix="Print Template Administration"></eg-title>
+<eg-staff-banner bannerText="Print Template Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-fm-record-editor #editDialog idlClass="cpt"
+ [preloadLinkedValues]="true" hiddenFields="template">
+</eg-fm-record-editor>
+
+
+<div class="row mb-3">
+ <div class="col-lg-3">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text" i18n>Owner</span>
+ </div>
+ <eg-org-select
+ [limitPerms]="['ADMIN_PRINT_TEMPLATE']"
+ [initialOrg]="contextOrg"
+ (onChange)="orgOnChange($event)">
+ </eg-org-select>
+ </div>
+ </div>
+ <div class="col-lg-3">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text" i18n>Template</span>
+ </div>
+ <eg-combobox [entries]="entries" #templateSelector
+ (onChange)="selectTemplate($event ? $event.id : null)">
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="col-lg-3" *ngIf="localeEntries.length > 0">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text" i18n>Locale</span>
+ </div>
+ <eg-combobox [entries]="localeEntries"
+ [startId]="localeCode"
+ (onChange)="localeOnChange($event ? $event.id : null)">
+ </eg-combobox>
+ </div>
+ </div>
+</div>
+
+<ngb-tabset *ngIf="template" #tabs (tabChange)="onTabChange($event)">
+ <ngb-tab title="Template" i18n-title id='template'>
+ <ng-template ngbTabContent>
+ <div class="row">
+ <div class="col-lg-6 mt-3">
+ <button class="btn btn-success" (click)="openEditDialog()" i18n>
+ Edit Template Attributes
+ </button>
+ <button class="btn btn-info ml-2" (click)="applyChanges()" i18n>
+ Save Template and Refresh Preview
+ </button>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-6">
+ <h4 i18n>
+ Template for "{{template.label()}}"
+ <span class="pl-2 text-warning" *ngIf="template.active() == 'f'">
+ (Inactive)
+ </span>
+ </h4>
+ <textarea rows="30" class="form-control"
+ spellcheck="false"
+ [ngModel]="template.template()"
+ (ngModelChange)="template.template($event); template.ischanged(true)">
+ </textarea>
+ </div>
+ <div class="col-lg-6">
+ <h4 i18n>Preview</h4>
+ <div class="border border-dark w-100" id="template-preview-pane">
+ </div>
+ <h4 class="mt-3" i18n>Compiled Content</h4>
+ <div class="border border-dark w-100">
+ <pre class="p-1">{{compiledContent}}</pre>
+ </div>
+ </div>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Sample Data" i18n-title id='data'>
+ <ng-template ngbTabContent>
+ <textarea rows="20" [(ngModel)]="sampleJson"
+ spellcheck="false" class="form-control">
+ </textarea>
+ </ng-template>
+ </ngb-tab>
+</ngb-tabset>
+
--- /dev/null
+import {Component, OnInit, ViewChild, TemplateRef} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {ComboboxComponent, ComboboxEntry
+ } from '@eg/share/combobox/combobox.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {SampleDataService} from '@eg/share/util/sample-data.service';
+
+/**
+ * Print Template Admin Page
+ */
+
+@Component({
+ templateUrl: 'print-template.component.html'
+})
+
+export class PrintTemplateComponent implements OnInit {
+
+ contextOrg: IdlObject;
+ entries: ComboboxEntry[];
+ template: IdlObject;
+ sampleJson: string;
+ localeCode: string;
+ localeEntries: ComboboxEntry[];
+ compiledContent: string;
+
+ @ViewChild('templateSelector') templateSelector: ComboboxComponent;
+ @ViewChild('tabs') tabs: NgbTabset;
+ @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+
+ // Define some sample data that can be used for various templates
+ // Data will be filled out via the sample data service.
+ sampleData: any = {
+ patron_address: {
+ patron: null,
+ address: null
+ }
+ }
+
+ constructor(
+ private route: ActivatedRoute,
+ private idl: IdlService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private locale: LocaleService,
+ private printer: PrintService,
+ private samples: SampleDataService
+ ) {
+ this.entries = [];
+ this.localeEntries = [];
+ }
+
+ ngOnInit() {
+ this.contextOrg = this.org.get(this.auth.user().ws_ou());
+ this.localeCode = this.locale.currentLocaleCode();
+ this.locale.supportedLocales().subscribe(
+ l => this.localeEntries.push({id: l.code(), label: l.name()}));
+ this.setTemplateInfo();
+ this.fleshSampleData();
+ }
+
+ fleshSampleData() {
+
+ // NOTE: server templates work fine with IDL objects, but
+ // vanilla hashes are easier to work with in the admin UI.
+ const patrons = this.idl.toHash(this.samples.listOfThings('au', 10));
+ const addresses = this.idl.toHash(this.samples.listOfThings('aua', 10));
+
+ this.sampleData.patron_address = {
+ patron: patrons[0],
+ address: addresses[0]
+ };
+ }
+
+ onTabChange(evt: NgbTabChangeEvent) {
+ if (evt.nextId === 'template') {
+ this.refreshPreview();
+ }
+ }
+
+ container(): any {
+ // Only present when its tab is visible
+ return document.getElementById('template-preview-pane');
+ }
+
+ orgOnChange(org: IdlObject) {
+ this.contextOrg = org;
+ this.setTemplateInfo();
+ }
+
+ localeOnChange(code: string) {
+ if (code) {
+ this.localeCode = code;
+ this.setTemplateInfo();
+ }
+ }
+
+ // Fetch name/id for all templates in range.
+ // Avoid fetching the template content until needed.
+ setTemplateInfo() {
+ this.entries = [];
+ this.template = null;
+ this.templateSelector.applyEntryId(null);
+ this.compiledContent = '';
+
+ this.pcrud.search('cpt',
+ {
+ owner: this.contextOrg.id(),
+ locale: this.localeCode
+ },
+ {
+ select: {cpt: ['id', 'label']},
+ order_by: {cpt: 'label'}
+ }
+ ).subscribe(tmpl =>
+ this.entries.push({id: tmpl.id(), label: tmpl.label()})
+ );
+ }
+
+ selectTemplate(id: number) {
+ this.pcrud.retrieve('cpt', id).subscribe(t => {
+ this.template = t;
+ const data = this.sampleData[t.name()];
+ if (data) {
+ this.sampleJson = JSON.stringify(data, null, 2);
+ this.refreshPreview();
+ }
+ });
+ }
+
+ refreshPreview() {
+ if (!this.sampleJson) return;
+ this.compiledContent = '';
+
+ let data;
+ try {
+ data = JSON.parse(this.sampleJson);
+ } catch (E) {
+ // TODO: i18n/AlertDialog
+ alert('Invalid Sample Data JSON');
+ }
+
+ this.printer.compileRemoteTemplate({
+ templateId: this.template.id(),
+ contextData: data,
+ printContext: 'default' // required, has no impact here
+
+ }).then(response => {
+
+ this.compiledContent = response.content;
+ if (response.contentType === 'text/html') {
+ this.container().innerHTML = response.content;
+ } else {
+ // Assumes text/plain or similar
+ this.container().innerHTML = '<pre>' + response.content + '</pre>';
+ }
+ });
+ }
+
+ applyChanges() {
+ this.container().innerHTML = '';
+ this.pcrud.update(this.template).toPromise()
+ .then(() =>this.refreshPreview());
+ }
+
+ openEditDialog() {
+ // TODO: PENDING EXTERNAL FIXES
+ //this.editDialog.record = this.template;
+ this.editDialog.recordId = this.template.id();
+ this.editDialog.mode = 'update';
+ this.editDialog.open({size: 'lg'});
+ }
+}
+
+
import {RouterModule, Routes} from '@angular/router';
import {AdminServerSplashComponent} from './admin-server-splash.component';
import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+import {PrintTemplateComponent} from './print-template.component';
const routes: Routes = [{
path: 'splash',
component: AdminServerSplashComponent
}, {
+ path: 'config/print_template',
+ component: PrintTemplateComponent
+}, {
path: ':schema/:table',
component: BasicAdminPageComponent
}];
<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>
+
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {FormatService} from '@eg/core/format.service';
import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {SampleDataService} from '@eg/share/util/sample-data.service';
@Component({
templateUrl: 'sandbox.component.html'
private strings: StringService,
private toast: ToastService,
private format: FormatService,
- private printer: PrintService
+ private printer: PrintService,
+ private samples: SampleDataService
) {
}
.then(txt => this.toast.success(txt));
}, 4000);
}
-}
+ testServerPrint() {
+
+ // Note these values can be IDL objects or plain hashes.
+ const templateData = {
+ patron: this.samples.listOfThings('au')[0],
+ address: this.samples.listOfThings('aua')[0]
+ }
+
+ // NOTE: eventually this will be baked into the print service.
+ this.printer.print({
+ templateName: 'patron_address',
+ contextData: templateData,
+ printContext: 'default'
+ });
+ }
+}
import {StaffCommonModule} from '@eg/staff/common.module';
import {SandboxRoutingModule} from './routing.module';
import {SandboxComponent} from './sandbox.component';
+import {SampleDataService} from '@eg/share/util/sample-data.service';
@NgModule({
declarations: [
SandboxRoutingModule,
],
providers: [
+ SampleDataService
]
})
</eg-grid>
<eg-fm-record-editor #editDialog idlClass="{{idlClass}}"
+ [fieldOptions]="fieldOptions"
[preloadLinkedValues]="true" readonlyFields="{{readonlyFields}}">
</eg-fm-record-editor>
import {OrgService} from '@eg/core/org.service';
import {PermService} from '@eg/core/perm.service';
import {AuthService} from '@eg/core/auth.service';
-import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {FmRecordEditorComponent, FmFieldOptions
+ } from '@eg/share/fm-editor/fm-editor.component';
import {StringComponent} from '@eg/share/string/string.component';
/**
// Optional comma-separated list of read-only fields
@Input() readonlyFields: string;
+ // Override field options for create/edit dialog
+ @Input() fieldOptions: {[field: string]: FmFieldOptions};
+
@ViewChild('grid') grid: GridComponent;
@ViewChild('editDialog') editDialog: FmRecordEditorComponent;
@ViewChild('successString') successString: StringComponent;
--- /dev/null
+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 HTML::Restrict;
+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;
+}
+
+# Remove all but the following elements and attributes from text/html
+# compiled content.
+my $rules = {
+ b => [qw(class style)],
+ caption => [qw(class style)],
+ center => [qw(class style)],
+ div => [qw(class style)],
+ em => [qw(class style)],
+ i => [qw(class style)],
+ img => [qw(class style src)],
+ li => [qw(class style)],
+ ol => [qw(class style)],
+ p => [qw(class style)],
+ span => [qw(class style)],
+ strong => [qw(class style)],
+ style => [],
+ sub => [qw(class style)],
+ sup => [qw(class style)],
+ table => [qw(class style)],
+ tbody => [qw(class style)],
+ td => [qw(class style)],
+ th => [qw(class style)],
+ thead => [qw(class style)],
+ tr => [qw(class style)],
+ u => [qw(class style)],
+ ul => [qw(class style)],
+};
+my $hr = HTML::Restrict->new(rules => $rules);
+
+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 = $cgi->param('owner') || $e->requestor->ws_ou;
+ my $locale = $cgi->param('locale') || 'en-US';
+ my $template_id = $cgi->param('template_id');
+ my $template_name = $cgi->param('template_name');
+ my $template_data = $cgi->param('template_data');
+
+ return Apache2::Const::HTTP_BAD_REQUEST
+ unless $template_name || $template_id;
+
+ my $template = find_template($e, $template_id, $template_name, $locale, $owner)
+ or return Apache2::Const::NOT_FOUND;
+
+ 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 $output = '';
+ my $tt = Template->new;
+ my $tmpl = $template->template;
+
+ my $stat = $tt->process(\$tmpl, {template_data => $data}, \$output);
+
+ if ($stat) { # OK
+ my $ctype = $template->content_type;
+ if ($ctype eq 'text/html') {
+ # Scrub the HTML
+ $output = $hr->process($output);
+ }
+ # TODO
+ # client current expects content type to only contain type.
+ # $r->content_type("$ctype; encoding=utf8");
+ $r->content_type($ctype);
+ $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, $template_id, $name, $locale, $owner) = @_;
+
+ if ($template_id) {
+ # Requesting by ID, generally used for testing,
+ # always pulls the latest value and ignores the active flag
+ return $e->retrieve_config_print_template($template_id);
+ }
+
+ 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,
+ active => 't'
+ })->[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;
--- /dev/null
+
+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),
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ 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, 'patron_address', 'en-US',
+ (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
+ oils_i18n_gettext(1, 'Address Label', '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;
+
+++ /dev/null
-<!--
-Template for printing a patron address. Fields include:
-
-* first_given_name
-* second_given_name
-* family_name
-* address.street1
-* address.street2
-* address.city
-* address.state
-* address.post_code
-
--->
-<div>
- <div>
- {{patron.first_given_name}}
- {{patron.second_given_name}}
- {{patron.family_name}}
- </div>
- <div>{{address.street1}}</div>
- <div ng-if="address.street2">{{address.street2}}</div>
- <div>
- {{address.city}}, {{address.state}} {{address.post_code}}
- </div>
-</div>
$scope.template_changed = function() {
$scope.print.load_failed = false;
- egCore.print.getPrintTemplate($scope.print.template_name)
+ egCore.print.getPrintTemplate({template: $scope.print.template_name})
.then(
function(html) {
$scope.print.template_content = html;
$scope.print_address = function(addr) {
egCore.print.print({
context : 'default',
- template : 'patron_address',
+ template: 'patron_address',
scope : {
patron : egCore.idl.toHash(patronSvc.current),
address : egCore.idl.toHash(addr)
service.template_base_path = 'share/print_templates/t_';
+ service.server_template_path = '/print_template';
/*
* context : 'default', 'receipt','label', etc.
* show_dialog : boolean, if true, print dialog is shown. This setting
* only affects remote printers, since browser printers
* do not allow such control
+ * server_hosted: if true, have the server compile the template for us.
*/
service.print = function(args) {
if (!args) return $q.when();
if (args.template) {
// fetch the template, then proceed to printing
- return service.getPrintTemplate(args.template)
+ return service.getPrintTemplate(args)
.then(function(content) {
args.content = content;
if (!args.content_type) args.content_type = 'text/html';
return service.print_content(args);
});
});
-
}
return service.print_content(args);
}
+ service.compileRemoteTemplate = function(templateName, templateData) {
+
+ var formData = new FormData();
+
+ formData.append('ses', egAuth.token());
+ formData.append('template_name', templateName);
+ formData.append('template_data', js2JSON(templateData));
+ formData.append('locale', OpenSRF.locale);
+
+ return new Promise((resolve, reject) => {
+ var 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', service.server_template_path, true);
+ xhttp.send(formData);
+ });
+ }
+
// add commonly used attributes to the print scope
service.fleshPrintScope = function(scope) {
if (!scope) scope = {};
}).then(function(content) {
// Ingest the content into the page DOM.
- return service.ingest_print_content(type, content, printScope);
+ if (args.server_hosted) {
+ // Server hosted print templates arrive fully formed.
+ return service.ingest_print_content(type, null, null, content);
+ } else {
+ return service.ingest_print_content(type, content, printScope);
+ }
}).then(function(html) {
});
}
- // loads an HTML print template by name from the server
- // If no template is available in local/hatch storage,
- // fetch the template as an HTML file from the server.
- service.getPrintTemplate = function(name) {
+ // loads an HTML print template by name from the server If no
+ // template is available in local/hatch storage, fetch the template
+ // as an HTML file from the server. if no HTML file is available,
+ // try loading the content from a server-hosted template.
+ service.getPrintTemplate = function(args) {
var deferred = $q.defer();
+ var name = args.template;
egHatch.getItem('eg.print.template.' + name)
.then(function(html) {
if (html) {
// we have a locally stored template
+ console.debug('Found saved template for ' + name);
deferred.resolve(html);
return;
}
console.debug('fetching template ' + path);
$http.get(path).then(
- function(data) { deferred.resolve(data.data) },
+ function(data) {
+
+ if (data.data.match(/Print Template Not Found/)) {
+
+ // AngJS templates return a dummy template w/ the
+ // above text if the template is not found instead
+ // of a 404.
+ return service.compileRemoteTemplate(name, args.scope)
+ .then(
+ function(response) {
+ console.debug('Found server-hosted template for ' + name);
+ args.content_type = response.contentType;
+ args.content = response.content;
+ deferred.resolve(args.content);
+ },
+ function() {
+ console.error('unable to locate print template: ' + name);
+ deferred.reject();
+ }
+ );
+ }
+
+ console.debug('Found server template file for ' + name);
+ deferred.resolve(data.data)
+ },
function() {
console.error('unable to locate print template: ' + name);
deferred.reject();