Commit a9fc012a authored by laurent l's avatar laurent l

Add cancel hold & renew loans actions for Bokeh

parent c46e6812
import { ObservableArray } from 'tns-core-modules/data/observable-array';
import { View } from 'tns-core-modules/ui/core/view';
import { Controller } from './controller';
import { BarcodeScanner } from 'nativescript-barcodescanner';
import { AccountsController } from '../controllers/accounts';
......@@ -16,10 +17,12 @@ import {
WrongLoginPassword,
Cookies,
Manager,
Iso8601Date
Iso8601Date,
PortalActionResult
} from '../models/';
import { RadSideDrawer } from 'nativescript-ui-sidedrawer';
import { RadListView } from 'nativescript-ui-listview';
import { WebView } from 'tns-core-modules/ui/web-view';
import { OAuthWebViewClient } from './utils/oauth-webview-client';
import { WebViewUtils } from 'nativescript-webview-utils';
......@@ -34,7 +37,7 @@ export class LibraryController extends Controller {
protected _on_loans_page_loaded = null;
protected _on_successful_login = null;
protected _manager: Manager;
protected _view;
protected _view: View;
public setPortal(portal: Portal): LibraryController {
this._portal = portal;
......@@ -381,6 +384,48 @@ export class LibraryController extends Controller {
}
public renewLoanAction(args) {
const loan = <Loan>args.object.bindingContext;
return this._portal
.renewLoan(loan.getAccount(), loan)
.then((result: PortalActionResult) => {
const message = result.isSuccess()
? L('loan_renewed', new Iso8601Date(loan.getDateDue()).asLongDate())
: result.getMessage()
this.dialogs.alert({
title: L('renew'),
message: message,
okButtonText: L('OK')
})
})
}
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 +554,22 @@ 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))
})
const loans = this._account.findLoans(Database.current())
loans.forEach((loan) => {
loan.set('renewable', this._portal.isRenewable(this._account,
loan))
})
return this
._updateItems(this._loans, this._account.findLoans(Database.current()))
._updateItems(this._holds, this._account.findHolds(Database.current()))
._updateItems(this._loans, loans)
._updateItems(this._holds, holds)
}
......@@ -532,13 +590,13 @@ export class LibraryController extends Controller {
if (!this._view)
return this;
let loans = this._view.getViewById("loans")
let loans: RadListView = this._view.getViewById("loans")
if (loans != undefined) {
loans.notifyPullToRefreshFinished()
loans.refresh()
}
let holds = this._view.getViewById("holds")
let holds: RadListView = this._view.getViewById("holds")
if (holds != undefined) {
holds.notifyPullToRefreshFinished()
holds.refresh()
......
......@@ -94,4 +94,6 @@
</string>
<string name="used_software">Software used</string>
<string name="used_software_content">The application used following software libraries:</string>
<string name="renew">Renew</string>
<string name="confirm_cancel_hold">Are you sure to cancel this hold ?</string>
</resources>
......@@ -94,4 +94,6 @@
</string>
<string name="used_software">Software utilizado</string>
<string name="used_software_content">La aplicación utiliza las siguientes bibliotecas de software:</string>
<string name="renew">Extender</string>
<string name="confirm_cancel_hold">Cancelar reserva ?</string>
</resources>
......@@ -94,4 +94,7 @@
</string>
<string name="used_software">Logiciels utilisés</string>
<string name="used_software_content">L\'application utilise les bibliothèques logicielles suivantes:</string>
<string name="renew">Prolonger</string>
<string name="confirm_cancel_hold">Voulez-vous annuler cette réservation ?</string>
<string name="loan_renewed">Prêt prolongé jusqu\'au %s</string>
</resources>
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, Loan, 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,38 @@ export class Portal {
}
public cancelHold(account: Account, hold: Hold): Promise<any> {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.cancelHold(account, hold)
}
public renewLoan(account: Account, loan: Loan): Promise<any> {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.renewLoan(account, loan)
}
public isCancellable(account: Account, hold: Hold): boolean {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.isCancellable(hold)
}
public isRenewable(account: Account, loan: Loan): boolean {
return this
.findAdapterIdentifiedBy(account.getAdapterIdentifier())
.isRenewable(loan)
}
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 +108,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,127 @@ 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)
}
public renewLoan(account: Account, loan: Loan): Promise<PortalActionResult> {
return Promise.reject(new PortalActionResult)
}
......@@ -127,37 +138,47 @@ 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;
}
public isRenewable(loan: Loan): 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