84c7f354dd9d4eee9c278c64b917f5a4bc82205f
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / marcbatch / marcbatch.component.ts
1 import {Component, OnInit, ViewChild, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {HttpClient} from '@angular/common/http';
4 import {tap} from 'rxjs/operators';
5 import {NetService} from '@eg/core/net.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
9 import {MarcRecord, MarcField} from '@eg/staff/share/marc-edit/marcrecord';
10 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
11 import {ServerStoreService} from '@eg/core/server-store.service';
12
13 const SESSION_POLL_INTERVAL = 2; // seconds
14 const MERGE_TEMPLATE_PATH = '/opac/extras/merge_template';
15
16 interface TemplateRule {
17     ruleType: 'r' | 'a' | 'd';
18     marcTag?: string;
19     marcSubfields?: string;
20     marcData?: string;
21     advSubfield?: string;
22     advRegex?: string;
23 }
24
25 @Component({
26   templateUrl: 'marcbatch.component.html'
27 })
28 export class MarcBatchComponent implements OnInit {
29
30     session: string;
31     source: 'b' | 'c' | 'r' = 'b';
32     buckets: ComboboxEntry[];
33     bucket: number;
34     recordId: number;
35     csvColumn = 0;
36     csvFile: File;
37     templateRules: TemplateRule[] = [];
38     record: MarcRecord;
39
40     processing = false;
41     progressMax: number = null;
42     progressValue: number = null;
43     numSucceeded = 0;
44     numFailed = 0;
45
46     constructor(
47         private router: Router,
48         private route: ActivatedRoute,
49         private http: HttpClient,
50         private renderer: Renderer2,
51         private net: NetService,
52         private pcrud: PcrudService,
53         private auth: AuthService,
54         private store: ServerStoreService,
55         private cache: AnonCacheService
56     ) {}
57
58     ngOnInit() {
59         this.load();
60     }
61
62     load() {
63         this.addRule();
64         this.getBuckets();
65     }
66
67     rulesetToRecord(resetRuleData?: boolean) {
68         this.record = new MarcRecord();
69
70         this.templateRules.forEach(rule => {
71
72             if (!rule.marcTag) { return; }
73
74             let ruleText = rule.marcTag + (rule.marcSubfields || '');
75             if (rule.advSubfield) {
76                 ruleText +=
77                     `[${rule.advSubfield || ''} ~ ${rule.advRegex || ''}]`;
78             }
79
80             // Merge behavior is encoded in the 905 field.
81             const ruleTag = this.record.newField({
82                 tag: '905',
83                 ind1: ' ',
84                 ind2: ' ',
85                 subfields: [[rule.ruleType, ruleText, 0]]
86             });
87
88             this.record.insertOrderedFields(ruleTag);
89
90             if (rule.ruleType === 'd') {
91                 rule.marcData = '';
92                 return;
93             }
94
95             const dataRec = new MarcRecord();
96             if (resetRuleData || !rule.marcData) {
97
98                 // Build a new value for the 'MARC Data' field based on
99                 // changes to the selected tag or subfields.
100
101                 const subfields = rule.marcSubfields ?
102                     rule.marcSubfields.split('').map((sf, idx) => [sf, '', idx])
103                     : [];
104
105                 dataRec.appendFields(
106                     dataRec.newField({
107                         tag: rule.marcTag,
108                         ind1: ' ',
109                         ind2: ' ',
110                         subfields: subfields
111                     })
112                 );
113
114                 console.log(dataRec.toBreaker());
115                 rule.marcData = dataRec.toBreaker().split(/\n/)[1];
116
117             } else {
118
119                 // Absorb the breaker data already in the 'MARC Data' field
120                 // so it can be added to the template record in progress.
121
122                 dataRec.breakerText = rule.marcData;
123                 dataRec.absorbBreakerChanges();
124             }
125
126             this.record.appendFields(dataRec.fields[0]);
127         });
128     }
129
130     breakerRows(): number {
131         if (this.record) {
132             const breaker = this.record.toBreaker();
133             if (breaker) {
134                 return breaker.split(/\n/).length + 1;
135             }
136         }
137         return 3;
138     }
139
140     breaker(): string {
141         return this.record ? this.record.toBreaker() : '';
142     }
143
144     addRule() {
145         this.templateRules.push({ruleType: 'r'});
146     }
147
148     removeRule(idx: number) {
149         this.templateRules.splice(idx, 1);
150     }
151
152     getBuckets(): Promise<any> {
153         if (this.buckets) { return Promise.resolve(); }
154
155         return this.net.request(
156             'open-ils.actor',
157             'open-ils.actor.container.retrieve_by_class',
158             this.auth.token(), this.auth.user().id(),
159             'biblio', ['staff_client', 'vandelay_queue']
160
161         ).pipe(tap(buckets => {
162             this.buckets = buckets
163             .sort((b1, b2) => b1.name() < b2.name() ? -1 : 1)
164             .map(b => ({id: b.id(), label: b.name()}));
165
166         })).toPromise();
167     }
168
169     bucketChanged(entry: ComboboxEntry) {
170         this.bucket = entry ? entry.id : null;
171     }
172
173     fileSelected($event) {
174        this.csvFile = $event.target.files[0];
175     }
176
177     disableSave(): boolean {
178         if (!this.record || !this.source || this.processing) {
179             return true;
180         }
181
182         if (this.source === 'b') {
183             return !this.bucket;
184
185         } else if (this.source === 'c') {
186             return (!this.csvColumn || !this.csvFile);
187
188         } else if (this.source === 'r') {
189             return !this.recordId;
190         }
191     }
192
193     process() {
194         this.processing = true;
195         this.progressValue = null;
196         this.progressMax = null;
197         this.numSucceeded = 0;
198         this.numFailed = 0;
199         this.setReplaceMode();
200         this.postForm().then(_ => this.pollProgress());
201     }
202
203     setReplaceMode() {
204         if (this.record.subfield('905', 'r').length === 0) {
205             // Force replace mode w/ no-op replace rule.
206             this.record.appendFields(
207                 this.record.newField({
208                     tag : '905',
209                     ind1 : ' ',
210                     ind2 : ' ',
211                     subfields : [['r', '901c']]
212                 })
213             );
214         }
215     }
216
217     postForm(): Promise<any> {
218
219         const formData: FormData = new FormData();
220         formData.append('ses', this.auth.token());
221         formData.append('skipui', '1');
222         formData.append('template', this.record.toXml());
223         formData.append('recordSource', this.source);
224         formData.append('xactPerRecord', '1');
225
226         if (this.source === 'b') {
227             formData.append('containerid', this.bucket + '');
228
229         } else if (this.source === 'c') {
230             formData.append('idcolumn', this.csvColumn + '');
231             formData.append('idfile', this.csvFile, this.csvFile.name);
232
233         } else if (this.source === 'r') {
234             formData.append('recid', this.recordId + '');
235         }
236
237         return this.http.post(
238             MERGE_TEMPLATE_PATH, formData, {responseType: 'text'})
239         .pipe(tap(cacheKey => this.session = cacheKey))
240         .toPromise();
241     }
242
243     pollProgress(): Promise<any> {
244         console.debug('Polling session ', this.session);
245
246         return this.cache.getItem(this.session, 'batch_edit_progress')
247         .then(progress => {
248             // {"success":"t","complete":1,"failed":0,"succeeded":252}
249
250             if (!progress) {
251                 console.error('No batch edit session found for ', this.session);
252                 return;
253             }
254
255             this.progressValue = progress.succeeded;
256             this.progressMax = progress.total;
257             this.numSucceeded = progress.succeeded;
258             this.numFailed = progress.failed;
259
260             if (progress.complete) {
261                 this.processing = false;
262                 return;
263             }
264
265             setTimeout(() => this.pollProgress(), SESSION_POLL_INTERVAL * 1000);
266         });
267     }
268 }
269