Commit 0a4f14f4 authored by Benjamin Rokseth's avatar Benjamin Rokseth
Browse files

Merge branch 'release/0.6.0'

CHANGELOG:

- catalinker:
  - DEICH-469_manglende_oppdatering_ved_sletting_av_opplysninger
  - DEICH-471_registrerte_opplysninger_vises_ikke_i_skjema
- patron-client:
  - DEICH-466-validering_aarstall_registrering
  - DEICH-274 fix multiple items on profile page
  - DEICH-149: Add components for publication fields
  - DEICH-476: Remove Bjerke from dropdown
  - DEICH-445: mypage: add expected date
  - DEICH-467: Add select to change pickup location
- services:
  - map more 300$b values to media types
parents 447118d6 ebb5f4c0
......@@ -13,9 +13,27 @@ The resulting compose file can then be used with `docker-compose up -d` to provi
# Releases
## 0.6.0 (2016-11-22)
KOHA: 3df1e1097b2fa4d2eae7766745fc172184d2644c
- catalinker:
- DEICH-469_manglende_oppdatering_ved_sletting_av_opplysninger
- DEICH-471_registrerte_opplysninger_vises_ikke_i_skjema
- patron-client:
- DEICH-466-validering_aarstall_registrering
- DEICH-274 fix multiple items on profile page
- DEICH-149: Add components for publication fields
- DEICH-476: Remove Bjerke from dropdown
- DEICH-445: mypage: add expected date
- DEICH-467: Add select to change pickup location
- services:
- map more 300$b values to media types
## 0.5.0 (2016-11-16)
GITREF:
GITREF: f7f4fbd3bcd03236e2e2f325a650a417c8e01b8e
KOHA: 3df1e1097b2fa4d2eae7766745fc172184d2644c
- koha:
......
......@@ -81,6 +81,9 @@ body {
}
.select2-container {
.select2-selection--multiple {
min-height: 38px !important;
}
.select2-selection {
&:focus {
@include focus()
......@@ -95,7 +98,6 @@ body {
background-repeat: no-repeat;
background-position: 99% 6px;
padding-top: 1px;
min-height: 36px;
margin: -3px 1px !important;
padding: 0px 0px 1px 5px !important;
ul {
......@@ -558,7 +560,7 @@ body {
left: 271px;
top: 28px;
display: block;
margin-top: -12px;
margin-top: -23px;
}
.support-panel-expander {
display: inline-block;
......@@ -773,6 +775,9 @@ body {
padding: 8px;
.inputs {
.input {
.index-type-select {
min-height: 37px;
}
background: $mintgreen;
padding: 0 12px;
margin-bottom: -1px;
......
......@@ -91,6 +91,9 @@
"@id": "deichman:isRelatedTo",
"@container": "@set"
},
"ean": {
"@id": "deichman:hasEan"
},
"prefLabel": {
"@id": "deichman:prefLabel"
},
......@@ -188,11 +191,15 @@
"edition": {
"@id": "deichman:edition"
},
"extent": {
"@id": "deichman:hasExtent"
},
"numberOfPages": {
"@id": "deichman:numberOfPages"
},
"isbn": {
"@id": "deichman:isbn"
"@id": "deichman:isbn",
"@container": "@set"
},
"literaryForms": {
"@id": "deichman:literaryForm",
......@@ -251,7 +258,7 @@
"@id": "deichman:genre",
"@type": "@id"
},
"hasPublicationPart": {
"publicationParts": {
"@id": "deichman:hasPublicationPart",
"@container": "@set"
},
......@@ -355,6 +362,10 @@
"publications": {
"type": "deichman:Publication",
"@embed": "@always",
"publicationParts": {
"@embed": "@always",
"type": "deichman:PublicationPart"
},
"contributors": {
"@embed": "@always",
"type": "deichman:Contribution",
......
......@@ -39,4 +39,23 @@ module.exports = (app) => {
response.sendStatus(500)
})
})
app.put('/api/v1/holds', jsonParser, (request, response) => {
fetch(`http://xkoha:8081/api/v1/holds/${request.body.reserveId}`, {
method: 'PUT',
body: JSON.stringify({
branchcode: request.body.branchCode,
priority: 1
})
}).then(res => {
if (res.status === 200) {
response.sendStatus(200)
} else {
throw Error('Could not change pickup location')
}
}).catch(error => {
console.log(error)
response.sendStatus(500)
})
})
}
......@@ -21,7 +21,7 @@ module.exports = (app) => {
function filterLibraries (libraries) {
return libraries.filter(library => {
if (![ 'api', 'hsko', 'ukjent' ].includes(library.branchcode)) { return true }
if (![ 'api', 'hsko', 'ukjent', 'fbje' ].includes(library.branchcode)) { return true }
})
}
}
......@@ -111,32 +111,39 @@ module.exports = (app) => {
}
function fetchHoldFromBiblioNumber (hold, request) {
return fetch(`http://xkoha:8081/api/v1/biblios/${hold.biblionumber}`)
.then(res => {
if (res.status === 200) {
return res.json()
} else {
throw Error(res.statusText)
}
}).then(json => {
const waitingPeriod = hold.found === 'T' ? '1-2 dager' : 'cirka 2-4 uker'
const expiry = hold.waitingdate ? new Date(Date.parse(`${hold.waitingdate}`) + (1000 * 60 * 60 * 24 * 7)).toISOString(1).split('T')[ 0 ] : 'unknown'
return {
recordId: hold.biblionumber,
reserveId: hold.reserve_id,
title: json.title,
author: json.author,
publicationYear: json.publicationYear,
orderedDate: hold.reservedate,
branchCode: hold.branchcode,
status: hold.found,
waitingDate: hold.waitingdate,
expiry: expiry,
waitingPeriod: waitingPeriod,
pickupNumber: hold.pickupnumber,
queuePlace: hold.priority
}
})
return Promise.all([
fetch(`http://xkoha:8081/api/v1/biblios/${hold.biblionumber}`)
.then(res => {
if (res.status === 200) {
return res.json()
} else {
throw Error(res.statusText)
}
}),
getExpectedAvailableDateByBiblio(hold.biblionumber)
]).then(([json, items]) => {
const pickupNumber = hold.waitingdate ? `${hold.waitingdate.split('-')[ 2 ]}/${hold.reserve_id}` : 'unknown'
const waitingPeriod = hold.found === 'T' ? '1-2 dager' : 'cirka 2-4 uker'
const expiry = hold.waitingdate ? new Date(Date.parse(`${hold.waitingdate}`) + (1000 * 60 * 60 * 24 * 7)).toISOString(1).split('T')[ 0 ] : 'unknown'
const expectedDate = estimateExpectedWait(hold.priority, items)
return {
recordId: hold.biblionumber,
reserveId: hold.reserve_id,
title: json.title,
author: json.author,
publicationYear: json.publicationYear,
orderedDate: hold.reservedate,
branchCode: hold.branchcode,
status: hold.found,
expected: expectedDate,
waitingDate: hold.waitingdate,
expiry: expiry,
waitingPeriod: waitingPeriod,
pickupNumber: pickupNumber,
queuePlace: hold.priority
}
})
}
function fetchAllCheckouts (request) {
......@@ -176,6 +183,90 @@ module.exports = (app) => {
})
}
function getExpectedAvailableDateByBiblio (biblionumber) {
return fetch(`http://xkoha:8081/api/v1/biblios/${biblionumber}/items`)
.then(res => {
if (res.status === 200) {
return res.json()
} else {
throw Error(res.statusText)
}
}).then(json => {
return json.items
})
}
function estimateExpectedWait (queuePlace, items) {
const queued = queuePlace || 1
const eligibleItems = getEligibleItems(items)
const numberOfItems = eligibleItems.length
const resultOfQueryForLengthOfLoanForItem = getEstimatedPeriod(eligibleItems)
if (resultOfQueryForLengthOfLoanForItem === 'unknown') {
return resultOfQueryForLengthOfLoanForItem
}
const estimate = ((queued / numberOfItems) * resultOfQueryForLengthOfLoanForItem)
let ceiling = Math.ceil(estimate)
const floor = Math.floor(estimate)
if (floor === estimate) {
ceiling = ceiling + 1
}
let returnVal = `${floor}${ceiling}`
if (ceiling >= 12) {
returnVal = 12
}
return returnVal
}
function getEligibleItems (items) {
return items.filter(isIncludedItemType).filter(isIncludedByAttribute)
}
function getEstimatedPeriod (items) {
const secondsInAWeek = 1000 * 60 * 60 * 24 * 7
if (items.length > 0) {
const from = Date.parse(items[ 0 ].datelastborrowed)
const to = Date.parse(items[ 0 ].onloan)
const estimate = Math.ceil((to - from) / secondsInAWeek)
return isNaN(estimate) ? 'unknown' : estimate
} else {
return 'unknown'
}
}
function isIncludedItemType (item) {
const itemType = item.itype
let included = true
switch (itemType) {
case 'DAGSLAAN' :
case 'EBOK' :
case 'REALIA' :
case 'TOUKESLAAN' :
case 'UKESLAAN' :
case 'UKJENT' :
included = false
break
default :
included = true
break
}
return included
}
function isIncludedByAttribute (item) {
let returnValue = true
if (item.withdrawn !== '0' ||
item.notforloan !== '0' ||
item.itemlost !== '0' ||
item.damaged !== '0') {
returnValue = false
}
return returnValue
}
app.post('/api/v1/profile/settings', jsonParser, (request, response) => {
fetch(`http://xkoha:8081/api/v1/messagepreferences/${request.session.borrowerNumber}`, {
method: 'PUT',
......
......@@ -143,12 +143,15 @@ function transformWork (input) {
function transformPublications (publications) {
return publications.map(publication => {
return {
ageLimit: publication.ageLimit,
binding: publication.binding,
contentAdaptations: publication.contentAdaptations,
contributors: transformContributors(publication.contributors),
description: publication.description,
duration: publication.duration,
ean: publication.ean,
edition: publication.edition,
extent: publication.extent,
formatAdaptations: publication.formatAdaptations,
formats: publication.formats,
genres: publication.genres,
......@@ -163,17 +166,32 @@ function transformPublications (publications) {
partNumber: publication.partNumber,
partTitle: publication.partTitle,
placeOfPublication: publication.hasPlaceOfPublication ? publication.hasPlaceOfPublication.prefLabel : undefined,
publicationYear: publication.publicationYear || publication['deichman:publicationYear'],
publicationParts: transformPublicationParts(publication.publicationParts),
publicationYear: publication.publicationYear,
publisher: publication.publishedBy ? publication.publishedBy.name : undefined,
publishers: publication.publishers,
recordId: publication.recordId,
serialIssues: transformSerialIssues(publication.serialIssues),
subtitle: publication.subtitle,
subtitles: publication.subtitles,
uri: publication.id
}
})
}
function transformPublicationParts (input) {
try {
return input.map(inputPublicationPart => {
return {
/* TODO */
}
})
} catch (error) {
console.log(error)
return []
}
}
function transformSerials (work) {
try {
return [].concat(...work.publications.map(publication => publication.serialIssues.map(inSerial => inSerial.name)))
......@@ -232,22 +250,18 @@ function transformBy (contributors) {
}
}
function transformCompositionType (hasCompositionType) {
function transformCompositionType (hasCompositionType = []) {
try {
if (hasCompositionType) {
return hasCompositionType.map(compositionType => compositionType.prefLabel)
}
return hasCompositionType.map(compositionType => compositionType.prefLabel)
} catch (error) {
console.log(error)
return []
}
}
function transformInstruments (hasInstrument) {
function transformInstruments (hasInstrument = []) {
try {
if (hasInstrument) {
return hasInstrument.map(instrument => instrument.hasInstrument.prefLabel)
}
return hasInstrument.map(instrument => instrument.hasInstrument.prefLabel)
} catch (error) {
console.log(error)
return []
......
......@@ -33,6 +33,7 @@ module.exports = {
},
acceptTerms: {
required: true,
asyncValidation: true
asyncValidation: true,
requiredMessageOverride: 'termsMustBeAccepted'
}
}
......@@ -8,7 +8,8 @@ module.exports = form => values => {
Object.keys(form).forEach(field => {
const value = values[ field ]
if (requiredFields.includes(field) && !value) {
errors[ field ] = 'required'
errors[ field ] = form[field].requiredMessageOverride || 'required'
return
} else if (!value) {
return
}
......
......@@ -54,11 +54,6 @@ module.exports = {
if (values.pin !== values.repeatPin) {
return 'pinsMustBeEqual'
}
},
acceptTerms: acceptTerms => {
if (!acceptTerms) {
return 'termsMustBeAccepted'
}
}
}
......
......@@ -5,6 +5,44 @@ import { showModal } from './ModalActions'
import ModalComponents from '../constants/ModalComponents'
import Errors from '../constants/Errors'
import * as ProfileActions from './ProfileActions'
import { action, errorAction } from './GenericActions'
export function changePickupLocation (reserveId, branchCode) {
const url = '/api/v1/holds/'
return dispatch => {
dispatch(requestChangePickupLocation(reserveId, branchCode))
return fetch(url, {
method: 'PUT',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ reserveId, branchCode })
})
.then(response => {
if (response.status === 200) {
setTimeout(() => {
dispatch(changePickupLocationSuccess(reserveId, branchCode))
}, 500)
} else {
throw Error(Errors.reservation.GENERIC_CHANGE_PICKUP_LOCATION_ERROR)
}
})
.catch(error => dispatch(changePickupLocationFailure(error)))
}
}
export const requestChangePickupLocation = (reserveId, branchCode) => action(types.REQUEST_CHANGE_PICKUP_LOCATION, {
reserveId,
branchCode
})
export const changePickupLocationSuccess = (reserveId, branchCode) => action(types.CHANGE_PICKUP_LOCATION_SUCCESS, {
reserveId,
branchCode
})
export const changePickupLocationFailure = error => errorAction(types.CHANGE_PICKUP_LOCATION_FAILURE, error)
export function startReservation (recordId) {
return requireLoginBeforeAction(showModal(ModalComponents.RESERVATION, { recordId: recordId }))
......
import React, { PropTypes } from 'react'
class Libraries extends React.Component {
constructor (props) {
super(props)
this.handleSelectChange = this.handleSelectChange.bind(this)
}
renderOptions () {
const branchOptions = []
const libraries = this.props.libraries
const { libraries } = this.props
Object.keys(libraries).forEach(branchCode => {
const branchName = libraries[ branchCode ]
branchOptions.push(
......@@ -19,10 +24,15 @@ class Libraries extends React.Component {
return this.select.value
}
handleSelectChange () {
this.props.onChangeAction(this.props.reserveId, this.select.value)
}
render () {
const { selectProps } = this.props
const { selectProps, selectedBranchCode, onChangeAction } = this.props
return (
<select ref={e => this.select = e} {...selectProps}>
<select ref={e => this.select = e} {...selectProps} defaultValue={selectedBranchCode}
onChange={onChangeAction ? this.handleSelectChange : undefined}>
{this.renderOptions()}
</select>
)
......@@ -31,7 +41,10 @@ class Libraries extends React.Component {
Libraries.propTypes = {
libraries: PropTypes.object.isRequired,
selectProps: PropTypes.object
selectProps: PropTypes.object,
selectedBranchCode: PropTypes.string,
reserveId: PropTypes.string,
onChangeAction: PropTypes.func
}
export default Libraries
import React from 'react'
const Loading = () => (
<span className="loading">
<i className="icon-spin4 animate-spin" />
</span>
)
export default Loading
......@@ -3,14 +3,14 @@ import { FormattedMessage } from 'react-intl'
const MetaItem = (props) => {
const dataAutomationId = props[ 'data-automation-id' ]
const { label, content } = props
const { label, children } = props
return (
<div className="meta-item">
<span className="meta-label"><FormattedMessage {...label} />: </span>
<span
data-automation-id={dataAutomationId}
className="meta-content">{content}</span>
className="meta-content">{children}</span>
</div>
)
}
......@@ -20,7 +20,7 @@ MetaItem.propTypes = {
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired
}).isRequired,
content: PropTypes.oneOfType([ PropTypes.string, PropTypes.node ]).isRequired,
children: PropTypes.node.isRequired,
'data-automation-id': PropTypes.string
}
......
......@@ -2,18 +2,11 @@ import React, { PropTypes } from 'react'
import { injectIntl, intlShape, defineMessages, FormattedMessage } from 'react-intl'
import ClickableElement from '../components/ClickableElement'
import Constants from '../constants/Constants'
import title from '../utils/title'
class Publication extends React.Component {
renderTitle (publication) {
let title = publication.mainTitle
if (publication.partTitle) {
title += ` — ${publication.partTitle}`
}
return title
}
renderBookCover (publication) {
const coverAltText = this.props.intl.formatMessage(messages.coverImageOf, { title: this.renderTitle(publication) })
const coverAltText = this.props.intl.formatMessage(messages.coverImageOf, { title: title(publication) })