'moment'
'od_config'
'od_session'
-], ($, _, json, C, M, config, Session) ->
+ 'od_data'
+], ($, _, json, C, M, config, Session, D) ->
# Dump the given arguments or log them to console
log = ->
# whenever result data becomes available after making an API request.
eventList = [
'od.clientaccess'
- 'od.libraryaccount'
+ 'od.libraryinfo'
'od.metadata'
'od.availability'
when 'a' then @serializeArray()
else @
- # Mutate an ISO 8601 date string into a Moment object. If the argument is
- # just a date value, then it specifies an absolute date in ISO 8601 format.
- # If the argument is a pair, then it specifies a date relative to now. For
- # an ISO 8601 date, we correct for what seems to be an error in time zone,
- # Zulu time is really East Coast time.
- momentize = (date, unit) ->
- switch arguments.length
- when 1
- if date then M(date.replace /Z$/, '-0400') else M()
- when 2
- if date then M().add date, unit else M()
- else M()
# We define the public interface of the module
# TODO wrap od in jquery so that we can use it to trigger events and bind event handlers
# API URL.
if method is 'post'
if /\/holds|\/suspension/.test url
- x = arguments[0]
- x.holdPlacedDate = momentize x.holdPlacedDate
- x.holdExpires = momentize x.holdExpires
- if x.holdSuspension
- x.holdSuspension.numberOfDays = momentize x.holdSuspension.numberOfDays, 'days'
+ x = new D.Holds holds: [ arguments[0] ]
od.$.triggerHandler 'od.hold.update', x
if /\/checkouts/.test url
- x = arguments[0]
- x.expires = momentize x.expires
+ x = new D.Checkouts checkouts: [ arguments[0] ]
od.$.triggerHandler 'od.checkout.update', x
# For a delete method, we do not get a data object in reply,
- # thus we pattern match for the specific ID and trigger an
+ # thus we pattern match for the specific ID, and trigger an
# event with the ID.
if method is 'delete'
if id = url.match /\/holds\/(.+)\/suspension$/
return # no relevant event
if id = url.match /\/holds\/(.+)$/
- od.$.triggerHandler 'od.hold.delete', reserveId: id[1]
+ od.$.triggerHandler 'od.hold.delete', id[1]
if id = url.match /\/checkouts\/(.+)$/
- od.$.triggerHandler 'od.checkout.delete', reserveId: id[1]
+ od.$.triggerHandler 'od.checkout.delete', id[1]
.fail ->
od.$.triggerHandler 'od.error', [url, arguments[0]]
#$('<div>')._notify arguments[1].statusText, arguments[0].responseText
- # Get a library access token so that we can use the Discovery API
+ # Get a library access token so that we can use the Discovery API.
+ # The token is cached and also published to other modules.
apiDiscAccess: ->
ok = (x) ->
- # Cache the server's response object and publish it to other
- # modules
session.token.update x
od.$.triggerHandler 'od.clientaccess', x
+ return x
_api session.links.token.href, grant_type: 'client_credentials'
# relogin. In effect, we would only proceed with a retry to get a
# library access token, but if the user has logged in, we would not.
#
- apiAccount: ->
+ apiLibraryInfo: ->
get = -> od.api session.links.libraries.href
ok = (x) ->
session.links.update x
session.labels.update x
- od.$.triggerHandler 'od.libraryaccount', x
- return
+ od.$.triggerHandler 'od.libraryinfo', x
+ return x
retry = (jqXHR) ->
ok = (x) ->
session.token.update x
od.$.triggerHandler 'od.patronaccess', x
+ return x
# Get patron preferences page
$.get '/eg/opac/myopac/prefs'
return unless x
od.api session.links.products.href, get, x
- # TODO we can probably get away with using one normalization routine
- # instead of using one for each type of data object, because they don't
- # share property names.
-
apiMetadata: (x) ->
return unless x.id
+
od.api "#{session.links.products.href}/#{x.id}/metadata"
.then (y) ->
- # Convert ID to upper case to match same case found in EG catalogue
- y.id = y.id.toUpperCase()
- # Provide a simplified notion of author: first name in creators
- # list having a role of author
- y.author = (v.name for v in y.creators when v.role is 'Author')[0] or ''
- # Publish the metadata object
+ y = new D.Metadata y
od.$.triggerHandler 'od.metadata', y
y
od.api url
- # Post-process the result, eg, fill in empty properties
.then (y) ->
- # Normalize the result by adding zero values
- y.copiesOwned = 0 unless y.copiesOwned
- y.copiesAvailable = 0 unless y.copiesAvailable
- y.numberOfHolds = 0 unless y.numberOfHolds
-
- if y.actions?.hold
- # The reserve ID is empty in the actions.hold.fields; we have to fill it ourselves.
- _.where(y.actions.hold.fields, name: 'reserveId')[0].value = y.id
- # We jam the email address from the prefs page into the fields object from the server
- # so that the new form will display it.
- if email_address = session.prefs.email_address
- _.where(y.actions.hold.fields, name: 'emailAddress')[0].value = email_address
-
+ y = new D.Availability y, session.prefs.email_address
od.$.triggerHandler 'od.availability', y
- arguments
+ return y
.fail -> od.$.triggerHandler 'od.availability', x
apiPatronInfo: ->
+
ok = (x) ->
session.links.update x
od.$.triggerHandler 'od.patroninfo', x
- return
+ return x
od.api session.links.patrons.href
.then ok, logError
+ # Get a specific hold or all holds
apiHoldsGet: (x) ->
return unless session.token.is_patron_access()
od.api "#{session.links.holds.href}#{if x?.productID then x.productID else ''}"
- # Post-process the result, eg, fill in empty properties, sort list,
- # remove redundant actions or add missing actions
.then (y) ->
-
- # Normalize the result by adding an empty holds list
- xs = y.holds or []
-
- # For each hold, convert any ISO 8601 date strings into a
- # Moment object (at the local time zone)
- for x in xs
- x.holdPlacedDate = momentize x.holdPlacedDate
- x.holdExpires = momentize x.holdExpires
- if x.holdSuspension
- x.holdSuspension.numberOfDays = momentize x.holdSuspension.numberOfDays, 'days'
-
- # Count the number of holds that can be checked out now
- y.ready = _.countBy xs, (x) -> if x.actions.checkout then 'forCheckout' else 'other'
- y.ready.forCheckout = 0 unless y.ready.forCheckout
-
- # Delete action to release a suspension if a hold is not
- # suspended, because such actions are redundant
- delete x.actions.releaseSuspension for x in xs when not x.holdSuspension
-
- # Sort the holds list by position and placed date
- # and sort ready holds first
- #y.holds = _.sortBy xs, ['holdListPosition', 'holdPlacedDate']
- y.holds = _(xs)
- .sortBy ['holdListPosition', 'holdPlacedDate']
- .sortBy (x) -> x.actions.checkout
- .value()
-
+ y = new D.Holds y
od.$.triggerHandler 'od.holds', y
- arguments
+ return y
+ # Get a specific checkout or all checkouts
apiCheckoutsGet: (x) ->
return unless session.token.is_patron_access()
od.api "#{session.links.checkouts.href}#{if x?.reserveID then x.reserveID else ''}"
- # Post-process the result, eg, fill in empty properties, sort list,
- # remove redundant actions or add missing actions
.then (y) ->
-
- # Normalize the result by adding an empty checkouts list
- xs = y.checkouts or []
-
- # Convert any ISO 8601 date strings into a Moment object (at
- # the local time zone)
- for x in xs
- x.expires = momentize x.expires
-
- # Sort the checkout list by expiration date
- y.checkouts = _.sortBy xs, 'expires'
-
+ y = new D.Checkouts y
od.$.triggerHandler 'od.checkouts', y
- arguments
+ return y
- # Get a list of user's 'interests', ie, holds and checkouts
+ # Consolidate the holds and checkouts lists into an object that
+ # represents the 'interests' of the patron
apiInterestsGet: ->
$.when(
od.apiHoldsGet()
od.apiCheckoutsGet()
)
-
- # Consolidate the holds and checkouts information into an object
- # that represents the 'interests' of the patron
.then (h, c) ->
-
- # A useful condition to handle if the API calls could not
- # be fulfilled because they are not within the scope of the
- # current access token
- # TODO possibly redundant or unnecessary
- ###
- unless h and c
- page {}, {}
- return
- ###
-
- h = h[0]
- c = c[0]
-
- interests =
- nHolds: h.totalItems
- nHoldsReady: h.ready.forCheckout
- nCheckouts: c.totalItems
- nCheckoutsReady: c.totalCheckouts
- ofHolds: h.holds
- ofCheckouts: c.checkouts
- # The following property is a map from product ID to a hold or
- # a checkout object, eg, interests.byID(124)
- byID: do (hs = h.holds, cs = c.checkouts) ->
- byID = {}
- for v, n in hs
- v.type = 'hold'
- byID[v.reserveId] = v
- for v, n in cs
- v.type = 'checkout'
- byID[v.reserveId] = v
- return byID
-
- # Publish patron's interests to all areas of the screen
- od.$.triggerHandler 'od.interests', interests
- return interests
+ y = new D.Interests h, c
+ od.$.triggerHandler 'od.interests', y
+ return y
return od
--- /dev/null
+define [
+ 'lodash'
+ 'moment'
+], (
+ _
+ M
+) ->
+
+ # A base class defining utilitarian methods
+ class U
+ constructor: (x) ->
+ return unless x
+ t = @
+ t extends x
+ return
+
+ # Mutate an ISO 8601 date string into a Moment object. If the argument is
+ # just a date value, then it specifies an absolute date in ISO 8601 format.
+ # If the argument is a pair, then it specifies a date relative to now. For
+ # an ISO 8601 date, we correct for what seems to be an error in time zone,
+ # Zulu time is really East Coast time.
+ momentize: (date, unit) ->
+ switch arguments.length
+ when 1
+ if date then M(date.replace /Z$/, '-0400') else M()
+ when 2
+ if date then M().add date, unit else M()
+ else M()
+
+
+ class Metadata extends U
+ constructor: (x) ->
+ super x
+
+ # Convert ID to upper case to match same case found in EG catalogue
+ @id = @id.toUpperCase()
+ # Provide a simplified notion of author: first name in creators
+ # list having a role of author
+ @author = (v.name for v in @creators when v.role is 'Author')[0] or ''
+
+ return
+
+
+ class Availability extends U
+ constructor: (x, email_address) ->
+ super x
+
+ @zero()
+ @hold email_address if @actions?.hold
+
+ return @
+
+ # Add zero values
+ zero: ->
+ @copiesOwned = 0 unless @copiesOwned
+ @copiesAvailable = 0 unless @copiesAvailable
+ @numberOfHolds = 0 unless @numberOfHolds
+ return @
+
+ hold: (email_address) ->
+ # The reserve ID is empty in the actions.hold.fields; we have to fill it ourselves.
+ _.where(@actions.hold.fields, name: 'reserveId')[0].value = @id
+ # We jam the email address from the prefs page into the fields object from the server
+ # so that the new form will display it.
+ if email_address
+ _.where(@actions.hold.fields, name: 'emailAddress')[0].value = email_address
+ return @
+
+
+ class Holds extends U
+ constructor: (x) ->
+ super x
+
+ @add()
+ .remove()
+ .moments()
+ .count()
+ .sort()
+
+ return
+
+ # Ensure there is always a holds list, even if it's empty
+ add: ->
+ @holds = [] if @holds is undefined
+ return @
+
+ # Delete action to release a suspension if a hold is not
+ # suspended, because such actions are redundant
+ remove: ->
+ delete x.actions.releaseSuspension for x in @holds when not x.holdSuspension
+ return @
+
+ # For each hold, convert any ISO 8601 date strings into a
+ # Moment object (at local time zone)
+ moments: ->
+ for x in @holds
+ x.holdPlacedDate = @momentize x.holdPlacedDate
+ x.holdExpires = @momentize x.holdExpires
+ if x.holdSuspension
+ x.holdSuspension.numberOfDays = @momentize x.holdSuspension.numberOfDays, 'days'
+ return @
+
+ # Count the number of holds that can be checked out now
+ count: ->
+ @ready = _.countBy @holds, (x) -> if x.actions.checkout then 'forCheckout' else 'other'
+ @ready.forCheckout = 0 unless @ready.forCheckout
+ return @
+
+ # Sort the holds list by position and placed date
+ # and sort ready holds first
+ sort: ->
+ @holds = _(@holds)
+ .sortBy ['holdListPosition', 'holdPlacedDate']
+ .sortBy (x) -> x.actions.checkout
+ .value()
+ return @
+
+
+ class Checkouts extends U
+ constructor: (x) ->
+ super x
+
+ @add()
+ .moments()
+ .sort()
+
+ return
+
+ # Ensure there is always a checkouts list, even if it's empty
+ add: ->
+ @checkouts = [] if @checkouts is undefined
+ return @
+
+ # For each checkout, convert any ISO 8601 date strings into a
+ # Moment object (at local time zone)
+ moments: ->
+ for x in @checkouts
+ x.expires = @momentize x.expires
+ return @
+
+ # Sort the checkout list by expiration date
+ sort: ->
+ @checkouts = _.sortBy @checkouts, 'expires'
+ return @
+
+
+ class Interests
+ constructor: (h, c) ->
+ return {
+ nHolds: h.totalItems
+ nHoldsReady: h.ready.forCheckout
+ nCheckouts: c.totalItems
+ nCheckoutsReady: c.totalCheckouts
+ ofHolds: h.holds
+ ofCheckouts: c.checkouts
+ # The following property is a map from product ID to a hold or
+ # a checkout object, eg, interests.byID(124)
+ byID: do (hs = h.holds, cs = c.checkouts) ->
+ byID = {}
+ for v, n in hs
+ v.type = 'hold'
+ byID[v.reserveId] = v
+ for v, n in cs
+ v.type = 'checkout'
+ byID[v.reserveId] = v
+ return byID
+ }
+
+ return {
+ Metadata: Metadata
+ Availability: Availability
+ Holds: Holds
+ Checkouts: Checkouts
+ Interests: Interests
+ }