dd7673df87bd48ccdad3c950451152e936bf7d91
[sitka/overdrive-evergreen-opac.git] / src / od_data.coffee
1 define [
2         'lodash'
3         'moment'
4 ], (
5         _
6         M
7 ) ->
8
9         # A base class defining utilitarian methods
10         class U
11                 constructor: (x) ->
12                         return unless x
13                         t = @
14                         t extends x
15                         return
16
17                 # Mutate an ISO 8601 date string into a Moment object.  If the argument is
18                 # just a date value, then it specifies an absolute date in ISO 8601 format.
19                 # If the argument is a pair, then it specifies a date relative to now.  For
20                 # an ISO 8601 date, we correct for what seems to be an error in time zone,
21                 # Zulu time is really East Coast time.
22                 momentize: (date, unit) ->
23                         switch arguments.length
24                                 when 1
25                                         if date then M(date.replace /Z$/, '-0400') else M()
26                                 when 2
27                                         if date then M().add date, unit else M()
28                                 else M()
29
30                 # The URL endpoint is converted to its reverse proxy version,
31                 # because we are using the Evergreen server as a reverse proxy to
32                 # the Overdrive server.
33                 proxy: (x) ->
34                         return unless x
35                         y = x
36                         y = y.replace 'https://', '//'
37                         y = y.replace 'http://' , '//'
38                         y = y.replace '//oauth-patron.overdrive.com', '/od/oauth-patron'
39                         y = y.replace        '//oauth.overdrive.com', '/od/oauth'
40                         y = y.replace   '//patron.api.overdrive.com', '/od/api-patron'
41                         y = y.replace          '//api.overdrive.com', '/od/api'
42                         y = y.replace  '//images.contentreserve.com', '/od/images'
43                         y = y.replace '//fulfill.contentreserve.com', '/od/fulfill'
44                         #log "proxy #{x} -> #{y}"
45                         y
46                 proxies: (x) ->
47                         (v.href = @proxy l) for n, v of x when l = v.href
48                         return x
49
50
51         class Metadata extends U
52                 constructor: (x) ->
53                         super x
54
55                         # Convert ID to upper case to match same case found in EG catalogue
56                         @id = @id.toUpperCase()
57                         # Provide a simplified notion of author: first name in creators
58                         # list having a role of author
59                         @author = (v.name for v in @creators when v.role is 'Author')[0] or ''
60                         # Convert image links to use reverse proxy
61                         @proxies @images
62
63                         return
64
65
66         class Availability extends U
67                 constructor: (x, email_address) ->
68                         super x
69
70                         @zero()
71                         @hold email_address if @actions?.hold
72                         @proxies @actions if @actions?
73                         @action_formats()
74
75                         return @
76
77                 # Add zero values
78                 zero: ->
79                         @copiesOwned     = 0 unless @copiesOwned
80                         @copiesAvailable = 0 unless @copiesAvailable
81                         @numberOfHolds   = 0 unless @numberOfHolds
82                         return @
83
84                 hold: (email_address) ->
85                         # The reserve ID is empty in the actions.hold.fields; we have to fill it ourselves.
86                         _.where(@actions.hold.fields, name: 'reserveId')[0].value = @id
87                         # We jam the email address from the prefs page into the fields object from the server
88                         # so that the new form will display it.
89                         if email_address
90                                 _.where(@actions.hold.fields, name: 'emailAddress')[0].value = email_address
91                         return @
92
93                 # Surface the format options list that might be buried in an actions object
94                 action_formats: ->
95                         xs = @actions?.checkout?.fields
96                         return @ unless xs?.length > 0
97                         break for x in xs when x.name is 'formatType'
98                         return @ unless x.options.length > 0
99                         @formats = ( { id: v, name: '' } for v in x.options )
100                         return @
101
102
103         class Holds extends U
104                 constructor: (x) ->
105                         super x
106
107                         @add()
108                         .remove()
109                         .proxy_urls()
110                         .moments()
111                         .count()
112                         .sort()
113
114                         return
115
116                 # Ensure there is always a holds list, even if it's empty
117                 add: ->
118                         @holds = [] if @holds is undefined
119                         return @
120
121                 # Delete action to release a suspension if a hold is not
122                 # suspended, because such actions are redundant
123                 remove: ->
124                         delete x.actions.releaseSuspension for x in @holds when not x.holdSuspension
125                         return @
126
127                 proxy_urls: ->
128                         (@proxies v.actions) for v, n in @holds
129                         return @
130                         
131                 # For each hold, convert any ISO 8601 date strings into a
132                 # Moment object (at local time zone)
133                 moments: ->
134                         for x in @holds
135                                 x.holdPlacedDate = @momentize x.holdPlacedDate
136                                 x.holdExpires = @momentize x.holdExpires
137                                 if x.holdSuspension
138                                         x.holdSuspension.numberOfDays = @momentize x.holdSuspension.numberOfDays, 'days'
139                         return @
140
141                 # Count the number of holds that can be checked out now
142                 count: ->
143                         @ready = _.countBy @holds, (x) -> if x.actions.checkout then 'forCheckout' else 'other'
144                         @ready.forCheckout = 0 unless @ready.forCheckout
145                         return @
146
147                 # Sort the holds list by position and placed date
148                 # and sort ready holds first
149                 sort: ->
150                         @holds = _(@holds)
151                                 .sortBy ['holdListPosition', 'holdPlacedDate']
152                                 .sortBy (x) -> x.actions.checkout
153                                 .value()
154                         return @
155
156
157         class Checkouts extends U
158                 constructor: (x) ->
159                         super x
160
161                         @add()
162                         .proxy_urls()
163                         .moments()
164                         .sort()
165
166                         return
167
168                 # Ensure there is always a checkouts list, even if it's empty
169                 add: ->
170                         @checkouts = [] if @checkouts is undefined
171                         return @
172
173                 proxy_urls: ->
174                         for x in @checkouts
175                                 @proxies x.actions
176                                 @proxies y.linkTemplates for y in x.formats
177                         return @
178
179                 # For each checkout, convert any ISO 8601 date strings into a
180                 # Moment object (at local time zone)
181                 moments: ->
182                         for x in @checkouts
183                                 x.expires = @momentize x.expires
184                         return @
185
186                 # Sort the checkout list by expiration date
187                 sort: ->
188                         @checkouts = _.sortBy @checkouts, 'expires'
189                         return @
190
191
192         class Interests
193                 constructor: (h, c) ->
194                         return {
195                                 nHolds: h.totalItems
196                                 nHoldsReady: h.ready.forCheckout
197                                 nCheckouts: c.totalItems
198                                 nCheckoutsReady: c.totalCheckouts
199                                 ofHolds: h.holds
200                                 ofCheckouts: c.checkouts
201                                 # The following property is a map from product ID to a hold or
202                                 # a checkout object, eg, interests.byID(124)
203                                 byID: do (hs = h.holds, cs = c.checkouts) ->
204                                         byID = {}
205                                         for v, n in hs
206                                                 v.type = 'hold'
207                                                 byID[v.reserveId] = v
208                                         for v, n in cs
209                                                 v.type = 'checkout'
210                                                 byID[v.reserveId] = v
211                                         return byID
212                         }
213         
214         return {
215                 Metadata:     Metadata
216                 Availability: Availability
217                 Holds:        Holds
218                 Checkouts:    Checkouts
219                 Interests:    Interests
220         }