Fix handling of errors for downloading Overdrive Read format
[sitka/overdrive-evergreen-opac.git] / src / overdrive.coffee
CommitLineData
8ebc3fff
SC
1# TODO memory leaks
2#
3# TODO Author/Title links could specify ebook filter
4#
5# TODO If logged in, could bypass place hold page and use action dialogue directly
6#
7# TODO Simple, cheap two-way data binding:
8# We could publish a partial request object as an abstract way of making
9# an API request, ie, od.$.triggerHandler 'od.metadata', id: id
10# Subscribe to same event to receive reply object, ie,
11# od.$.on 'od.metadata', (ev, reply) -> # do something with reply
12
13require.config
14
15 paths:
16 jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
17 'jquery-ui': 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min'
18 lodash: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min'
19 moment: 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min'
20 cookies: 'https://cdnjs.cloudflare.com/ajax/libs/Cookies.js/0.3.1/cookies.min'
21 json: 'https://cdnjs.cloudflare.com/ajax/libs/json3/3.3.0/json3.min'
22
23 waitSeconds: 120
24
25require [
26 'jquery'
27 'lodash'
28 'cookies'
29 'od_api'
2221a640 30 'od_config'
8ebc3fff
SC
31 'od_pages_opac'
32 'od_pages_myopac'
33 'od_action'
2221a640 34], ($, _, C, od, config) ->
8ebc3fff
SC
35
36 # Indicate the logged in status; the value is determined within document
37 # ready handler.
38 logged_in = false
39
40 # Various debugging functions; not used in production
41 log_page = -> console.log window.location.pathname
42 notify = (what) -> console.log "#{what} is in progress"
43 failed = (what) -> console.log "#{what} failed"
44 reload_page = -> window.location.reload true
45 replace_page = (href) -> window.location.replace href
46
47 # Query a search string of the current page for the value or existence of a
48 # property
49 search_params = (p) ->
895eed6f
SC
50 # Convert for example, '?a=1&b=2' to { a:1, b:2 }.
51 # Also, convert any pluses to spaces.
8ebc3fff 52 o =
cef83683 53 if xs = (decodeURIComponent window.location.search)?.split('?')?[1]?.split(/&|;/)
895eed6f 54 _.zipObject( x.replace(/\+/g, ' ').split('=') for x in xs )
8ebc3fff
SC
55 else
56 {}
57 # Return either the value of a specific property, whether the property
58 # exists, or the whole object
59 if arguments.length is 1 then o[p] or o.hasOwnProperty p else o
60
61
62 # Return an abbreviation of the pathname of the current page,
63 # eg, if window.location.pathname equals 'eg/opac/record' or
64 # 'eg/opac/record/123', then return 'record', otherwise return ''
65 page_name = ->
66 xs = window.location.pathname.match /eg\/opac\/(.+)/
67 if xs then xs[1].replace /\/\d+/, '' else ''
68
8ebc3fff
SC
69 # Routing table: map an URL pattern to a handler that will perform actions
70 # or modify areas on the screen.
71 routes =
72
73 # Scan through property names and execute the function value if the
74 # name pattern matches against the window.location.pathname, eg,
75 # routes.handle(). handle() does not try to execute itself. Returns a
76 # list of results for each handler that was executed. A result is
77 # undefined if no subscriptions to an OD service was needed.
78 handle: (p = window.location.pathname) ->
c6c9faec 79 for own n, v of @ when n isnt 'handle'
8ebc3fff
SC
80 v() if (new RegExp n).test p
81
82 'eg\/opac': ->
83
84 # Add a new dashboard to show total counts of e-items.
85 # Start the dashboard w/ zero counts.
86 $dash = $('#dash_wrapper')._dashboard()
87
88 od.$.on
89
90 # Set the dashboard counts to summarize the patron's account
91 'od.interests': (ev, x) -> $dash._dashboard
92 ncheckouts: x.nCheckouts
93 nholds: x.nHolds
94 nholdsready: x.nHoldsReady
95
96 # Decrement the dashboard counts because an item has been
97 # removed from the holds or checkout list
98 'od.hold.delete': -> $dash._dashboard nholds: -1
99 'od.checkout.delete': -> $dash._dashboard ncheckouts: -1
100
101 # Log out of EG if we are logged in and if an OD patron access
102 # token seems to have expired
103 'od.logout': (ev, x) ->
104 if x is 'od'
d0717859 105 window.location.replace '/eg/opac/logout' if logged_in
8ebc3fff
SC
106
107 'opac\/myopac': ( this_page = page_name() ) ->
108
109 # Add a new tab for e-items to the current page if it is showing a
110 # system of tabs
111 $('#acct_holds_tabs, #acct_checked_tabs')._etabs this_page, search_params 'e_items'
112 # Relabel history tabs if they are showing on current page
113 $('#tab_holds_history, #tab_circs_history')._tab_history()
114 return
115
116 'opac\/home': ->
117
118 # Signal that EG may have logged out
119 od.$.triggerHandler 'od.logout', 'eg' unless logged_in
120
121 'opac\/login': ->
122
123 # On submitting the login form, we initiate the login sequence with the
124 # username/password from the login form
125 $('form', '#login-form-box').one 'submit', ->
126 od.login
127 username: $('[name=username]').val()
128 password: $('[name=password]').val()
129
130
131 # TODO In order to perform OD login after EG login, we could
132 # automatically get the prefs page and scrape the barcode value,
133 # but in the general case, we would also need the password value
134 # that was previously submitted on the login page.
135
136 # We could scrape the barcode value from the prefs page by having it
137 # being parsed into DOM within an iframe (using an inscrutable sequence
138 # of DOM traversal). Unfortunately, it will reload script tags and
139 # make XHR calls unnecessarily.
140 #
141 # The alternative is to GET the prefs page and parse the HTML string
142 # directly for the barcode value, but admittedly, we need to use an
143 # inscrutable regex pattern.
144
145 # On the myopac account summary area, add links to hold list and
146 # checkout list of e-items
147 'myopac\/main': ( $table = $('.acct_sum_table') ) ->
148 return unless $table.length
149
150 totals = $table._account_summary()
151
152 od.$.on 'od.interests', (ev, x) ->
153
154 $table._account_summary
155 ncheckouts: totals[0]
156 nholds: totals[1]
157 nready: totals[2]
158 n_checkouts: x.nCheckouts
159 n_holds: x.nHolds
160 n_ready: x.nHoldsReady
161
162 # Each time the patron's preferences page is shown, publish values that
163 # might have changed because the patron has edited them. Example
164 # scenario: patron changes email address on the prefs page and then
165 # places hold, expecting the place hold form to default to the newer
166 # address.
167 'myopac\/prefs': ->
168 $tr = $('#myopac_summary_tbody > tr')
169 em = $tr.eq(6).find('td').eq(1).text()
170 bc = $tr.eq(7).find('td').eq(1).text()
171 hl = $tr.eq(8).find('td').eq(1).text()
172 od.$.triggerHandler 'od.prefs', email_address:em, barcode:bc, home_library:hl
173
174 'opac\/results': (interested = {}) ->
175
176 # List of hrefs which correspond to Overdrive e-items
177 # TODO this list is duplicated in module od_pages_opac
178 hrefs = [
179 'a[href*="downloads.bclibrary.ca"]' # Used in OPAC
180 'a[href*="elm.lib.overdrive.com"]' # Used in XUL staff client
181 ]
182
183 # Prepare each row of the results table which has an embedded
184 # Overdrive product ID. A list of Overdrive product IDs is
185 # returned, which can be used to find each row directly.
186 ids = $(hrefs.join ',').closest('.result_table_row')._results()
187 return if ids?.length is 0
188
189 od.$.on
190
191 # When patron holds and checkouts become available...
192 'od.interests': (ev, x) ->
193
194 # Initiate request for each Overdrive product ID
195 for id in ids
8ebc3fff 196 od.apiAvailability id: id
e07737bf
SC
197 # If the user isn't logged in, the format list will
198 # always be found from Metadata
199 od.apiMetadata id: id unless logged_in
8ebc3fff
SC
200
201 # Cache the relationship between product IDs and patron
202 # holds and checkouts, ie, has the patron placed a hold on
203 # an ID or checked out an ID?
204 interested = x.byID
205
206 # Fill in format values when they become available
207 'od.metadata': (ev, x) -> $("##{x.id}")._results_meta x
208
209 # Fill in availability values when they become available
210 'od.availability': (ev, x) ->
211 $("##{x.id}")
212 ._results_avail x
213 ._replace_place_hold_link x, interested[x.id]?.type
214
e07737bf
SC
215 # Irregular logic warning: If the user is logged in, the
216 # format list might be found from Availability if the item
217 # is available for checkout, otherwise it will be found
218 # from Metadata.
219 if logged_in
220 if x.available then $("##{x.id}")._results_meta x else od.apiMetadata id: x.id
221
222
8ebc3fff
SC
223 'opac\/record': (interested = {}) ->
224
225 # Add an empty container of format and availability values
226 return unless id = $('div.rdetail_uris')._record()
227
228 od.$.on
229
230 # When patron holds and checkouts become available...
231 'od.interests': (ev, x) ->
232
233 # Initiate request for metadata and availability values when
e07737bf 234 od.apiMetadata id: id unless logged_in
8ebc3fff
SC
235 od.apiAvailability id: id
236
237 # Has the user placed a hold on an ID or checked out an ID?
238 interested = x.byID
239
240 # Fill in format values when they become available
241 'od.metadata': (ev, x) -> $("##{x.id}")._record_meta x
242
243 # Fill in availability values when they become available
244 'od.availability': (ev, x) ->
245 $("##{x.id}")._record_avail x
246 $('#rdetail_actions_div')._replace_place_hold_link x, interested[x.id]?.type
247
e07737bf
SC
248 if logged_in
249 if x.available then $("##{x.id}")._record_meta x else od.apiMetadata id: x.id
250
8ebc3fff
SC
251 # For the case where the patron is trying to place a hold if not logged
252 # in, there is a loophole in the Availability API; if using a patron
253 # access token and patron already has a hold on it, avail.actions.hold
254 # will still be present, falsely indicating that patron may place a
255 # hold, which will lead to a server error. The same situation will
256 # occur if patron has already checked out. It seems the OD server does
257 # not check the status of the item wrt the patron before generating the
258 # server response.
259 #
260 # To fix the problem, we will check if avail.id is already held or
261 # checked out, and if so, then go back history two pages so that
262 # original result list or record page is shown, with the proper action
263 # link generated when the page reloads.
264
265 # Replace the original Place Hold form with a table row to show
266 # available actions, either 'Check out' or 'Place hold', depending on
267 # whether the item is available or not, respectively.
268 #
269 # The following page handler does not replace the place_hold page, but
270 # is meant to be called by the place hold link.
271 # If the place_hold page is encountered, the handler will return
272 # without doing anything, because no id is passed in.
273 'opac\/place_hold': (id, interested = {}) ->
274 return unless id
275
276 $('#myopac_holds_div')._replace_title 'Place E-Item on Hold'
277 $('#myopac_checked_div')._replace_title 'Check out E-Item'
278
279 $('#holds_main, #checked_main, .warning_box').remove()
280
281 $div = $('<div id="#holds_main">')
282 ._holds_main() # Add an empty table
283 ._holdings_row id # Add an empty row
284 .appendTo $('#myopac_holds_div, #myopac_checked_div')
285
07895f87 286 # Fill in empty row when data becomes available
8ebc3fff
SC
287 od.$.on
288
289 'od.interests': (ev, x) ->
e07737bf 290
8ebc3fff
SC
291 # Has the user placed a hold on an ID or checked out an ID?
292 interested = x.byID
293
e07737bf
SC
294 $.when(
295 od.apiMetadata id: id
296 od.apiAvailability id: id
297 )
298 .then (x, y) ->
299
300 # Check if this patron has checked out or placed a hold on
301 # avail.id and if so, then go back two pages to the result list
302 # or record page. The page being skipped over is the login page
303 # that comes up because the user needs to log in before being
304 # able to see the place hold page. Thus, the logic is only
305 # relevant if the user has not logged in before trying to place
306 # a hold.
307 if interested[y.id]?.type
308 window.history.go -2
309
310 else
311 $("##{x.id}")._row_meta(x, 'thumbnail', 'title', 'author')
312 $("##{x.id}")._row_meta (if y.available then y else x), 'formats'
313
314 $("##{y.id}")
315 # Fill in availability column
316 ._holdings_row_avail y
317 # Auto-focus on place hold or checkout button
318 .find '.opac-button.hold, .opac-button.checkout'
319 .focus()
320 .end()
8ebc3fff
SC
321
322 'myopac\/holds': ->
323
324 # If we arrive here with an interested ID value, we are intending
325 # to place a hold on an e-item
326 if id = search_params 'interested'
327 return routes['opac\/place_hold'] id
328
329 # Rewrite the text in the warning box to distinguish physical items from e-items
330 unless search_params 'e_items'
331 $('.warning_box').text $('.warning_box').text().replace ' holds', ' physical holds'
332 return
333
334 return unless ($holds_div = $('#myopac_holds_div')).length
335
336 $holds_div._replace_title 'Current E-Items on Hold'
337
338 $('#holds_main, .warning_box').remove()
339
340 # Replace with an empty table for a list of holds for e-items
341 $div = $('<div id="#holds_main">')
342 ._holds_main()
343 .appendTo $holds_div
344
345 # Subscribe to notifications of relevant data objects
346 od.$.on
347
348 'od.interests': (ev, x) ->
349
350 # Focus on patron's hold interests, and if the search
351 # parameters say so, further focus on holds of items that
352 # are ready to be checked out
353 holds = x?.ofHolds
354 holds = _.filter(holds, (x) -> x.actions.checkout) if search_params 'available'
355
356 # Add an empty list of holds
357 ids = $div._holds_rows holds
358
359 # Try to get the metadata and availability values for
360 # this hold
361 for id in ids
362 od.apiMetadata id: id
363 od.apiAvailability id: id
364
365 # Add metadata values to a hold
366 'od.metadata': (ev, x) -> $("##{x.id}")._row_meta x, 'thumbnail', 'title', 'author', 'formats'
367 # Add availability values to a hold
368 'od.availability': (ev, x) -> $("##{x.id}")._holds_row_avail x
369
9669700c
SC
370 'od.hold.update': (ev, x) ->
371 x = x.holds[0]
372 $("##{x.reserveId}")._holds_row x
373
374 'od.hold.delete': (ev, id) -> $("##{id}").remove()
8ebc3fff
SC
375
376 'myopac\/circs': ->
377
378 # If we arrive here with an interested ID value, we are intending
379 # to checking out an e-item
380 if id = search_params 'interested'
381 return routes['opac\/place_hold'] id
382
3865fb23
SC
383 # If there is an error message embedded in the search parameters
384 # that resulted from an attempt at downloading content, show it in
385 # a floating notification box.
639929ef
SC
386 if code = search_params 'read_error'
387 $('<div>')._notify 'Error message', """
388 <p>Could not get content</p>
389 <div>Error code: #{code}</div>
390 <div>Reserve ID: #{search_params 'reserveid'}</div>
391 """
392 else if code = search_params 'ErrorCode'
3865fb23
SC
393 $('<div>')._notify 'Error message', """
394 <p>#{search_params 'ErrorDescription'}</p>
639929ef
SC
395 <div>Error code: #{code}</div>
396 <div>Details: #{search_params 'ErrorDetails'}</div>
3865fb23
SC
397 """
398
8ebc3fff
SC
399 # Rewrite the text in the warning box to distinguish physical items from e-items
400 unless search_params 'e_items'
401 $('.warning_box').text $('.warning_box').text().replace ' items', ' physical items'
402 return
403
404 return unless ($checked_div = $('#myopac_checked_div')).length
405
406 $checked_div._replace_title 'Current E-Items Checked Out'
407
408 $('#checked_main, .warning_box').remove()
409
410 # Build an empty table for a list of checkouts of e-items
411 $div = $('<div id="#checked_main">')
412 ._checkouts_main()
413 .appendTo $checked_div
414
415 # Subscribe to notifications of relevant data objects
416 od.$.on
417
418 'od.interests': (ev, x) ->
419
420 # Fill in checkout list
421 ids = $div._checkouts_rows x?.ofCheckouts
422
423 # Try to get metadata values for these checkouts
424 od.apiMetadata id: id for id in ids
425
426 # Add metadata values to a checkout
427 'od.metadata': (ev, x) -> $("##{x.id}")._row_meta x, 'thumbnail', 'title', 'author'
428
9669700c
SC
429 'od.checkout.update': (ev, x) ->
430 x = x.checkouts[0]
431 $("##{x.reserveId}")._row_checkout x
432
433 'od.checkout.delete': (ev, id) -> $("##{id}").remove()
8ebc3fff
SC
434
435 # Begin sequence after the DOM is ready...
436 $ ->
437
438 return if window.IAMXUL # Comment out to run inside XUL staff client
439
2221a640
SC
440 # Do not implement if hostname is blacklisted
441 return if config.blacklisted()
442
8ebc3fff
SC
443 # We are logged into EG if indicated by a cookie or if running
444 # inside XUL staff client.
445 logged_in = Boolean C('eg_loggedin') or window.IAMXUL
446
447 # Dispatch handlers corresponding to the current location
448 # and return immediately if none of them require OD services
449 return if _.every routes.handle() , (r) -> r is undefined
450
451 # Try to get library account info
9669700c 452 od.apiLibraryInfo()
8ebc3fff
SC
453
454 # If we are logged in, we 'compute' the patron's interests in product
455 # IDs; otherwise, we set patron interests to an empty object.
456 .then ->
457
458 # If logged in, ensure that we have a patron access token from OD
459 # before getting patron's 'interests'
460 if logged_in
461 od.login().then od.apiInterestsGet
462
463 # Otherwise, return no interests
464 # TODO should do the following in od_api module
465 else
466 interests = byID: {}
467 od.$.triggerHandler 'od.interests', interests
468 return interests
469
470 return
471 return