<div class="col-lg-3" i18n>Record Source: </div>
<div class="col-lg-6">
<select class="form-control" [(ngModel)]="source">
- <option value='bucket' i18n>Bucket</option>
- <option value='csv' i18n>CSV File</option>
- <option value='id' i18n>Bib Record ID</option>
+ <option value='b' i18n>Bucket</option>
+ <option value='c' i18n>CSV File</option>
+ <option value='r' i18n>Bib Record ID</option>
</select>
</div>
</div>
<div class="row mt-2 pt-2 pb-2 border">
- <ng-container *ngIf="source == 'bucket'">
+ <ng-container *ngIf="source == 'b'">
<div class="col-lg-3" i18n>Bucket named: </div>
<div class="col-lg-6">
<eg-combobox [entries]="buckets" (onChange)="bucketChanged($event)">
</eg-combobox>
</div>
</ng-container>
- <ng-container *ngIf="source == 'id'">
+ <ng-container *ngIf="source == 'r'">
<div class="col-lg-3" i18n>Record ID: </div>
<div class="col-lg-3">
<input type="text" class="form-control" [(ngModel)]="recordId"/>
</div>
</ng-container>
- <ng-container *ngIf="source == 'csv'">
+ <ng-container *ngIf="source == 'c'">
<div class="col-lg-12">
<div class="row">
<div class="col-lg-3" i18n>Column: </div>
</div>
<div class="row mt-2 pt-2 pb-2 border">
<div class="col-lg-12">
- <button class="btn btn-outline-dark" (click)="process()" i18n>Go!</button>
+ <button class="btn btn-outline-dark"
+ [disabled]="disableSave()" (click)="process()" i18n>Go!</button>
</div>
</div>
<div class="row mt-2 p-2" *ngIf="processing">
</eg-progress-inline>
</div>
</div>
+ <div class="row mt-2 p-2" *ngIf="!processing && progressMax">
+ <div class="col-lg-10 offset-lg-1">
+ <div i18n>Processing Complete</div>
+ <div i18n>Success count: {{this.numSucceeded}}</div>
+ <div i18n>Failed count: {{this.numFailed}}</div>
+ </div>
+ </div>
</div>
</div>
import {Component, OnInit, ViewChild, Renderer2} from '@angular/core';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {HttpClient} from '@angular/common/http';
import {tap} from 'rxjs/operators';
import {NetService} from '@eg/core/net.service';
import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {MarcRecord, MarcField} from '@eg/staff/share/marc-edit/marcrecord';
+import {AnonCacheService} from '@eg/share/util/anon-cache.service';
+
+const MERGE_TEMPLATE_PATH = '/opac/extras/merge_template';
+const SESSION_POLL_INTERVAL = 2; // seconds
interface TemplateRule {
ruleType: 'r' | 'a' | 'd';
export class MarcBatchComponent implements OnInit {
session: string;
- source: 'bucket' | 'csv' | 'id' = 'bucket';
+ source: 'b' | 'c' | 'r' = 'b';
buckets: ComboboxEntry[];
bucket: number;
recordId: number;
csvColumn = 0;
- selectedFile: File;
+ csvFile: File;
xactPerRecord = false;
templateRules: TemplateRule[] = [];
record: MarcRecord;
processing = false;
progressMax: number = null;
progressValue: number = null;
+ numSucceeded = 0;
+ numFailed = 0;
constructor(
private router: Router,
private route: ActivatedRoute,
+ private http: HttpClient,
private renderer: Renderer2,
private net: NetService,
private pcrud: PcrudService,
- private auth: AuthService
+ private auth: AuthService,
+ private cache: AnonCacheService
) {}
ngOnInit() {
- this.route.paramMap.subscribe((params: ParamMap) => {
- this.session = this.route.snapshot.paramMap.get('session');
- });
-
this.load();
}
}
fileSelected($event) {
- this.selectedFile = $event.target.files[0];
+ this.csvFile = $event.target.files[0];
+ }
+
+ disableSave(): boolean {
+ if (!this.record || !this.source || this.processing) {
+ return true;
+ }
+
+ if (!this.processing && this.progressMax) {
+ // Just completed a session.
+ return true;
+ }
+
+ if (this.source === 'b') {
+ return !this.bucket;
+
+ } else if (this.source === 'c') {
+ return (!this.csvColumn || !this.csvFile);
+
+ } else if (this.source === 'r') {
+ return !this.recordId;
+ }
}
process() {
this.processing = true;
- this.postForm()
- .then(_ => this.pollProgress())
- .then(_ => this.processing = false);
+ this.progressValue = null;
+ this.progressMax = null;
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+
+ this.postForm().then(_ => this.pollProgress());
}
postForm(): Promise<any> {
- return Promise.resolve();
+
+ const formData: FormData = new FormData();
+ formData.append('ses', this.auth.token());
+ formData.append('skipui', '1');
+ formData.append('template', this.record.toXml());
+ formData.append('recordSource', this.source);
+
+ if (this.source === 'b') {
+ formData.append('containerid', this.bucket + '');
+
+ } else if (this.source === 'c') {
+ formData.append('idcolumn', this.csvColumn + '');
+ formData.append('idfile', this.csvFile, this.csvFile.name);
+
+ } else if (this.source === 'r') {
+ formData.append('recid', this.recordId + '');
+ }
+
+ return this.http.post(
+ MERGE_TEMPLATE_PATH, formData, {responseType: 'text'})
+ .pipe(tap(cacheKey => this.session = cacheKey))
+ .toPromise();
}
pollProgress(): Promise<any> {
- return Promise.resolve();
+ console.debug('Polling session ', this.session);
+
+ return this.cache.getItem(this.session, 'batch_edit_progress')
+ .then(progress => {
+ // {"success":"t","complete":1,"failed":0,"succeeded":252}
+
+ if (!progress) {
+ console.error('No batch edit session found for ', this.session);
+ return;
+ }
+
+ this.progressValue = progress.succeeded;
+ this.progressMax = progress.total;
+ this.numSucceeded = progress.succeeded;
+ this.numFailed = progress.failed;
+
+ if (progress.complete) {
+ this.processing = false;
+ return;
+ }
+
+ setTimeout(() => this.pollProgress(), SESSION_POLL_INTERVAL * 1000);
+ });
}
}
use bytes;
use Apache2::Log;
-use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log);
+use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND HTTP_BAD_REQUEST :log);
use APR::Const -compile => qw(:error SUCCESS);
use APR::Table;
my $cgi = new CGI;
my $authid = $cgi->cookie('ses') || $cgi->param('ses');
+
+ # Avoid sending the HTML to the caller. Final response will
+ # will just be the cache key or HTTP_BAD_REQUEST on error.
+ my $skipui = $cgi->param('skipui');
+
my $usr = verify_login($authid);
- return show_template($r) unless ($usr);
+ return show_template($r, $skipui) unless ($usr);
my $template = $cgi->param('template');
- return show_template($r) unless ($template);
+ return show_template($r, $skipui) unless ($template);
my $rsource = $cgi->param('recordSource');
unless (@records) {
$e->request('open-ils.cstore.transaction.rollback')->gather(1);
$e->disconnect;
- return show_template($r);
+ return show_template($r, $skipui);
}
# we have a template and some record ids, so...
->request('open-ils.cat.container.template_overlay.background', $authid, $bucket->id)
->gather(1);
- return show_processing_template($r, $bucket->id, \@records, $cache_key);
+ return show_processing_template(
+ $r, $bucket->id, \@records, $cache_key, $skipui);
}
sub verify_login {
my $bid = shift;
my $recs = shift;
my $cache_key = shift;
+ my $skipui = shift;
+
+ if ($skipui) {
+ $r->content_type('text/plain');
+ $r->print($cache_key);
+ return Apache2::Const::OK;
+ }
my $rec_string = @$recs;
sub show_template {
my $r = shift;
+ my $skipui = shift;
+
+ # Makes no sense to call the API in such a way that the caller
+ # is returned the UI code if skipui is set.
+ return Apache2::Const::HTTP_BAD_REQUEST if $skipui;
$r->content_type('text/html');
$r->print(<<'HTML');