Commit 93265149 authored by laurent l's avatar laurent l

Add cancel hold action for Bokeh

parent c46e6812
......@@ -16,7 +16,8 @@ import {
WrongLoginPassword,
Cookies,
Manager,
Iso8601Date
Iso8601Date,
PortalActionResult
} from '../models/';
import { RadSideDrawer } from 'nativescript-ui-sidedrawer';
......@@ -381,6 +382,30 @@ export class LibraryController extends Controller {
}
public cancelHoldAction(args) {
const hold = <Hold>args.object.bindingContext;
this.dialogs
.confirm({
title: hold.getTitle(),
message: L('confirm_cancel_hold'),
okButtonText: L('OK'),
cancelButtonText: L('cancel')
})
.then((result) => {
if (!result) return;
return this._portal
.cancelHold(hold.getAccount(), hold)
.then((result: PortalActionResult) => {
if (result.isSuccess()) {
Database.current().delete(hold);
this._loadLoansAndHolds()
}
})
})
}
protected _authenticationDone(account: Account): LibraryController {
Database.current().save(account);
this.set('busy', true);
......@@ -509,9 +534,15 @@ export class LibraryController extends Controller {
protected _loadLoansAndHolds(): this {
const holds = this._account.findHolds(Database.current());
holds.forEach((hold) => {
hold.set('cancellable', this._portal.isCancellable(this._account,
hold));
})
return this
._updateItems(this._loans, this._account.findLoans(Database.current()))
._updateItems(this._holds, this._account.findHolds(Database.current()))
._updateItems(this._holds, holds)
}
......
import {Loan,
Hold,
Card,
Persistable,
Serializable,
DataSource} from '../models/'
import {
Loan,
Hold,
Card,
Persistable,
Serializable,
DataSource
} from '../models/'
export class Account extends Persistable {
......@@ -27,8 +29,8 @@ export class Account extends Persistable {
return this._cookies
}
public setLabel(label:string): Account {
public setLabel(label: string): Account {
this._label = label
return this
}
......@@ -66,7 +68,7 @@ export class Account extends Persistable {
.forEach((loan) => {
datasource.delete(loan)
})
this._number_of_loans = 0
return this
}
......@@ -78,7 +80,7 @@ export class Account extends Persistable {
.forEach((hold) => {
datasource.delete(hold)
})
this._number_of_holds = 0
return this
}
......@@ -88,7 +90,7 @@ export class Account extends Persistable {
this._number_of_loans = number_of_loans
return this
}
public setNumberOfHolds(number_of_holds: number): this {
this._number_of_holds = number_of_holds
......@@ -100,7 +102,10 @@ export class Account extends Persistable {
return this
._findAll(datasource, Loan)
.sort((a, b) => {
return a.getDateDue().localeCompare(b.getDateDue())
const date_compare = a.getDateDue().localeCompare(b.getDateDue())
return (date_compare !== 0)
? date_compare
: a.getTitle().localeCompare(b.getTitle())
})
}
......@@ -108,12 +113,15 @@ export class Account extends Persistable {
public findHolds(datasource: DataSource): Array<Hold> {
return this._findAll(datasource, Hold)
.sort((a, b) => {
return a.getStatus().localeCompare(b.getStatus())
const status_compare = a.getStatus().localeCompare(b.getStatus())
return (status_compare !== 0)
? status_compare
: a.getTitle().localeCompare(b.getTitle())
})
}
protected _findAll<T extends Persistable>(datasource: DataSource, modelClass:{new():T}): Array<T> {
protected _findAll<T extends Persistable>(datasource: DataSource, modelClass: { new(): T }): Array<T> {
return datasource
.findAll(modelClass)
.filter((obj) => {
......@@ -136,7 +144,7 @@ export class Account extends Persistable {
this._url.replace(new RegExp('/$'), '')
if (!this._url.includes('://'))
this._url = 'http://' + this._url
return this._url
return this._url
}
......@@ -164,7 +172,7 @@ export class Account extends Persistable {
public beforeSave(datasource: DataSource) {
this._cacheCounters(datasource)
datasource.save(this._card)
datasource.save(this._card)
}
......@@ -174,7 +182,7 @@ export class Account extends Persistable {
}
public getAdapterIdentifier():string {
public getAdapterIdentifier(): string {
return this._credentials['portal']
? this._credentials['portal']
: 'autodetect'
......@@ -204,7 +212,7 @@ export class Account extends Persistable {
public daysToNextLoanAction(to_date: Date = new Date()): number {
if (!this.loanEarliestDateDue())
return null
let from_date: Date = new Date(this.loanEarliestDateDue())
let time_diff = to_date.getTime() - from_date.getTime()
return Math.ceil(time_diff / (1000 * 3600 * 24))
......@@ -231,7 +239,7 @@ export class Account extends Persistable {
this._card = serializable.get('card', Card)
if (!this._card)
this._card = new Card()
this._number_of_loans = serializable.get('number_of_loans')
if (!this._number_of_loans)
this._number_of_loans = 0
......
import {ItemOperation, Serializable} from '../models';
import { ItemOperation, Serializable } from '../models';
export class Hold extends ItemOperation {
protected _hold_id: string;
protected _status: string;
public setHoldId(hold_id: string): this {
this._hold_id = hold_id;
return this;
......@@ -33,11 +33,10 @@ export class Hold extends ItemOperation {
.set('status', this._status);
}
public materializeFrom(serializable: Serializable) {
super.materializeFrom(serializable);
this._hold_id = serializable.get('hold_id');
this._status = serializable.get('status');
}
}
export { Persistable, Serializable, DataSource } from './persistable';
export { Database } from './database';
export { Account } from './account';
export { ItemOperation } from './item-operation';
export { Loan } from './loan';
export { Hold } from './hold';
export { Portal } from './portal';
export { HTTPClient } from './http/http-client';
export { NSHTTPClient } from './http/ns-http-client';
export { NullHTTPClient } from './http/null-http-client';
export { PortalAdapter } from './portal/adapter';
export { OAuthTokenNotSet } from './portal/oauth-token-not-set';
export { WrongLoginPassword } from './portal/wrong-login-password';
export { PortalDetectionFail } from './portal/portal-detection-fail';
export { Cookies } from './cookies/cookies';
export { Manager, MBPortal } from './manager';
export { Record } from './record';
export { Item } from './item';
export { Novelties } from './novelties';
export { Card } from './card';
export { Iso8601Date } from './iso8601_date';
export { BackgroundJobs } from './background-jobs/background-jobs';
export { Form } from './form';
export { Persistable, Serializable, DataSource } from './persistable'
export { Database } from './database'
export { Account } from './account'
export { ItemOperation } from './item-operation'
export { Loan } from './loan'
export { Hold } from './hold'
export { Portal } from './portal'
export { HTTPClient } from './http/http-client'
export { NSHTTPClient } from './http/ns-http-client'
export { NullHTTPClient } from './http/null-http-client'
export { PortalAdapter } from './portal/adapter'
export { OAuthTokenNotSet } from './portal/oauth-token-not-set'
export { WrongLoginPassword } from './portal/wrong-login-password'
export { PortalDetectionFail } from './portal/portal-detection-fail'
export { PortalActionResult } from './portal/portal-action-result'
export { Cookies } from './cookies/cookies'
export { Manager, MBPortal } from './manager'
export { Record } from './record'
export { Item } from './item'
export { Novelties } from './novelties'
export { Card } from './card'
export { Iso8601Date } from './iso8601_date'
export { BackgroundJobs } from './background-jobs/background-jobs'
export { Form } from './form'
import {Account, Persistable, Serializable} from '../models/';
import { Account, Persistable, Serializable } from '../models/';
export abstract class ItemOperation extends Persistable {
protected _account: Account;
......@@ -38,24 +38,24 @@ export abstract class ItemOperation extends Persistable {
}
public setTitle(title:string): this {
public setTitle(title: string): this {
this._item.title = title;
return this;
}
public getTitle():string {
return this._item.title;
public getTitle(): string {
return this._item.title || '';
}
public setAuthor(author:string): this {
public setAuthor(author: string): this {
this._item.author = author;
return this;
}
public getAuthor():string {
public getAuthor(): string {
return this._item.author;
}
......@@ -93,14 +93,14 @@ export abstract class ItemOperation extends Persistable {
return this._account && this._account.getId() == other.getId();
}
public serializeOn(serializable: Serializable) {
serializable
.set('item', this._item)
.set('account', this._account.getId());
}
public materializeFrom(serializable: Serializable) {
this._item = serializable.get("item") || {};
this._account = serializable.get('account', Account);
......
......@@ -45,15 +45,18 @@ export abstract class Persistable extends Observable {
export interface Serializable {
interface Serializable {
set(name, value): Serializable;
get(name, modelClass?: any): any;
serialize(): Object;
}
export interface DataSource {
interface DataSource {
delete(model: Persistable): DataSource;
save(model: Persistable): DataSource;
findAll(modelClass: any): any;
}
export { DataSource, Serializable }
import * as PortalAdapters from './portal/index';
import {PortalAdapter} from './portal/adapter';
import {Database, Account, Item} from '../models';
import { PortalAdapter } from './portal/adapter';
import { Database, Account, Hold, Item } from '../models';
export class Portal {
protected _adapters: Map<string, PortalAdapter> = new Map<string, PortalAdapter>();
get adapters(): Map<string, PortalAdapter> {
return this._adapters.size > 0
? this._adapters
......@@ -39,7 +40,7 @@ export class Portal {
.searchUrl(account, terms);
}
public canFetchNovelties(account: Account): boolean {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
......@@ -68,10 +69,24 @@ export class Portal {
}
public cancelHold(account: Account, hold: Hold): Promise<any> {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.cancelHold(account, hold)
}
public isCancellable(account: Account, hold: Hold): boolean {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.isCancellable(hold)
}
public updateAccountInfo(account: Account): Promise<any> {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.updateCard(account).then( () => {
.updateCard(account).then(() => {
Database.current().save(account)
return account
});
......@@ -79,7 +94,7 @@ export class Portal {
protected _populateAdapters(): Map<string, PortalAdapter> {
for(let key in PortalAdapters) {
for (let key in PortalAdapters) {
let adapter: PortalAdapter = new PortalAdapters[key]();
this._adapters.set(adapter.getIdentifier(), adapter);
}
......
import * as http from 'tns-core-modules/http';
import * as http from 'tns-core-modules/http'
import { ImageSource } from 'tns-core-modules/image-source'
import {
Cookies,
......@@ -9,116 +9,122 @@ import {
Loan,
Item,
Form,
Hold
} from '../../models';
Hold,
PortalActionResult
} from '../../models'
export abstract class PortalAdapter {
private _http: any;
private _cookies: Cookies;
private _http: any
private _cookies: Cookies
public constructor() {
this._http = new NSHTTPClient();
this._cookies = new Cookies();
this._http = new NSHTTPClient()
this._cookies = new Cookies()
}
public setHTTP(http: any): PortalAdapter {
this._http = http;
return this;
public setHTTP(http: any): this {
this._http = http
return this
}
public updateAdapterHTTPClient(adapter: PortalAdapter): PortalAdapter {
adapter.setHTTP(this._http);
return this;
adapter.setHTTP(this._http)
return this
}
public signIn(account): Promise<any> {
this._cookies.clearCookies();
this._cookies.clearCookies()
return this
._signIn(account)
.then(() => {
return this.updateAccountInfo(account);
});
return this.updateAccountInfo(account)
})
}
public refresh(account): Promise<any> {
if (!account.isConnected()) {
account.setCookies(new Array<any>());
account.setCookies(new Array<any>())
return this._signIn(account).then(() => {
return this.refresh(account);
});
return this.refresh(account)
})
}
let db: Database = Database.current();
let db: Database = Database.current()
return this
._fetchLoans(account)
.then((loans) => {
account.deleteLoans(db);
account.deleteLoans(db)
loans.forEach((loan) => {
db.save(loan.setAccount(account));
});
db.save(loan.setAccount(account))
})
db.save(account);
db.save(account)
})
.then(() => {
return this._fetchHolds(account);
return this._fetchHolds(account)
})
.then((holds) => {
account.deleteHolds(db);
account.deleteHolds(db)
holds.forEach((hold) => {
db.save(hold.setAccount(account));
});
db.save(hold.setAccount(account))
})
account.setNumberOfHolds(holds.length);
db.save(account);
account.setNumberOfHolds(holds.length)
db.save(account)
})
}
public request(account: Account, options: any): Promise<http.HttpResponse> {
this._cookies.setCookies(account.getCookies());;
this._cookies.setCookies(account.getCookies())
return this._http
.request(options)
.then((response) => {
account.setCookies(this._cookies.getCookies());
return response;
});
account.setCookies(this._cookies.getCookies())
return response
})
}
public searchUrl(account: Account, terms: string): Promise<string> {
return Promise.resolve('');
return Promise.resolve('')
}
public canSearch(): boolean {
return false;
return false
}
public canFetchNovelties(): boolean {
return false;
return false
}
public fetchNovelties(account: Account, page: number): Promise<any> {
return Promise.resolve(new Array());
return Promise.resolve(new Array())
}
public updateCard(account: Account): Promise<any> {
return Promise.resolve();
return Promise.resolve()
}
public cancelHold(account: Account, hold: Hold): Promise<PortalActionResult> {
return Promise.reject(new PortalActionResult)
}
......@@ -127,37 +133,42 @@ export abstract class PortalAdapter {
? Promise.resolve()
: this._fetchAccountLabel(account).then(
(label) => {
account.setLabel(label);
});
account.setLabel(label)
})
}
public fetchItem(account, barcode): Promise<Item> {
return Promise.reject(null);
return Promise.reject(null)
}
public isCancellable(hold: Hold): boolean {
return false;
}
protected _fetchAccountLabel(account: Account): Promise<any> {
return Promise.resolve(account.credentials['login']);
};
return Promise.resolve(account.credentials['login'])
}
protected _fetchHolds(account: Account): Promise<Array<Hold>> {
return Promise.resolve(new Array<Hold>());
return Promise.resolve(new Array<Hold>())
}
protected _fetchThumbnails<T extends ItemOperation>(account: Account,
items: Array<T>): Promise<Array<T>> {
return Promise.all(items.map((item) => {
return this._fetchThumbnail(account, item);
return this._fetchThumbnail(account, item)
}))
}
protected _fetchThumbnail<T extends ItemOperation>(account: Account, item: T): Promise<T> {
if (!item.getRecordThumbnail())
return Promise.resolve(item);
return Promise.resolve(item)
return this
.request(account,
......@@ -179,7 +190,7 @@ export abstract class PortalAdapter {
return response.content
.toImage()
.then((image_source: ImageSource) => {
return item.setRecordThumbnail('data:image/jpg;base64,' + image_source.toBase64String('jpg'));
return item.setRecordThumbnail('data:image/jpg;base64,' + image_source.toBase64String('jpg'))
})
})
}
......@@ -189,15 +200,15 @@ export abstract class PortalAdapter {
return new Form(form).toUrlEncodedString()
}
protected abstract _signIn(account: Account): Promise<any>;
protected abstract _signIn(account: Account): Promise<any>
public abstract getIdentifier(): string;
public abstract getIdentifier(): string
public abstract getLabel(): string;
public abstract getLabel(): string
public abstract canHandleWebsite($): boolean;
public abstract canHandleWebsite($): boolean
protected abstract _fetchLoans(account: Account): Promise<Array<Loan>>;
protected abstract _fetchLoans(account: Account): Promise<Array<Loan>>
}
import { PortalAdapter } from './adapter';
import { Database, Account, Hold, Loan, Record, Item } from '../../models';
import { Database, Account, Hold, Loan, Record, Item, PortalActionResult } from '../../models';
import { OAuthTokenNotSet } from './oauth-token-not-set';
......@@ -52,6 +52,18 @@ export class Bokeh extends PortalAdapter {
}
public cancelHold(account: Account, hold: Hold): Promise<PortalActionResult> {
return this
.requestUser(account, 'cancel-hold/id/' + hold.getHoldId())
.then((datas) => {
if (datas['status'] == 'canceled')
return new PortalActionResult().beSuccess()
return new PortalActionResult().beError(datas['error']);
})
}
public fetchNovelties(account: Account, page: number): Promise<any> {
return this
.request(account,
......@@ -122,6 +134,11 @@ export class Bokeh extends PortalAdapter {
}
public isCancellable(hold: Hold): boolean {
return true;
}
protected _fetchAccountLabel(account: Account): Promise<any> {
return this
.requestUser(account, 'account')
......
export {Bokeh} from './bokeh';
export {PMB} from './pmb';
export {OrpheeMedia} from './orpheemedia';
export {DecalogOpac} from './decalog-opac';
export {DecalogOpacV2} from './decalog-opac-v2';
export {Ermes} from './ermes';
export {Koha} from './koha';
export {KohaCas} from './koha-cas';
export {InMedia} from './inmedia';
export {Symphony} from './symphony';
export {InMediaV2} from './inmedia-v2';
export {IguanaV3} from './iguana-v3';
export {Autodetect} from './autodetect';
export { Bokeh } from './bokeh';
export { PMB } from './pmb';
export { OrpheeMedia } from './orpheemedia';
export { DecalogOpac } from './decalog-opac';
export { DecalogOpacV2 } from './decalog-opac-v2';
export { Ermes } from './ermes';
export { Koha } from './koha';
export { KohaCas } from './koha-cas';
export { InMedia } from './inmedia';
export { Symphony } from './symphony';
export { InMediaV2 } from './inmedia-v2';
export { IguanaV3 } from './iguana-v3';
export { Autodetect } from './autodetect';
export class PortalActionResult {
static readonly SUCCESS: string = 'SUCCESS'
static readonly ERROR: string = 'ERROR'
static readonly UNSUPPORTED: string = 'UNSUPPORTED'
protected _status: string = PortalActionResult.UNSUPPORTED
protected _message: string
public beSuccess(): this {
this._status = PortalActionResult.SUCCESS
return this