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