Commits

Rich Manalang [Atlassian] committed 1d5f3ac

cleaned up AMD, added minified version, and added a build script

Comments (0)

Files changed (56)

 "Travel Request." In addition, this app requires five custom fields to be
 created to store data required by the travel approval process.
 
-For now all the custom field names and project id is hard-coded in `src/main/resources/coffee/config.coffee`.
+For now all the custom field names and project id is hard-coded in [`src/main/resources/app/config.coffee`](https://bitbucket.org/atlassian/jira-travel-microapp/src/master/src/main/resources/app/config.coffee).
 In the future, we will utilize Jonathan Doklovic's workflow and project
 importer in order to bootstrap the creation of the required project, custom
 fields, issue type and workflow.
 
 Check out the CoffeeScript code under:
 
-    src/main/resources/coffee
+    src/main/resources/app
 
-Read the `config.coffee` to learn about how to set up JIRA for this to work. This
+Read the [`config.coffee`](https://bitbucket.org/atlassian/jira-travel-microapp/src/master/src/main/resources/app/config.coffee) to learn about how to set up JIRA for this to work. This
 app is a single page app that uses [Backbonejs](http://backbonejs.org/), 
 [CoffeeScript](http://coffeescript.org/), and [Requirejs](http://requirejs.org/).
 
+#!/bin/sh
+r.js -o baseUrl=./src/main/resources/js/ name=almond include=main out=./src/main/resources/js/main.min.js wrap=true

src/main/resources/app/app.coffee

+define (require)->
+
+  config                  = require 'config'
+  helpers                 = require 'helpers'
+
+  UserModel               = require 'models/user_model'
+  CurrentUserModel        = require 'models/current_user_model'
+  ProjectModel            = require 'models/project_model'
+  SavedTripModel          = require 'models/saved_trip_model'
+  TripModel               = require 'models/trip_model'
+
+  CustomFieldsCollection  = require 'collections/custom_fields_collection'
+  IssueTypesCollection    = require 'collections/issue_types_collection'
+  TripsCollection         = require 'collections/trips_collection'
+  UsersCollection         = require 'collections/users_collection'
+
+  AddTripView             = require 'views/add_trip_view'
+  FlashView               = require 'views/flash_view'
+  MyTripView              = require 'views/my_trip_view'
+  MyTripsView             = require 'views/my_trips_view'
+  PendingTripsView        = require 'views/pending_trips_view'
+
+  Router                  = require 'router'
+  
+  # config needs to be global because it's referenced templates
+  window.config = config
+
+  currentUser = new CurrentUserModel
+  trips = new TripsCollection
+  project = new ProjectModel
+  issuetype = new IssueTypesCollection
+  customFields = new CustomFieldsCollection
+
+  addTripView = new AddTripView 
+    collection: trips
+
+  trips.fetch().done ->
+    myTripsView = new MyTripsView 
+      collection: trips
+
+    pendingTripsView = new PendingTripsView 
+      collection: trips
+
+  appRouter = new Router
+    addTripView: addTripView
+
+  Backbone.history.start()

src/main/resources/app/collections/custom_fields_collection.coffee

+define (require)->
+
+  config = require '../config'
+
+  class CustomFieldsCollection extends Backbone.Collection
+    url: "#{config.restPath}/api/2/field"
+    initialize: ->
+      @fetch().done @setFields
+    setFields: =>
+      _.each config.customFields, (cf) =>
+        field = @find (f) =>
+          f.attributes.name == cf.name
+        _.extend cf, field.attributes

src/main/resources/app/collections/issue_types_collection.coffee

+define (require)->
+
+  config = require '../config'
+
+  class IssueTypesCollection extends Backbone.Collection
+    url: "#{config.restPath}/api/2/issuetype"
+    initialize: ->
+      @fetch().done @findTravelReqIssueType
+    findTravelReqIssueType: =>
+      issueType = @find (it) ->
+        it.attributes.name == config.issueTypeName
+      @id = issueType.attributes.id

src/main/resources/app/collections/trips_collection.coffee

+define (require)->
+
+  config    = require '../config'
+  TripModel = require '../models/trip_model'
+
+  class TripsCollection extends Backbone.Collection
+    url: "#{config.restPath}/api/2/search"
+
+    model: TripModel
+
+    comparator: (a,b) =>
+      a.get(config.customFields.departDate.id) < b.get(config.customFields.returnDate.id)
+
+    parse: (d) ->
+      _.map d.issues, (i) =>
+        _.extend i.fields,
+          id: i.id
+          key: i.key

src/main/resources/app/collections/users_collection.coffee

+define (require)->
+
+  config = require '../config'
+    
+  class UsersCollection extends Backbone.Collection
+    url: (opts) =>
+      "#{config.restPath}/api/2/user/search?username=#{@q}"
+    initialize: (options) ->
+      @q = options.q
+      @fetch(options).done (d) =>
+        options.callback(d) if options.callback
+

src/main/resources/app/config.coffee

+# JIRA Travel Approval microapp
+# =============================
+# Before you can use this, you'll have to perform the following steps in your JIRA instance:
+#
+#     1. Create a new project to store your travel requests. The default project key used by this app is "TRAVEL"
+#     2. Create 5 new custom fields:
+#       1. Origin (string)
+#       2. Destination (string)
+#       3. Depart Date (date)
+#       4. Return Date (date)
+#       5. Purchase Amount (float)
+#       6. Currency (string)
+#     3. Create a issue type called "Travel Request"
+#     4. Create a custom workflow with four new statuses:
+#       1. Open
+#       2. Approved
+#       3. Denied
+#       4. Booked
+#
+# How you manage the transitions between those four statuses is up to you. You can modify any additional
+# configurations in the object below.
+
+define
+
+  # Specify your JIRA config here:
+  restPath:           '/jira/rest' # This is the relative path to the rest api (http://localhost[/jira/rest]/api/2/...
+  projectKey:         'TRAVEL' # This is the project key where travel approvals will be created under
+  issueTypeName:      'Travel Request' # Name of the Issue Type you want issues to be created with
+  bookTransitionName: 'Book' # Name of the transition you want to use to "book" travel requests
+  customFields: # These are the names of your custom fields. We'll match them with the actual custom field at runtime
+    origin:
+      name: "Origin"
+    destination:
+      name: "Destination"
+    departDate:
+      name: "Depart Date"
+    returnDate:
+      name: "Return Date"
+    purchaseAmt:
+      name: "Purchase Amount"
+    currency:
+      name: "Currency"

src/main/resources/app/helpers.coffee

+define ->
+  
+  # used inside template to assign button class based on text value
+  window.statusButton = (status) ->
+    switch status
+      when 'Open' then 'btn-info'
+      when 'Approved' then 'btn-success'
+      when 'Denied' then 'btn-danger'
+      when 'Booked' then 'btn-inverse'
+
+  # JSONP handler for exchange rate conversion
+  window.parseExchangeRate = (data) ->
+    name = data.query.results.row.name
+    rate = parseFloat data.query.results.row.rate, 10
+    $('#costin').html (parseFloat $('#cost').val() * rate)
+      .round(2)
+      .format(2) + 
+      " AUD"
+
+  # JSONP handler for place name lookup
+  window.handlePlaceJSONP = (d) ->
+    window.typeahead.process _.map d, (place)->
+      place.replace /NS/, "NSW"

src/main/resources/app/main.coffee

+require [
+  'app'
+]

src/main/resources/app/models/current_user_model.coffee

+define (require)->
+
+  config    = require '../config'
+  UserModel = require './user_model'
+    
+  class CurrentUserModel extends Backbone.Model
+    url: "#{config.restPath}/auth/1/session"
+
+    initialize: ->
+      @fetch()
+
+    fetch: ->
+      super.done =>
+        new UserModel
+          id        : @get 'name'
+          callback  : (d) =>
+            @set 'profile', d
+            @setLoggedIn()
+
+    setLoggedIn: ->
+      $('.curruser').html @get('profile').displayName
+      $('.avatar').attr('src', @get('profile').avatarUrls["48x48"])

src/main/resources/app/models/project_model.coffee

+define (require)->
+
+  config = require '../config'
+
+  class ProjectModel extends Backbone.Model
+    url: "#{config.restPath}/api/2/project/#{config.projectKey}"
+    initialize: ->
+      @fetch().done (d)->
+        @id = d.id

src/main/resources/app/models/saved_trip_model.coffee

+define (require)->
+
+  config = require '../config'
+    
+  class SavedTripModel extends Backbone.Model
+    url: =>
+      "#{config.restPath}/api/2/issue/#{@key}"
+
+    initialize: (options) ->
+      @key = if options and options.key then options.key else ""
+      if @key
+        @fetch(options).done (d) =>
+          options.callback(d) if options.callback
+
+    parse: (d) ->
+      if d.fields
+        _.extend d.fields,
+          id: d.id
+          key: d.key
+      else
+        d

src/main/resources/app/models/trip_model.coffee

+define (require)->
+
+  config = require '../config'
+    
+  class TripModel extends Backbone.Model
+    url: =>
+      "#{config.restPath}/api/2/issue"
+
+    initialize: (options) ->
+      @errors = []
+      @bind 'error', (model, errs) =>
+        console.log 'TripModel Error', arguments
+        @errors = errs 
+        errs
+
+    parse: (d) ->
+      if d.fields
+        _.extend d.fields,
+          id: d.id
+          key: d.key
+      else
+        d
+
+    validate:
+      summary:
+        required: true
+      description:
+        required: true
+      origin:
+        required: true
+      destination:
+        required: true
+      departDate:
+        required: true
+      returnDate:
+        required: true
+      purchaseAmt:
+        required: true
+      currency:
+        required: true
+      assignee:
+        required: true

src/main/resources/app/models/user_model.coffee

+define (require)->
+
+  config = require '../config'
+    
+  class UserModel extends Backbone.Model
+    url: =>
+      "#{config.restPath}/api/2/user?username=#{@id}"
+    initialize: (options) ->
+      @fetch(options).done (d) =>
+        options.callback d if options.callback

src/main/resources/app/router.coffee

+define ->
+  
+  class Router extends Backbone.Router
+    routes:
+      ''          : 'addTrip'
+      'trips'     : 'myTrips'
+      'trip/book' : 'bookTrip'
+      'trip/new'  : 'addTrip'
+      '*path'     : 'notFound'
+
+    initialize: (options)->
+      @addTripView = options.addTripView
+
+    before: ->
+      $('.flash .alert').remove()
+
+    activate: (id) ->
+      $('.content').addClass 'hidden'
+      $('#'+id).removeClass 'hidden'
+      $('.topnav').removeClass 'active'
+      $('.'+id).addClass 'active'
+      return @
+
+    addTrip: =>
+      @activate 'addtrip'
+      @addTripView.focus()
+
+    myTrips: ->
+      @activate 'mytrips'
+
+    bookTrip: ->
+      @activate 'booktrip'
+
+    notFound: ->
+      @activate '404'

src/main/resources/app/views/add_trip_view.coffee

+define (require)->
+  
+  config          = require '../config'
+  TripModel       = require '../models/trip_model'
+  SavedTripModel  = require '../models/saved_trip_model'
+  UsersCollection = require '../collections/users_collection'
+    
+  class AddTripView extends Backbone.View
+    el: $ '#addtrip'
+
+    template: _.template $('#addtrip-template').html()
+
+    initialize: (options) ->
+      @render()
+
+    events:
+      # 'changeDate .date':       'renderFriendlyDate'
+      'change #cost':           'updateConvertedRate'
+      'change #curr':           'updateConvertedRate'
+      'submit #add-trip-form':  'saveTrip'
+
+    render: =>
+      @$el.html @.template()
+
+      $('#depart-date').data('date', Date.create().format("{M}/{dd}/{yyyy}"))
+      $('#return-date').data('date', Date.create().addDays(7).format("{M}/{dd}/{yyyy}"))
+
+      $('div.date').datepicker()
+
+      $('.place.from').typeahead
+        property: "origin"
+        source: (typeahead, query) =>
+          console.log 1, @
+          return if query.length < 3
+          window.typeahead = typeahead;
+          @getPlace(query)
+
+      $('.place.to').typeahead
+        property: "destination"
+        source: (typeahead, query) =>
+          return if query.length < 3
+          window.typeahead = typeahead;
+          @getPlace(query)
+
+      $('#assignee').typeahead
+        # property: "assignee"
+        source: (typeahead, query) =>
+          # _.map people.items, (o) ->
+          #   o.label
+          return if query.length < 3
+          new UsersCollection
+            q: query
+            callback: (d) =>
+              # console.log 3,d,_.pluck(d, 'name')
+              typeahead.process _.pluck(d, 'name')
+
+      $('#cost, #curr').on('change keyup', =>)
+      @
+
+    getRate: (from, to) ->
+      $('#currscript').remove()
+      script = document.createElement 'script'
+      script.setAttribute 'src', 
+        "http://query.yahooapis.com/v1/public/yql?q=select%20rate%2Cname%20from%20csv%20where%20url%3D'http%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes%3Fs%3D#{from}#{to}%253DX%26f%3Dl1n'%20and%20columns%3D'rate%2Cname'&format=json&callback=parseExchangeRate"
+      script.setAttribute 'id', 'currscript'
+      document.body.appendChild script
+
+    updateConvertedRate: ->
+      @getRate $('#curr').val(), 'AUD'
+
+    getPlace: (q) ->
+      $('#placescript').remove()
+      script = document.createElement 'script'
+      script.setAttribute 'src',
+        "http://gd.geobytes.com/AutoCompleteCity?q=#{q}&callback=handlePlaceJSONP"
+      script.setAttribute 'id', 'placescript'
+      document.body.appendChild script
+
+    focus: ->
+      focusCb = => @$el.find("input[type='text']:first").focus()
+      setTimeout focusCb, 0
+
+    showInvalid: (model, errs) =>
+      for attr of errs
+        @$el.find('.'+attr).addClass 'error'
+      false
+
+    showSavedToast: ->
+      @flash = new FlashView
+        flash: 
+          title:    'Success'
+          message:  'Your travel approval request has been saved and sent for approval'
+
+    saveTrip: ->
+      @$('.error').removeClass 'error'    
+
+      trip = $('form').serializeObject()
+      trip[config.customFields.departDate.id] = Date.create($('#depart-date input').data('date')).format(Date.ISO8601_DATETIME)
+      trip[config.customFields.returnDate.id] = Date.create($('#return-date input').data('date')).format(Date.ISO8601_DATETIME)
+
+      tripObj =
+        fields:
+          issuetype:
+            id: issuetype.id
+          project:
+            id: project.id
+
+      _.extend tripObj.fields,
+        summary: trip.summary if trip.summary
+      _.extend tripObj.fields,
+        description: trip.description if trip.description
+      tripObj.fields[config.customFields.origin.id] = trip.origin if trip.origin
+      tripObj.fields[config.customFields.destination.id] = trip.destination if trip.destination
+      tripObj.fields[config.customFields.departDate.id] = trip[config.customFields.departDate.id] if trip[config.customFields.departDate.id]
+      tripObj.fields[config.customFields.returnDate.id] = trip[config.customFields.returnDate.id] if trip[config.customFields.returnDate.id]
+      tripObj.fields[config.customFields.purchaseAmt.id] = parseInt(trip.purchaseAmt) if trip.purchaseAmt
+
+      if trip.currency
+        tripObj.fields[config.customFields.currency.id] = {}
+        tripObj.fields[config.customFields.currency.id]['value'] = trip.currency
+
+      _.extend tripObj.fields, if trip.assignee
+        assignee:
+            # name: trip.assignee
+            name: "alex"
+      @model = new TripModel
+      @model.on 'error', @showInvalid
+      @model.set trip
+      if @model.errors.length == 0 
+        rslt = @model.save tripObj,
+          success: (model, d) =>
+            @render()
+            @showSavedToast()
+            console.log 'success', @, arguments
+            savedTrip = new SavedTripModel
+              key: d.key
+              callback: (trip) =>
+                console.log 'callback', savedTrip
+                @collection.add savedTrip
+          error: (model, errs) =>
+            # @showInvalid model, errs
+
+      false

src/main/resources/app/views/flash_view.coffee

+define ->
+  
+  class FlashView extends Backbone.View
+    tagName: "div"
+    
+    className: "alert"
+    
+    template: _.template $('#flash-template').html()
+    
+    initialize: (options) ->
+      @render()
+    
+    render: ->
+      @$el.addClass @options.flash.class if @options.flash.class
+      @$el.html(@template(@options.flash)).prependTo('.flash')

src/main/resources/app/views/my_trip_view.coffee

+define ->
+  
+  class MyTripView extends Backbone.View
+    template: _.template $('#mytrip-template').html()
+
+    tagName: 'tr'
+    
+    initialize: (options) ->
+      @model.bind 'change', @render
+      @model.bind 'destroy', @remove
+
+    render: ->
+      @$el.html @template(@model) if @model
+
+    remove: ->
+      @$el.remove()

src/main/resources/app/views/my_trips_view.coffee

+define (require)->
+
+  MyTripView = require './my_trip_view'
+    
+  class MyTripsView extends Backbone.View
+    el: $ '#mytrips'
+
+    template: _.template $('#mytrips-template').html()
+
+    initialize: (options) ->
+      @collection.on 'add', @addOne
+      @collection.on 'change', @render, @
+      @render()
+
+    render: ->
+      @$el.html @template()
+      @addAll()
+
+    addOne: (model) =>
+      myTripView = new MyTripView({model: model})
+      myTripView.render();
+      @$el.find('#mytrips-rows').prepend myTripView.el
+      model.bind 'remove', myTripView.remove
+
+    addAll: ->
+      @collection.each @addOne
+

src/main/resources/app/views/pending_trips_view.coffee

+define ->
+  
+  class PendingTripsView extends Backbone.View
+    el: $ '#pendingtrips-rows'
+
+    template: _.template $('#pendingtrips-template').html()
+
+    initialize: (options)->
+      @render()
+
+    events:
+      "click .complete-booking": "completeBooking"
+
+    completeBooking: (e) ->
+      el = $(e.currentTarget)
+      trip = el.data()
+      $.get "#{config.restPath}/api/2/issue/#{trip.key}/transitions", (d) =>
+        bookTransition = _.find d.transitions, (t) =>
+          t.name == config.bookTransitionName
+        $.ajax
+          url: "#{config.restPath}/api/2/issue/#{trip.key}/transitions"
+          type: "post"
+          dataType: "json"
+          contentType: "application/json"
+          processData: false
+          data:
+            JSON.stringify
+              transition:
+                id: bookTransition.id
+          success: =>
+            el.removeClass('btn-success').addClass('btn-inverse').addClass('disabled').html('Booked')
+            @collection.get(trip.id).set('status', {name: "Booked"}, {silent: true})
+            @collection.trigger 'change'
+      false
+
+    render: ->
+      @collection.filter (d) ->
+        d.attributes.status.name == "Approved"
+      .sort (a,b) ->
+        a.get(config.customFields.departDate.id) > b.get(config.customFields.returnDate.id)
+      .each (model) =>
+        @$el.append @template(model)

src/main/resources/coffee/collections/custom_fields_collection.coffee

-define [
-  '../config'
-], (config)->
-  class CustomFieldsCollection extends Backbone.Collection
-    url: "#{config.restPath}/api/2/field"
-    initialize: ->
-      @fetch().done @setFields
-    setFields: =>
-      _.each config.customFields, (cf) =>
-        field = @find (f) =>
-          f.attributes.name == cf.name
-        _.extend cf, field.attributes

src/main/resources/coffee/collections/issue_types_collection.coffee

-define [
-  '../config'
-], (config)->
-
-  class IssueTypesCollection extends Backbone.Collection
-    url: "#{config.restPath}/api/2/issuetype"
-    initialize: ->
-      @fetch().done @findTravelReqIssueType
-    findTravelReqIssueType: =>
-      issueType = @find (it) ->
-        it.attributes.name == config.issueTypeName
-      @id = issueType.attributes.id

src/main/resources/coffee/collections/trips_collection.coffee

-define [
-  '../config'
-  '../models/trip_model'
-], (config, TripModel)->
-
-  class TripsCollection extends Backbone.Collection
-    url: "#{config.restPath}/api/2/search"
-
-    model: TripModel
-
-    comparator: (a,b) =>
-      a.get(config.customFields.departDate.id) < b.get(config.customFields.returnDate.id)
-
-    parse: (d) ->
-      _.map d.issues, (i) =>
-        _.extend i.fields,
-          id: i.id
-          key: i.key

src/main/resources/coffee/collections/users_collection.coffee

-define [
-  '../config'
-], (config)->
-    
-  class UsersCollection extends Backbone.Collection
-    url: (opts) =>
-      "#{config.restPath}/api/2/user/search?username=#{@q}"
-    initialize: (options) ->
-      @q = options.q
-      @fetch(options).done (d) =>
-        options.callback(d) if options.callback
-

src/main/resources/coffee/config.coffee

-# JIRA Travel Approval microapp
-# =============================
-# Before you can use this, you'll have to perform the following steps in your JIRA instance:
-#
-#     1. Create a new project to store your travel requests. The default project key used by this app is "TRAVEL"
-#     2. Create 5 new custom fields:
-#       1. Origin (string)
-#       2. Destination (string)
-#       3. Depart Date (date)
-#       4. Return Date (date)
-#       5. Purchase Amount (float)
-#       6. Currency (string)
-#     3. Create a issue type called "Travel Request"
-#     4. Create a custom workflow with four new statuses:
-#       1. Open
-#       2. Approved
-#       3. Denied
-#       4. Booked
-#
-# How you manage the transitions between those four statuses is up to you. You can modify any additional
-# configurations in the object below.
-
-define
-
-# Specify your JIRA config here:
-  restPath:           '/jira/rest' # This is the relative path to the rest api (http://localhost[/jira/rest]/api/2/...
-  projectKey:         'TRAVEL' # This is the project key where travel approvals will be created under
-  issueTypeName:      'Travel Request' # Name of the Issue Type you want issues to be created with
-  bookTransitionName: 'Book' # Name of the transition you want to use to "book" travel requests
-  customFields: # These are the names of your custom fields. We'll match them with the actual custom field at runtime
-    origin:
-      name: "Origin"
-    destination:
-      name: "Destination"
-    departDate:
-      name: "Depart Date"
-    returnDate:
-      name: "Return Date"
-    purchaseAmt:
-      name: "Purchase Amount"
-    currency:
-      name: "Currency"

src/main/resources/coffee/helpers.coffee

-define ->
-  
-  statusButton = (status) ->
-    switch status
-      when 'Open' then 'btn-info'
-      when 'Approved' then 'btn-success'
-      when 'Denied' then 'btn-danger'
-      when 'Booked' then 'btn-inverse'
-
-  getRate = (from, to) ->
-    $('#currscript').remove()
-    script = document.createElement 'script'
-    script.setAttribute 'src', 
-      "http://query.yahooapis.com/v1/public/yql?q=select%20rate%2Cname%20from%20csv%20where%20url%3D'http%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes%3Fs%3D#{from}#{to}%253DX%26f%3Dl1n'%20and%20columns%3D'rate%2Cname'&format=json&callback=parseExchangeRate"
-    script.setAttribute 'id', 'currscript'
-    document.body.appendChild script
-
-  window.parseExchangeRate = (data) ->
-    name = data.query.results.row.name
-    rate = parseFloat data.query.results.row.rate, 10
-    $('#costin').html (parseFloat $('#cost').val() * rate)
-      .round(2)
-      .format(2) + 
-      " AUD"
-
-  getPlace = (q) ->
-    $('#placescript').remove()
-    script = document.createElement 'script'
-    script.setAttribute 'src',
-      "http://gd.geobytes.com/AutoCompleteCity?q=#{q}&callback=handlePlaceJSONP"
-    script.setAttribute 'id', 'placescript'
-    document.body.appendChild script
-
-  window.handlePlaceJSONP = (d) ->
-    window.typeahead.process _.map d, (place)->
-      place.replace /NS/, "NSW"
-
-  statusButton: statusButton
-  getRate: getRate
-  getPlace: getPlace

src/main/resources/coffee/main.coffee

-require [
-  'config'
-  'helpers'
-  'models/user_model'
-  'models/current_user_model'
-  'models/project_model'
-  'models/saved_trip_model'
-  'models/trip_model'
-  'collections/custom_fields_collection'
-  'collections/issue_types_collection'
-  'collections/trips_collection'
-  'collections/users_collection'
-  'views/add_trip_view'
-  'views/flash_view'
-  'views/my_trip_view'
-  'views/my_trips_view'
-  'views/pending_trips_view'
-  'router'
-], (config, helpers, UserModel, CurrentUserModel, ProjectModel, SavedTripModel, TripModel, CustomFieldsCollection, IssueTypesCollection, TripsCollection, UsersCollection, AddTripView, FlashView, MyTripView, MyTripsView, PendingTripsView, Router)->
-  
-  window.config = config
-  window.helpers = helpers
-  window.currentUser = new CurrentUserModel
-  window.trips = new TripsCollection
-  window.project = new ProjectModel
-  window.issuetype = new IssueTypesCollection
-  window.customFields = new CustomFieldsCollection
-
-  addTripView = new AddTripView 
-    collection: trips
-
-  trips.fetch().done ->
-    myTripsView = new MyTripsView 
-      collection: trips
-
-    pendingTripsView = new PendingTripsView 
-      collection: trips
-
-  appRouter = new Router
-    addTripView: addTripView
-
-  Backbone.history.start()

src/main/resources/coffee/models/current_user_model.coffee

-define [
-  '../config'
-  './user_model'
-], (config, UserModel)->
-
-  console.log 1, arguments
-    
-  class CurrentUserModel extends Backbone.Model
-    url: "#{config.restPath}/auth/1/session"
-
-    initialize: ->
-      @fetch()
-
-    fetch: ->
-      super.done =>
-        new UserModel
-          id        : @get 'name'
-          callback  : (d) =>
-            @set 'profile', d
-            @setLoggedIn()
-
-    setLoggedIn: ->
-      $('.curruser').html currentUser.get('profile').displayName
-      $('.avatar').attr('src', currentUser.get('profile').avatarUrls["48x48"])

src/main/resources/coffee/models/project_model.coffee

-define [
-  '../config'
-], (config)->
-
-  class ProjectModel extends Backbone.Model
-    url: "#{config.restPath}/api/2/project/#{config.projectKey}"
-    initialize: ->
-      @fetch().done (d)->
-        @id = d.id

src/main/resources/coffee/models/saved_trip_model.coffee

-define [
-  '../config'
-], (config)->
-    
-  class SavedTripModel extends Backbone.Model
-    url: =>
-      "#{config.restPath}/api/2/issue/#{@key}"
-
-    initialize: (options) ->
-      @key = if options and options.key then options.key else ""
-      if @key
-        @fetch(options).done (d) =>
-          options.callback(d) if options.callback
-
-    parse: (d) ->
-      if d.fields
-        _.extend d.fields,
-          id: d.id
-          key: d.key
-      else
-        d

src/main/resources/coffee/models/trip_model.coffee

-define [
-  '../config'
-], (config)->
-    
-  class TripModel extends Backbone.Model
-    url: =>
-      "#{config.restPath}/api/2/issue"
-
-    initialize: (options) ->
-      @errors = []
-      @bind 'error', (model, errs) =>
-        console.log 'TripModel Error', arguments
-        @errors = errs 
-        errs
-
-    parse: (d) ->
-      if d.fields
-        _.extend d.fields,
-          id: d.id
-          key: d.key
-      else
-        d
-
-    validate:
-      summary:
-        required: true
-      description:
-        required: true
-      origin:
-        required: true
-      destination:
-        required: true
-      departDate:
-        required: true
-      returnDate:
-        required: true
-      purchaseAmt:
-        required: true
-      currency:
-        required: true
-      assignee:
-        required: true

src/main/resources/coffee/models/user_model.coffee

-define [
-  '../config'
-], (config)->
-    
-  class UserModel extends Backbone.Model
-    url: =>
-      "#{config.restPath}/api/2/user?username=#{@id}"
-    initialize: (options) ->
-      @fetch(options).done (d) =>
-        options.callback d if options.callback

src/main/resources/coffee/router.coffee

-define ->
-  
-  class Router extends Backbone.Router
-    routes:
-      ''          : 'addTrip'
-      'trips'     : 'myTrips'
-      'trip/book' : 'bookTrip'
-      'trip/new'  : 'addTrip'
-      '*path'     : 'notFound'
-
-    initialize: (options)->
-      @addTripView = options.addTripView
-
-    before: ->
-      $('.flash .alert').remove()
-
-    activate: (id) ->
-      $('.content').addClass 'hidden'
-      $('#'+id).removeClass 'hidden'
-      $('.topnav').removeClass 'active'
-      $('.'+id).addClass 'active'
-      return @
-
-    addTrip: =>
-      @activate 'addtrip'
-      @addTripView.focus()
-
-    myTrips: ->
-      @activate 'mytrips'
-
-    bookTrip: ->
-      @activate 'booktrip'
-
-    notFound: ->
-      @activate '404'

src/main/resources/coffee/views/add_trip_view.coffee

-define [
-  '../config'
-  '../models/trip_model'
-  '../models/saved_trip_model'
-  '../collections/users_collection'
-], (config, TripModel, SavedTripModel, UsersCollection)->
-    
-  class AddTripView extends Backbone.View
-    el: $ '#addtrip'
-
-    template: _.template $('#addtrip-template').html()
-
-    initialize: (options) ->
-      @render()
-
-    events:
-      # 'changeDate .date':       'renderFriendlyDate'
-      'change #cost':           'updateConvertedRate'
-      'change #curr':           'updateConvertedRate'
-      'submit #add-trip-form':  'saveTrip'
-
-    render: ->
-      @$el.html @.template()
-
-      $('#depart-date').data('date', Date.create().format("{M}/{dd}/{yyyy}"))
-      $('#return-date').data('date', Date.create().addDays(7).format("{M}/{dd}/{yyyy}"))
-
-      $('div.date').datepicker()
-
-      $('.place.from').typeahead
-        property: "origin"
-        source: (typeahead, query) ->
-          return if query.length < 3
-          window.typeahead = typeahead;
-          getPlace(query)
-
-      $('.place.to').typeahead
-        property: "destination"
-        source: (typeahead, query) ->
-          return if query.length < 3
-          window.typeahead = typeahead;
-          getPlace(query)
-
-      $('#assignee').typeahead
-        # property: "assignee"
-        source: (typeahead, query) =>
-          # _.map people.items, (o) ->
-          #   o.label
-          return if query.length < 3
-          new UsersCollection
-            q: query
-            callback: (d) =>
-              # console.log 3,d,_.pluck(d, 'name')
-              typeahead.process _.pluck(d, 'name')
-
-      $('#cost, #curr').on('change keyup', =>)
-      @
-
-    updateConvertedRate: ->
-      getRate $('#curr').val(), 'AUD'
-
-    focus: ->
-      focusCb = => @$el.find("input[type='text']:first").focus()
-      setTimeout focusCb, 0
-
-    showInvalid: (model, errs) =>
-      for attr of errs
-        @$el.find('.'+attr).addClass 'error'
-      false
-
-    showSavedToast: ->
-      @flash = new FlashView
-        flash: 
-          title:    'Success'
-          message:  'Your travel approval request has been saved and sent for approval'
-
-    saveTrip: ->
-      @$('.error').removeClass 'error'    
-
-      trip = $('form').serializeObject()
-      trip[config.customFields.departDate.id] = Date.create($('#depart-date input').data('date')).format(Date.ISO8601_DATETIME)
-      trip[config.customFields.returnDate.id] = Date.create($('#return-date input').data('date')).format(Date.ISO8601_DATETIME)
-
-      tripObj =
-        fields:
-          issuetype:
-            id: issuetype.id
-          project:
-            id: project.id
-
-      _.extend tripObj.fields,
-        summary: trip.summary if trip.summary
-      _.extend tripObj.fields,
-        description: trip.description if trip.description
-      tripObj.fields[config.customFields.origin.id] = trip.origin if trip.origin
-      tripObj.fields[config.customFields.destination.id] = trip.destination if trip.destination
-      tripObj.fields[config.customFields.departDate.id] = trip[config.customFields.departDate.id] if trip[config.customFields.departDate.id]
-      tripObj.fields[config.customFields.returnDate.id] = trip[config.customFields.returnDate.id] if trip[config.customFields.returnDate.id]
-      tripObj.fields[config.customFields.purchaseAmt.id] = parseInt(trip.purchaseAmt) if trip.purchaseAmt
-
-      if trip.currency
-        tripObj.fields[config.customFields.currency.id] = {}
-        tripObj.fields[config.customFields.currency.id]['value'] = trip.currency
-
-      _.extend tripObj.fields, if trip.assignee
-        assignee:
-            # name: trip.assignee
-            name: "alex"
-      @model = new TripModel
-      @model.on 'error', @showInvalid
-      @model.set trip
-      if @model.errors.length == 0 
-        rslt = @model.save tripObj,
-          success: (model, d) =>
-            @render()
-            @showSavedToast()
-            console.log 'success', @, arguments
-            savedTrip = new SavedTripModel
-              key: d.key
-              callback: (trip) =>
-                console.log 'callback', savedTrip
-                @collection.add savedTrip
-          error: (model, errs) =>
-            # @showInvalid model, errs
-
-      false

src/main/resources/coffee/views/flash_view.coffee

-define ->
-  
-  class FlashView extends Backbone.View
-    tagName: "div"
-    
-    className: "alert"
-    
-    template: _.template $('#flash-template').html()
-    
-    initialize: (options) ->
-      @render()
-    
-    render: ->
-      @$el.addClass @options.flash.class if @options.flash.class
-      @$el.html(@template(@options.flash)).prependTo('.flash')

src/main/resources/coffee/views/my_trip_view.coffee

-define ->
-  
-  class MyTripView extends Backbone.View
-    template: _.template $('#mytrip-template').html()
-
-    tagName: 'tr'
-    
-    initialize: (options) ->
-      @model.bind 'change', @render
-      @model.bind 'destroy', @remove
-
-    render: ->
-      @$el.html @template(@model) if @model
-
-    remove: ->
-      @$el.remove()

src/main/resources/coffee/views/my_trips_view.coffee

-define [
-  './my_trip_view'
-], (MyTripView)->
-    
-  class MyTripsView extends Backbone.View
-    el: $ '#mytrips'
-
-    template: _.template $('#mytrips-template').html()
-
-    initialize: (options) ->
-      @collection.on 'add', @addOne
-      @collection.on 'change', @render, @
-      @render()
-
-    render: ->
-      @$el.html @template()
-      @addAll()
-
-    addOne: (model) =>
-      myTripView = new MyTripView({model: model})
-      myTripView.render();
-      @$el.find('#mytrips-rows').prepend myTripView.el
-      model.bind 'remove', myTripView.remove
-
-    addAll: ->
-      @collection.each @addOne
-

src/main/resources/coffee/views/pending_trips_view.coffee

-define ->
-  
-  class PendingTripsView extends Backbone.View
-    el: $ '#pendingtrips-rows'
-
-    template: _.template $('#pendingtrips-template').html()
-
-    initialize: (options)->
-      @render()
-
-    events:
-      "click .complete-booking": "completeBooking"
-
-    completeBooking: (e) ->
-      el = $(e.currentTarget)
-      trip = el.data()
-      $.get "#{config.restPath}/api/2/issue/#{trip.key}/transitions", (d) =>
-        bookTransition = _.find d.transitions, (t) =>
-          t.name == config.bookTransitionName
-        $.ajax
-          url: "#{config.restPath}/api/2/issue/#{trip.key}/transitions"
-          type: "post"
-          dataType: "json"
-          contentType: "application/json"
-          processData: false
-          data:
-            JSON.stringify
-              transition:
-                id: bookTransition.id
-          success: =>
-            el.removeClass('btn-success').addClass('btn-inverse').addClass('disabled').html('Booked')
-            @collection.get(trip.id).set('status', {name: "Booked"}, {silent: true})
-            @collection.trigger 'change'
-      false
-
-    render: ->
-      @collection.filter (d) ->
-        d.attributes.status.name == "Approved"
-      .sort (a,b) ->
-        a.get(config.customFields.departDate.id) > b.get(config.customFields.returnDate.id)
-      .each (model) =>
-        @$el.append @template(model)

src/main/resources/js/almond.js

+/**
+ * almond 0.1.1 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/almond for details
+ */
+//Going sloppy to avoid 'use strict' string cost, but strict practices should
+//be followed.
+/*jslint sloppy: true */
+/*global setTimeout: false */
+
+var requirejs, require, define;
+(function (undef) {
+    var defined = {},
+        waiting = {},
+        config = {},
+        defining = {},
+        aps = [].slice,
+        main, req;
+
+    /**
+     * Given a relative module name, like ./something, normalize it to
+     * a real name that can be mapped to a path.
+     * @param {String} name the relative name
+     * @param {String} baseName a real name that the name arg is relative
+     * to.
+     * @returns {String} normalized name
+     */
+    function normalize(name, baseName) {
+        var baseParts = baseName && baseName.split("/"),
+            map = config.map,
+            starMap = (map && map['*']) || {},
+            nameParts, nameSegment, mapValue, foundMap, i, j, part;
+
+        //Adjust any relative paths.
+        if (name && name.charAt(0) === ".") {
+            //If have a base name, try to normalize against it,
+            //otherwise, assume it is a top-level require that will
+            //be relative to baseUrl in the end.
+            if (baseName) {
+                //Convert baseName to array, and lop off the last part,
+                //so that . matches that "directory" and not name of the baseName's
+                //module. For instance, baseName of "one/two/three", maps to
+                //"one/two/three.js", but we want the directory, "one/two" for
+                //this normalization.
+                baseParts = baseParts.slice(0, baseParts.length - 1);
+
+                name = baseParts.concat(name.split("/"));
+
+                //start trimDots
+                for (i = 0; (part = name[i]); i++) {
+                    if (part === ".") {
+                        name.splice(i, 1);
+                        i -= 1;
+                    } else if (part === "..") {
+                        if (i === 1 && (name[2] === '..' || name[0] === '..')) {
+                            //End of the line. Keep at least one non-dot
+                            //path segment at the front so it can be mapped
+                            //correctly to disk. Otherwise, there is likely
+                            //no path mapping for a path starting with '..'.
+                            //This can still fail, but catches the most reasonable
+                            //uses of ..
+                            return true;
+                        } else if (i > 0) {
+                            name.splice(i - 1, 2);
+                            i -= 2;
+                        }
+                    }
+                }
+                //end trimDots
+
+                name = name.join("/");
+            }
+        }
+
+        //Apply map config if available.
+        if ((baseParts || starMap) && map) {
+            nameParts = name.split('/');
+
+            for (i = nameParts.length; i > 0; i -= 1) {
+                nameSegment = nameParts.slice(0, i).join("/");
+
+                if (baseParts) {
+                    //Find the longest baseName segment match in the config.
+                    //So, do joins on the biggest to smallest lengths of baseParts.
+                    for (j = baseParts.length; j > 0; j -= 1) {
+                        mapValue = map[baseParts.slice(0, j).join('/')];
+
+                        //baseName segment has  config, find if it has one for
+                        //this name.
+                        if (mapValue) {
+                            mapValue = mapValue[nameSegment];
+                            if (mapValue) {
+                                //Match, update name to the new value.
+                                foundMap = mapValue;
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                foundMap = foundMap || starMap[nameSegment];
+
+                if (foundMap) {
+                    nameParts.splice(0, i, foundMap);
+                    name = nameParts.join('/');
+                    break;
+                }
+            }
+        }
+
+        return name;
+    }
+
+    function makeRequire(relName, forceSync) {
+        return function () {
+            //A version of a require function that passes a moduleName
+            //value for items that may need to
+            //look up paths relative to the moduleName
+            return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync]));
+        };
+    }
+
+    function makeNormalize(relName) {
+        return function (name) {
+            return normalize(name, relName);
+        };
+    }
+
+    function makeLoad(depName) {
+        return function (value) {
+            defined[depName] = value;
+        };
+    }
+
+    function callDep(name) {
+        if (waiting.hasOwnProperty(name)) {
+            var args = waiting[name];
+            delete waiting[name];
+            defining[name] = true;
+            main.apply(undef, args);
+        }
+
+        if (!defined.hasOwnProperty(name)) {
+            throw new Error('No ' + name);
+        }
+        return defined[name];
+    }
+
+    /**
+     * Makes a name map, normalizing the name, and using a plugin
+     * for normalization if necessary. Grabs a ref to plugin
+     * too, as an optimization.
+     */
+    function makeMap(name, relName) {
+        var prefix, plugin,
+            index = name.indexOf('!');
+
+        if (index !== -1) {
+            prefix = normalize(name.slice(0, index), relName);
+            name = name.slice(index + 1);
+            plugin = callDep(prefix);
+
+            //Normalize according
+            if (plugin && plugin.normalize) {
+                name = plugin.normalize(name, makeNormalize(relName));
+            } else {
+                name = normalize(name, relName);
+            }
+        } else {
+            name = normalize(name, relName);
+        }
+
+        //Using ridiculous property names for space reasons
+        return {
+            f: prefix ? prefix + '!' + name : name, //fullName
+            n: name,
+            p: plugin
+        };
+    }
+
+    function makeConfig(name) {
+        return function () {
+            return (config && config.config && config.config[name]) || {};
+        };
+    }
+
+    main = function (name, deps, callback, relName) {
+        var args = [],
+            usingExports,
+            cjsModule, depName, ret, map, i;
+
+        //Use name if no relName
+        relName = relName || name;
+
+        //Call the callback to define the module, if necessary.
+        if (typeof callback === 'function') {
+
+            //Pull out the defined dependencies and pass the ordered
+            //values to the callback.
+            //Default to [require, exports, module] if no deps
+            deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
+            for (i = 0; i < deps.length; i++) {
+                map = makeMap(deps[i], relName);
+                depName = map.f;
+
+                //Fast path CommonJS standard dependencies.
+                if (depName === "require") {
+                    args[i] = makeRequire(name);
+                } else if (depName === "exports") {
+                    //CommonJS module spec 1.1
+                    args[i] = defined[name] = {};
+                    usingExports = true;
+                } else if (depName === "module") {
+                    //CommonJS module spec 1.1
+                    cjsModule = args[i] = {
+                        id: name,
+                        uri: '',
+                        exports: defined[name],
+                        config: makeConfig(name)
+                    };
+                } else if (defined.hasOwnProperty(depName) || waiting.hasOwnProperty(depName)) {
+                    args[i] = callDep(depName);
+                } else if (map.p) {
+                    map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
+                    args[i] = defined[depName];
+                } else if (!defining[depName]) {
+                    throw new Error(name + ' missing ' + depName);
+                }
+            }
+
+            ret = callback.apply(defined[name], args);
+
+            if (name) {
+                //If setting exports via "module" is in play,
+                //favor that over return value and exports. After that,
+                //favor a non-undefined return value over exports use.
+                if (cjsModule && cjsModule.exports !== undef &&
+                    cjsModule.exports !== defined[name]) {
+                    defined[name] = cjsModule.exports;
+                } else if (ret !== undef || !usingExports) {
+                    //Use the return value from the function.
+                    defined[name] = ret;
+                }
+            }
+        } else if (name) {
+            //May just be an object definition for the module. Only
+            //worry about defining if have a module name.
+            defined[name] = callback;
+        }
+    };
+
+    requirejs = require = req = function (deps, callback, relName, forceSync) {
+        if (typeof deps === "string") {
+            //Just return the module wanted. In this scenario, the
+            //deps arg is the module name, and second arg (if passed)
+            //is just the relName.
+            //Normalize module name, if it contains . or ..
+            return callDep(makeMap(deps, callback).f);
+        } else if (!deps.splice) {
+            //deps is a config object, not an array.
+            config = deps;
+            if (callback.splice) {
+                //callback is an array, which means it is a dependency list.
+                //Adjust args if there are dependencies
+                deps = callback;
+                callback = relName;
+                relName = null;
+            } else {
+                deps = undef;
+            }
+        }
+
+        //Support require(['a'])
+        callback = callback || function () {};
+
+        //Simulate async callback;
+        if (forceSync) {
+            main(undef, deps, callback, relName);
+        } else {
+            setTimeout(function () {
+                main(undef, deps, callback, relName);
+            }, 15);
+        }
+
+        return req;
+    };
+
+    /**
+     * Just drops the config on the floor, but returns req in case
+     * the config return value is used.
+     */
+    req.config = function (cfg) {
+        config = cfg;
+        return req;
+    };
+
+    define = function (name, deps, callback) {
+
+        //This module may not have dependencies
+        if (!deps.splice) {
+            //deps is not an array, so probably means
+            //an object literal or factory function for
+            //the value. Adjust args.
+            callback = deps;
+            deps = [];
+        }
+
+        waiting[name] = [name, deps, callback];
+    };
+
+    define.amd = {
+        jQuery: true
+    };
+}());

src/main/resources/js/app.js

 // Generated by CoffeeScript 1.3.1
-var addTripView, appRouter;
 
-window.currentUser = new CurrentUserModel;
-
-window.trips = new TripsCollection;
-
-window.project = new ProjectModel;
-
-window.issuetype = new IssueTypesCollection;
-
-window.customFields = new CustomFieldsCollection;
-
-addTripView = new AddTripView({
-  collection: trips
-});
-
-trips.fetch().done(function() {
-  var myTripsView, pendingTripsView;
-  myTripsView = new MyTripsView({
+define(function(require) {
+  var AddTripView, CurrentUserModel, CustomFieldsCollection, FlashView, IssueTypesCollection, MyTripView, MyTripsView, PendingTripsView, ProjectModel, Router, SavedTripModel, TripModel, TripsCollection, UserModel, UsersCollection, addTripView, appRouter, config, currentUser, customFields, helpers, issuetype, project, trips;
+  config = require('config');
+  helpers = require('helpers');
+  UserModel = require('models/user_model');
+  CurrentUserModel = require('models/current_user_model');
+  ProjectModel = require('models/project_model');
+  SavedTripModel = require('models/saved_trip_model');
+  TripModel = require('models/trip_model');
+  CustomFieldsCollection = require('collections/custom_fields_collection');
+  IssueTypesCollection = require('collections/issue_types_collection');
+  TripsCollection = require('collections/trips_collection');
+  UsersCollection = require('collections/users_collection');
+  AddTripView = require('views/add_trip_view');
+  FlashView = require('views/flash_view');
+  MyTripView = require('views/my_trip_view');
+  MyTripsView = require('views/my_trips_view');
+  PendingTripsView = require('views/pending_trips_view');
+  Router = require('router');
+  window.config = config;
+  currentUser = new CurrentUserModel;
+  trips = new TripsCollection;
+  project = new ProjectModel;
+  issuetype = new IssueTypesCollection;
+  customFields = new CustomFieldsCollection;
+  addTripView = new AddTripView({
     collection: trips
   });
-  return pendingTripsView = new PendingTripsView({
-    collection: trips
+  trips.fetch().done(function() {
+    var myTripsView, pendingTripsView;
+    myTripsView = new MyTripsView({
+      collection: trips
+    });
+    return pendingTripsView = new PendingTripsView({
+      collection: trips
+    });
+  });
+  appRouter = new Router({
+    addTripView: addTripView
   });
+  return Backbone.history.start();
 });
-
-appRouter = new Router;
-
-Backbone.history.start();

src/main/resources/js/collections/custom_fields_collection.js

   __hasProp = {}.hasOwnProperty,
   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
 
-define(['../config'], function(config) {
-  var CustomFieldsCollection;
+define(function(require) {
+  var CustomFieldsCollection, config;
+  config = require('../config');
   return CustomFieldsCollection = (function(_super) {
 
     __extends(CustomFieldsCollection, _super);

src/main/resources/js/collections/issue_types_collection.js

   __hasProp = {}.hasOwnProperty,
   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
 
-define(['../config'], function(config) {
-  var IssueTypesCollection;
+define(function(require) {
+  var IssueTypesCollection, config;
+  config = require('../config');
   return IssueTypesCollection = (function(_super) {
 
     __extends(IssueTypesCollection, _super);

src/main/resources/js/collections/trips_collection.js

   __hasProp = {}.hasOwnProperty,
   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
 
-define(['../config', '../models/trip_model'], function(config, TripModel) {
-  var TripsCollection;
+define(function(require) {
+  var TripModel, TripsCollection, config;
+  config = require('../config');
+  TripModel = require('../models/trip_model');
   return TripsCollection = (function(_super) {
 
     __extends(TripsCollection, _super);

src/main/resources/js/collections/users_collection.js

   __hasProp = {}.hasOwnProperty,
   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
 
-define(['../config'], function(config) {
-  var UsersCollection;
+define(function(require) {
+  var UsersCollection, config;
+  config = require('../config');
   return UsersCollection = (function(_super) {
 
     __extends(UsersCollection, _super);

src/main/resources/js/helpers.js

 // Generated by CoffeeScript 1.3.1
 
 define(function() {
-  var getPlace, getRate, statusButton;
-  statusButton = function(status) {
+  window.statusButton = function(status) {
     switch (status) {
       case 'Open':
         return 'btn-info';
         return 'btn-inverse';
     }
   };
-  getRate = function(from, to) {
-    var script;
-    $('#currscript').remove();
-    script = document.createElement('script');
-    script.setAttribute('src', "http://query.yahooapis.com/v1/public/yql?q=select%20rate%2Cname%20from%20csv%20where%20url%3D'http%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes%3Fs%3D" + from + to + "%253DX%26f%3Dl1n'%20and%20columns%3D'rate%2Cname'&format=json&callback=parseExchangeRate");
-    script.setAttribute('id', 'currscript');
-    return document.body.appendChild(script);
-  };
   window.parseExchangeRate = function(data) {
     var name, rate;
     name = data.query.results.row.name;
     rate = parseFloat(data.query.results.row.rate, 10);
     return $('#costin').html((parseFloat($('#cost').val() * rate)).round(2).format(2) + " AUD");
   };
-  getPlace = function(q) {
-    var script;
-    $('#placescript').remove();
-    script = document.createElement('script');
-    script.setAttribute('src', "http://gd.geobytes.com/AutoCompleteCity?q=" + q + "&callback=handlePlaceJSONP");
-    script.setAttribute('id', 'placescript');
-    return document.body.appendChild(script);
-  };
-  window.handlePlaceJSONP = function(d) {
+  return window.handlePlaceJSONP = function(d) {
     return window.typeahead.process(_.map(d, function(place) {
       return place.replace(/NS/, "NSW");
     }));
   };
-  return {
-    statusButton: statusButton,
-    getRate: getRate,
-    getPlace: getPlace
-  };
 });

src/main/resources/js/main.js

 // Generated by CoffeeScript 1.3.1
 
-require(['config', 'helpers', 'models/user_model', 'models/current_user_model', 'models/project_model', 'models/saved_trip_model', 'models/trip_model', 'collections/custom_fields_collection', 'collections/issue_types_collection', 'collections/trips_collection', 'collections/users_collection', 'views/add_trip_view', 'views/flash_view', 'views/my_trip_view', 'views/my_trips_view', 'views/pending_trips_view', 'router'], function(config, helpers, UserModel, CurrentUserModel, ProjectModel, SavedTripModel, TripModel, CustomFieldsCollection, IssueTypesCollection, TripsCollection, UsersCollection, AddTripView, FlashView, MyTripView, MyTripsView, PendingTripsView, Router) {
-  var addTripView, appRouter;
-  window.config = config;
-  window.helpers = helpers;
-  window.currentUser = new CurrentUserModel;
-  window.trips = new TripsCollection;
-  window.project = new ProjectModel;
-  window.issuetype = new IssueTypesCollection;
-  window.customFields = new CustomFieldsCollection;
-  addTripView = new AddTripView({
-    collection: trips
-  });
-  trips.fetch().done(function() {
-    var myTripsView, pendingTripsView;
-    myTripsView = new MyTripsView({
-      collection: trips
-    });
-    return pendingTripsView = new PendingTripsView({
-      collection: trips
-    });
-  });
-  appRouter = new Router({
-    addTripView: addTripView
-  });
-  return Backbone.history.start();
-});
+require(['app']);

src/main/resources/js/main.min.js

-define("config",{restPath:"/jira/rest",projectKey:"TRAVEL",issueTypeName:"Travel Request",bookTransitionName:"Book",customFields:{origin:{name:"Origin"},destination:{name:"Destination"},departDate:{name:"Depart Date"},returnDate:{name:"Return Date"},purchaseAmt:{name:"Purchase Amount"},currency:{name:"Currency"}}}),define("helpers",[],function(){var a,b,c;return c=function(a){switch(a){case"Open":return"btn-info";case"Approved":return"btn-success";case"Denied":return"btn-danger";case"Booked":return"btn-inverse"}},b=function(a,b){var c;return $("#currscript").remove(),c=document.createElement("script"),c.setAttribute("src","http://query.yahooapis.com/v1/public/yql?q=select%20rate%2Cname%20from%20csv%20where%20url%3D'http%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes%3Fs%3D"+a+b+"%253DX%26f%3Dl1n'%20and%20columns%3D'rate%2Cname'&format=json&callback=parseExchangeRate"),c.setAttribute("id","currscript"),document.body.appendChild(c)},window.parseExchangeRate=function(a){var b,c;return b=a.query.results.row.name,c=parseFloat(a.query.results.row.rate,10),$("#costin").html(parseFloat($("#cost").val()*c).round(2).format(2)+" AUD")},a=function(a){var b;return $("#placescript").remove(),b=document.createElement("script"),b.setAttribute("src","http://gd.geobytes.com/AutoCompleteCity?q="+a+"&callback=handlePlaceJSONP"),b.setAttribute("id","placescript"),document.body.appendChild(b)},window.handlePlaceJSONP=function(a){return window.typeahead.process(_.map(a,function(a){return a.replace(/NS/,"NSW")}))},{statusButton:c,getRate:b,getPlace:a}});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("models/user_model",["../config"],function(a){var b;return b=function(b){function c(){return this.url=__bind(this.url,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="UserModel",c.prototype.url=function(){return""+a.restPath+"/api/2/user?username="+this.id},c.prototype.initialize=function(a){var b=this;return this.fetch(a).done(function(b){if(a.callback)return a.callback(b)})},c}(Backbone.Model)});var __hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("models/current_user_model",["../config","./user_model"],function(a,b){var c;return console.log(1,arguments),c=function(c){function d(){return d.__super__.constructor.apply(this,arguments)}return __extends(d,c),d.name="CurrentUserModel",d.prototype.url=""+a.restPath+"/auth/1/session",d.prototype.initialize=function(){return this.fetch()},d.prototype.fetch=function(){var a=this;return d.__super__.fetch.apply(this,arguments).done(function(){return new b({id:a.get("name"),callback:function(b){return a.set("profile",b),a.setLoggedIn()}})})},d.prototype.setLoggedIn=function(){return $(".curruser").html(currentUser.get("profile").displayName),$(".avatar").attr("src",currentUser.get("profile").avatarUrls["48x48"])},d}(Backbone.Model)});var __hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("models/project_model",["../config"],function(a){var b;return b=function(b){function c(){return c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="ProjectModel",c.prototype.url=""+a.restPath+"/api/2/project/"+a.projectKey,c.prototype.initialize=function(){return this.fetch().done(function(a){return this.id=a.id})},c}(Backbone.Model)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("models/saved_trip_model",["../config"],function(a){var b;return b=function(b){function c(){return this.url=__bind(this.url,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="SavedTripModel",c.prototype.url=function(){return""+a.restPath+"/api/2/issue/"+this.key},c.prototype.initialize=function(a){var b=this;this.key=a&&a.key?a.key:"";if(this.key)return this.fetch(a).done(function(b){if(a.callback)return a.callback(b)})},c.prototype.parse=function(a){return a.fields?_.extend(a.fields,{id:a.id,key:a.key}):a},c}(Backbone.Model)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("models/trip_model",["../config"],function(a){var b;return b=function(b){function c(){return this.url=__bind(this.url,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="TripModel",c.prototype.url=function(){return""+a.restPath+"/api/2/issue"},c.prototype.initialize=function(a){var b=this;return this.errors=[],this.bind("error",function(a,c){return console.log("TripModel Error",arguments),b.errors=c,c})},c.prototype.parse=function(a){return a.fields?_.extend(a.fields,{id:a.id,key:a.key}):a},c.prototype.validate={summary:{required:!0},description:{required:!0},origin:{required:!0},destination:{required:!0},departDate:{required:!0},returnDate:{required:!0},purchaseAmt:{required:!0},currency:{required:!0},assignee:{required:!0}},c}(Backbone.Model)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("collections/custom_fields_collection",["../config"],function(a){var b;return b=function(b){function c(){return this.setFields=__bind(this.setFields,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="CustomFieldsCollection",c.prototype.url=""+a.restPath+"/api/2/field",c.prototype.initialize=function(){return this.fetch().done(this.setFields)},c.prototype.setFields=function(){var b=this;return _.each(a.customFields,function(a){var c;return c=b.find(function(b){return b.attributes.name===a.name}),_.extend(a,c.attributes)})},c}(Backbone.Collection)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("collections/issue_types_collection",["../config"],function(a){var b;return b=function(b){function c(){return this.findTravelReqIssueType=__bind(this.findTravelReqIssueType,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="IssueTypesCollection",c.prototype.url=""+a.restPath+"/api/2/issuetype",c.prototype.initialize=function(){return this.fetch().done(this.findTravelReqIssueType)},c.prototype.findTravelReqIssueType=function(){var b;return b=this.find(function(b){return b.attributes.name===a.issueTypeName}),this.id=b.attributes.id},c}(Backbone.Collection)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("collections/trips_collection",["../config","../models/trip_model"],function(a,b){var c;return c=function(c){function d(){return this.comparator=__bind(this.comparator,this),d.__super__.constructor.apply(this,arguments)}return __extends(d,c),d.name="TripsCollection",d.prototype.url=""+a.restPath+"/api/2/search",d.prototype.model=b,d.prototype.comparator=function(b,c){return b.get(a.customFields.departDate.id)<c.get(a.customFields.returnDate.id)},d.prototype.parse=function(a){var b=this;return _.map(a.issues,function(a){return _.extend(a.fields,{id:a.id,key:a.key})})},d}(Backbone.Collection)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("collections/users_collection",["../config"],function(a){var b;return b=function(b){function c(){return this.url=__bind(this.url,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="UsersCollection",c.prototype.url=function(b){return""+a.restPath+"/api/2/user/search?username="+this.q},c.prototype.initialize=function(a){var b=this;return this.q=a.q,this.fetch(a).done(function(b){if(a.callback)return a.callback(b)})},c}(Backbone.Collection)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("views/add_trip_view",["../config","../models/trip_model","../models/saved_trip_model","../collections/users_collection"],function(a,b,c,d){var e;return e=function(e){function f(){return this.showInvalid=__bind(this.showInvalid,this),f.__super__.constructor.apply(this,arguments)}return __extends(f,e),f.name="AddTripView",f.prototype.el=$("#addtrip"),f.prototype.template=_.template($("#addtrip-template").html()),f.prototype.initialize=function(a){return this.render()},f.prototype.events={"change #cost":"updateConvertedRate","change #curr":"updateConvertedRate","submit #add-trip-form":"saveTrip"},f.prototype.render=function(){var a=this;return this.$el.html(this.template()),$("#depart-date").data("date",Date.create().format("{M}/{dd}/{yyyy}")),$("#return-date").data("date",Date.create().addDays(7).format("{M}/{dd}/{yyyy}")),$("div.date").datepicker(),$(".place.from").typeahead({property:"origin",source:function(a,b){if(b.length<3)return;return window.typeahead=a,getPlace(b)}}),$(".place.to").typeahead({property:"destination",source:function(a,b){if(b.length<3)return;return window.typeahead=a,getPlace(b)}}),$("#assignee").typeahead({source:function(a,b){if(b.length<3)return;return new d({q:b,callback:function(b){return a.process(_.pluck(b,"name"))}})}}),$("#cost, #curr").on("change keyup",function(){}),this},f.prototype.updateConvertedRate=function(){return getRate($("#curr").val(),"AUD")},f.prototype.focus=function(){var a,b=this;return a=function(){return b.$el.find("input[type='text']:first").focus()},setTimeout(a,0)},f.prototype.showInvalid=function(a,b){var c;for(c in b)this.$el.find("."+c).addClass("error");return!1},f.prototype.showSavedToast=function(){return this.flash=new FlashView({flash:{title:"Success",message:"Your travel approval request has been saved and sent for approval"}})},f.prototype.saveTrip=function(){var d,e,f,g=this;return this.$(".error").removeClass("error"),e=$("form").serializeObject(),e[a.customFields.departDate.id]=Date.create($("#depart-date input").data("date")).format(Date.ISO8601_DATETIME),e[a.customFields.returnDate.id]=Date.create($("#return-date input").data("date")).format(Date.ISO8601_DATETIME),f={fields:{issuetype:{id:issuetype.id},project:{id:project.id}}},_.extend(f.fields,e.summary?{summary:e.summary}:void 0),_.extend(f.fields,e.description?{description:e.description}:void 0),e.origin&&(f.fields[a.customFields.origin.id]=e.origin),e.destination&&(f.fields[a.customFields.destination.id]=e.destination),e[a.customFields.departDate.id]&&(f.fields[a.customFields.departDate.id]=e[a.customFields.departDate.id]),e[a.customFields.returnDate.id]&&(f.fields[a.customFields.returnDate.id]=e[a.customFields.returnDate.id]),e.purchaseAmt&&(f.fields[a.customFields.purchaseAmt.id]=parseInt(e.purchaseAmt)),e.currency&&(f.fields[a.customFields.currency.id]={},f.fields[a.customFields.currency.id].value=e.currency),_.extend(f.fields,e.assignee?{assignee:{name:"alex"}}:void 0),this.model=new b,this.model.on("error",this.showInvalid),this.model.set(e),this.model.errors.length===0&&(d=this.model.save(f,{success:function(a,b){var d;return g.render(),g.showSavedToast(),console.log("success",g,arguments),d=new c({key:b.key,callback:function(a){return console.log("callback",d),g.collection.add(d)}})},error:function(a,b){}})),!1},f}(Backbone.View)});var __hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("views/flash_view",[],function(){var a;return a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return __extends(b,a),b.name="FlashView",b.prototype.tagName="div",b.prototype.className="alert",b.prototype.template=_.template($("#flash-template").html()),b.prototype.initialize=function(a){return this.render()},b.prototype.render=function(){return this.options.flash["class"]&&this.$el.addClass(this.options.flash["class"]),this.$el.html(this.template(this.options.flash)).prependTo(".flash")},b}(Backbone.View)});var __hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("views/my_trip_view",[],function(){var a;return a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return __extends(b,a),b.name="MyTripView",b.prototype.template=_.template($("#mytrip-template").html()),b.prototype.tagName="tr",b.prototype.initialize=function(a){return this.model.bind("change",this.render),this.model.bind("destroy",this.remove)},b.prototype.render=function(){if(this.model)return this.$el.html(this.template(this.model))},b.prototype.remove=function(){return this.$el.remove()},b}(Backbone.View)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("views/my_trips_view",["./my_trip_view"],function(a){var b;return b=function(b){function c(){return this.addOne=__bind(this.addOne,this),c.__super__.constructor.apply(this,arguments)}return __extends(c,b),c.name="MyTripsView",c.prototype.el=$("#mytrips"),c.prototype.template=_.template($("#mytrips-template").html()),c.prototype.initialize=function(a){return this.collection.on("add",this.addOne),this.collection.on("change",this.render,this),this.render()},c.prototype.render=function(){return this.$el.html(this.template()),this.addAll()},c.prototype.addOne=function(b){var c;return c=new a({model:b}),c.render(),this.$el.find("#mytrips-rows").prepend(c.el),b.bind("remove",c.remove)},c.prototype.addAll=function(){return this.collection.each(this.addOne)},c}(Backbone.View)});var __hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("views/pending_trips_view",[],function(){var a;return a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return __extends(b,a),b.name="PendingTripsView",b.prototype.el=$("#pendingtrips-rows"),b.prototype.template=_.template($("#pendingtrips-template").html()),b.prototype.initialize=function(a){return this.render()},b.prototype.events={"click .complete-booking":"completeBooking"},b.prototype.completeBooking=function(a){var b,c,d=this;return b=$(a.currentTarget),c=b.data(),$.get(""+config.restPath+"/api/2/issue/"+c.key+"/transitions",function(a){var e;return e=_.find(a.transitions,function(a){return a.name===config.bookTransitionName}),$.ajax({url:""+config.restPath+"/api/2/issue/"+c.key+"/transitions",type:"post",dataType:"json",contentType:"application/json",processData:!1,data:JSON.stringify({transition:{id:e.id}}),success:function(){return b.removeClass("btn-success").addClass("btn-inverse").addClass("disabled").html("Booked"),d.collection.get(c.id).set("status",{name:"Booked"},{silent:!0}),d.collection.trigger("change")}})}),!1},b.prototype.render=function(){var a=this;return this.collection.filter(function(a){return a.attributes.status.name==="Approved"}).sort(function(a,b){return a.get(config.customFields.departDate.id)>b.get(config.customFields.returnDate.id)}).each(function(b){return a.$el.append(a.template(b))})},b}(Backbone.View)});var __bind=function(a,b){return function(){return a.apply(b,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(a,b){function d(){this.constructor=a}for(var c in b)__hasProp.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};define("router",[],function(){var a;return a=function(a){function b(){return this.addTrip=__bind(this.addTrip,this),b.__super__.constructor.apply(this,arguments)}return __extends(b,a),b.name="Router",b.prototype.routes={"":"addTrip",trips:"myTrips","trip/book":"bookTrip","trip/new":"addTrip","*path":"notFound"},b.prototype.initialize=function(a){return this.addTripView=a.addTripView},b.prototype.before=function(){return $(".flash .alert").remove()},b.prototype.activate=function(a){return $(".content").addClass("hidden"),$("#"+a).removeClass("hidden"),$(".topnav").removeClass("active"),$("."+a).addClass("active"),this},b.prototype.addTrip=function(){return this.activate("addtrip"),this.addTripView.focus()},b.prototype.myTrips=function(){return this.activate("mytrips")},b.prototype.bookTrip=function(){return this.activate("booktrip")},b.prototype.notFound=function(){return this.activate("404")},b}(Backbone.Router)}),require(["config","helpers","models/user_model","models/current_user_model","models/project_model","models/saved_trip_model","models/trip_model","collections/custom_fields_collection","collections/issue_types_collection","collections/trips_collection","collections/users_collection","views/add_trip_view","views/flash_view","views/my_trip_view","views/my_trips_view","views/pending_trips_view","router"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){var r,s;return window.config=a,window.helpers=b,window.currentUser=new d,window.trips=new j,window.project=new e,window.issuetype=new i,window.customFields=new h,r=new l({collection:trips}),trips.fetch().done(function(){var a,b;return a=new o({collection:trips}),b=new p({collection:trips})}),s=new q({addTripView:r}),Backbone.history.start()}),define("main",function(){})
+/**
+ * almond 0.1.1 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/almond for details
+ */
+
+(function(){var a,b,c;(function(d){function l(a,b){var c=b&&b.split("/"),d=g.map,e=d&&d["*"]||{},f,h,i,j,k,l,m;if(a&&a.charAt(0)==="."&&b){c=c.slice(0,c.length-1),a=c.concat(a.split("/"));for(k=0;m=a[k];k++)if(m===".")a.splice(k,1),k-=1;else if(m===".."){if(k===1&&(a[2]===".."||a[0]===".."))return!0;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}if((c||e)&&d){f=a.split("/");for(k=f.length;k>0;k-=1){h=f.slice(0,k).join("/");if(c)for(l=c.length;l>0;l-=1){i=d[c.slice(0,l).join("/")];if(i){i=i[h];if(i){j=i;break}}}j=j||e[h];if(j){f.splice(0,k,j),a=f.join("/");break}}}return a}function m(a,b){return function(){return k.apply(d,i.call(arguments,0).concat([a,b]))}}function n(a){return function(b){return l(b,a)}}function o(a){return function(b){e[a]=b}}function p(a){if(f.hasOwnProperty(a)){var b=f[a];delete f[a],h[a]=!0,j.apply(d,b)}if(!e.hasOwnProperty(a))throw new Error("No "+a);return e[a]}function q(a,b){var c,d,e=a.indexOf("!");return e!==-1?(c=l(a.slice(0,e),b),a=a.slice(e+1),d=p(c),d&&d.normalize?a=d.normalize(a,n(b)):a=l(a,b)):a=l(a,b),{f:c?c+"!"+a:a,n:a,p:d}}function r(a){return function(){return g&&g.config&&g.config[a]||{}}}var e={},f={},g={},h={},i=[].slice,j,k;j=function(a,b,c,g){var i=[],j,k,l,n,s,t;g=g||a;if(typeof c=="function"){b=!b.length&&c.length?["require","exports","module"]:b;for(t=0;t<b.length;t++){s=q(b[t],g),l=s.f;if(l==="require")i[t]=m(a);else if(l==="exports")i[t]=e[a]={},j=!0;else if(l==="module")k=i[t]={id:a,uri:"",exports:e[a],config:r(a)};else if(e.hasOwnProperty(l)||f.hasOwnProperty(l))i[t]=p(l);else if(s.p)s.p.load(s.n,m(g,!0),o(l),{}),i[t]=e[l];else if(!h[l])throw new Error(a+" missing "+l)}n=c.apply(e[a],i);if(a)if(k&&k.exports!==d&&k.exports!==e[a])e[a]=k.exports;else if(n!==d||!j)e[a]=n}else a&&(e[a]=c)},a=b=k=function(a,b,c,e){return typeof a=="string"?p(q(a,b).f):(a.splice||(g=a,b.splice?(a=b,b=c,c=null):a=d),b=b||function(){},e?j(d,a,b,c):setTimeout(function(){j(d,a,b,c)},15),k)},k.config=function(a){return g=a,k},c=function(a,b,c){b.splice||(c=b,b=[]),f[a]=[a,b,c]},c.amd={jQuery:!0}})(),c("almond",function(){}),c("config",{restPath:"/jira/rest",projectKey:"TRAVEL",issueTypeName:"Travel Request",bookTransitionName:"Book",customFields:{origin:{name:"Origin"},destination:{name:"Destination"},departDate:{name:"Depart Date"},returnDate:{name:"Return Date"},purchaseAmt:{name:"Purchase Amount"},currency:{name:"Currency"}}}),c("helpers",[],function(){return window.statusButton=function(a){switch(a){case"Open":return"btn-info";case"Approved":return"btn-success";case"Denied":return"btn-danger";case"Booked":return"btn-inverse"}},window.parseExchangeRate=function(a){var b,c;return b=a.query.results.row.name,c=parseFloat(a.query.results.row.rate,10),$("#costin").html(parseFloat($("#cost").val()*c).round(2).format(2)+" AUD")},window.handlePlaceJSONP=function(a){return window.typeahead.process(_.map(a,function(a){return a.replace(/NS/,"NSW")}))}});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("models/user_model",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return this.url=d(this.url,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="UserModel",b.prototype.url=function(){return""+c.restPath+"/api/2/user?username="+this.id},b.prototype.initialize=function(a){var b=this;return this.fetch(a).done(function(b){if(a.callback)return a.callback(b)})},b}(Backbone.Model)});var e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("models/current_user_model",["require","../config","./user_model"],function(a){var b,c,d;return d=a("../config"),c=a("./user_model"),b=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="CurrentUserModel",b.prototype.url=""+d.restPath+"/auth/1/session",b.prototype.initialize=function(){return this.fetch()},b.prototype.fetch=function(){var a=this;return b.__super__.fetch.apply(this,arguments).done(function(){return new c({id:a.get("name"),callback:function(b){return a.set("profile",b),a.setLoggedIn()}})})},b.prototype.setLoggedIn=function(){return $(".curruser").html(this.get("profile").displayName),$(".avatar").attr("src",this.get("profile").avatarUrls["48x48"])},b}(Backbone.Model)});var e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("models/project_model",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="ProjectModel",b.prototype.url=""+c.restPath+"/api/2/project/"+c.projectKey,b.prototype.initialize=function(){return this.fetch().done(function(a){return this.id=a.id})},b}(Backbone.Model)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("models/saved_trip_model",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return this.url=d(this.url,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="SavedTripModel",b.prototype.url=function(){return""+c.restPath+"/api/2/issue/"+this.key},b.prototype.initialize=function(a){var b=this;this.key=a&&a.key?a.key:"";if(this.key)return this.fetch(a).done(function(b){if(a.callback)return a.callback(b)})},b.prototype.parse=function(a){return a.fields?_.extend(a.fields,{id:a.id,key:a.key}):a},b}(Backbone.Model)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("models/trip_model",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return this.url=d(this.url,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="TripModel",b.prototype.url=function(){return""+c.restPath+"/api/2/issue"},b.prototype.initialize=function(a){var b=this;return this.errors=[],this.bind("error",function(a,c){return console.log("TripModel Error",arguments),b.errors=c,c})},b.prototype.parse=function(a){return a.fields?_.extend(a.fields,{id:a.id,key:a.key}):a},b.prototype.validate={summary:{required:!0},description:{required:!0},origin:{required:!0},destination:{required:!0},departDate:{required:!0},returnDate:{required:!0},purchaseAmt:{required:!0},currency:{required:!0},assignee:{required:!0}},b}(Backbone.Model)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("collections/custom_fields_collection",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return this.setFields=d(this.setFields,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="CustomFieldsCollection",b.prototype.url=""+c.restPath+"/api/2/field",b.prototype.initialize=function(){return this.fetch().done(this.setFields)},b.prototype.setFields=function(){var a=this;return _.each(c.customFields,function(b){var c;return c=a.find(function(a){return a.attributes.name===b.name}),_.extend(b,c.attributes)})},b}(Backbone.Collection)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("collections/issue_types_collection",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return this.findTravelReqIssueType=d(this.findTravelReqIssueType,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="IssueTypesCollection",b.prototype.url=""+c.restPath+"/api/2/issuetype",b.prototype.initialize=function(){return this.fetch().done(this.findTravelReqIssueType)},b.prototype.findTravelReqIssueType=function(){var a;return a=this.find(function(a){return a.attributes.name===c.issueTypeName}),this.id=a.attributes.id},b}(Backbone.Collection)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("collections/trips_collection",["require","../config","../models/trip_model"],function(a){var b,c,e;return e=a("../config"),b=a("../models/trip_model"),c=function(a){function c(){return this.comparator=d(this.comparator,this),c.__super__.constructor.apply(this,arguments)}return f(c,a),c.name="TripsCollection",c.prototype.url=""+e.restPath+"/api/2/search",c.prototype.model=b,c.prototype.comparator=function(a,b){return a.get(e.customFields.departDate.id)<b.get(e.customFields.returnDate.id)},c.prototype.parse=function(a){var b=this;return _.map(a.issues,function(a){return _.extend(a.fields,{id:a.id,key:a.key})})},c}(Backbone.Collection)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("collections/users_collection",["require","../config"],function(a){var b,c;return c=a("../config"),b=function(a){function b(){return this.url=d(this.url,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="UsersCollection",b.prototype.url=function(a){return""+c.restPath+"/api/2/user/search?username="+this.q},b.prototype.initialize=function(a){var b=this;return this.q=a.q,this.fetch(a).done(function(b){if(a.callback)return a.callback(b)})},b}(Backbone.Collection)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("views/add_trip_view",["require","../config","../models/trip_model","../models/saved_trip_model","../collections/users_collection"],function(a){var b,c,e,g,h;return h=a("../config"),e=a("../models/trip_model"),c=a("../models/saved_trip_model"),g=a("../collections/users_collection"),b=function(a){function b(){return this.showInvalid=d(this.showInvalid,this),this.render=d(this.render,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="AddTripView",b.prototype.el=$("#addtrip"),b.prototype.template=_.template($("#addtrip-template").html()),b.prototype.initialize=function(a){return this.render()},b.prototype.events={"change #cost":"updateConvertedRate","change #curr":"updateConvertedRate","submit #add-trip-form":"saveTrip"},b.prototype.render=function(){var a=this;return this.$el.html(this.template()),$("#depart-date").data("date",Date.create().format("{M}/{dd}/{yyyy}")),$("#return-date").data("date",Date.create().addDays(7).format("{M}/{dd}/{yyyy}")),$("div.date").datepicker(),$(".place.from").typeahead({property:"origin",source:function(b,c){console.log(1,a);if(c.length<3)return;return window.typeahead=b,a.getPlace(c)}}),$(".place.to").typeahead({property:"destination",source:function(b,c){if(c.length<3)return;return window.typeahead=b,a.getPlace(c)}}),$("#assignee").typeahead({source:function(a,b){if(b.length<3)return;return new g({q:b,callback:function(b){return a.process(_.pluck(b,"name"))}})}}),$("#cost, #curr").on("change keyup",function(){}),this},b.prototype.getRate=function(a,b){var c;return $("#currscript").remove(),c=document.createElement("script"),c.setAttribute("src","http://query.yahooapis.com/v1/public/yql?q=select%20rate%2Cname%20from%20csv%20where%20url%3D'http%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes%3Fs%3D"+a+b+"%253DX%26f%3Dl1n'%20and%20columns%3D'rate%2Cname'&format=json&callback=parseExchangeRate"),c.setAttribute("id","currscript"),document.body.appendChild(c)},b.prototype.updateConvertedRate=function(){return this.getRate($("#curr").val(),"AUD")},b.prototype.getPlace=function(a){var b;return $("#placescript").remove(),b=document.createElement("script"),b.setAttribute("src","http://gd.geobytes.com/AutoCompleteCity?q="+a+"&callback=handlePlaceJSONP"),b.setAttribute("id","placescript"),document.body.appendChild(b)},b.prototype.focus=function(){var a,b=this;return a=function(){return b.$el.find("input[type='text']:first").focus()},setTimeout(a,0)},b.prototype.showInvalid=function(a,b){var c;for(c in b)this.$el.find("."+c).addClass("error");return!1},b.prototype.showSavedToast=function(){return this.flash=new FlashView({flash:{title:"Success",message:"Your travel approval request has been saved and sent for approval"}})},b.prototype.saveTrip=function(){var a,b,d,f=this;return this.$(".error").removeClass("error"),b=$("form").serializeObject(),b[h.customFields.departDate.id]=Date.create($("#depart-date input").data("date")).format(Date.ISO8601_DATETIME),b[h.customFields.returnDate.id]=Date.create($("#return-date input").data("date")).format(Date.ISO8601_DATETIME),d={fields:{issuetype:{id:issuetype.id},project:{id:project.id}}},_.extend(d.fields,b.summary?{summary:b.summary}:void 0),_.extend(d.fields,b.description?{description:b.description}:void 0),b.origin&&(d.fields[h.customFields.origin.id]=b.origin),b.destination&&(d.fields[h.customFields.destination.id]=b.destination),b[h.customFields.departDate.id]&&(d.fields[h.customFields.departDate.id]=b[h.customFields.departDate.id]),b[h.customFields.returnDate.id]&&(d.fields[h.customFields.returnDate.id]=b[h.customFields.returnDate.id]),b.purchaseAmt&&(d.fields[h.customFields.purchaseAmt.id]=parseInt(b.purchaseAmt)),b.currency&&(d.fields[h.customFields.currency.id]={},d.fields[h.customFields.currency.id].value=b.currency),_.extend(d.fields,b.assignee?{assignee:{name:"alex"}}:void 0),this.model=new e,this.model.on("error",this.showInvalid),this.model.set(b),this.model.errors.length===0&&(a=this.model.save(d,{success:function(a,b){var d;return f.render(),f.showSavedToast(),console.log("success",f,arguments),d=new c({key:b.key,callback:function(a){return console.log("callback",d),f.collection.add(d)}})},error:function(a,b){}})),!1},b}(Backbone.View)});var e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("views/flash_view",[],function(){var a;return a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="FlashView",b.prototype.tagName="div",b.prototype.className="alert",b.prototype.template=_.template($("#flash-template").html()),b.prototype.initialize=function(a){return this.render()},b.prototype.render=function(){return this.options.flash["class"]&&this.$el.addClass(this.options.flash["class"]),this.$el.html(this.template(this.options.flash)).prependTo(".flash")},b}(Backbone.View)});var e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("views/my_trip_view",[],function(){var a;return a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="MyTripView",b.prototype.template=_.template($("#mytrip-template").html()),b.prototype.tagName="tr",b.prototype.initialize=function(a){return this.model.bind("change",this.render),this.model.bind("destroy",this.remove)},b.prototype.render=function(){if(this.model)return this.$el.html(this.template(this.model))},b.prototype.remove=function(){return this.$el.remove()},b}(Backbone.View)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("views/my_trips_view",["require","./my_trip_view"],function(a){var b,c;return b=a("./my_trip_view"),c=function(a){function c(){return this.addOne=d(this.addOne,this),c.__super__.constructor.apply(this,arguments)}return f(c,a),c.name="MyTripsView",c.prototype.el=$("#mytrips"),c.prototype.template=_.template($("#mytrips-template").html()),c.prototype.initialize=function(a){return this.collection.on("add",this.addOne),this.collection.on("change",this.render,this),this.render()},c.prototype.render=function(){return this.$el.html(this.template()),this.addAll()},c.prototype.addOne=function(a){var c;return c=new b({model:a}),c.render(),this.$el.find("#mytrips-rows").prepend(c.el),a.bind("remove",c.remove)},c.prototype.addAll=function(){return this.collection.each(this.addOne)},c}(Backbone.View)});var e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("views/pending_trips_view",[],function(){var a;return a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="PendingTripsView",b.prototype.el=$("#pendingtrips-rows"),b.prototype.template=_.template($("#pendingtrips-template").html()),b.prototype.initialize=function(a){return this.render()},b.prototype.events={"click .complete-booking":"completeBooking"},b.prototype.completeBooking=function(a){var b,c,d=this;return b=$(a.currentTarget),c=b.data(),$.get(""+config.restPath+"/api/2/issue/"+c.key+"/transitions",function(a){var e;return e=_.find(a.transitions,function(a){return a.name===config.bookTransitionName}),$.ajax({url:""+config.restPath+"/api/2/issue/"+c.key+"/transitions",type:"post",dataType:"json",contentType:"application/json",processData:!1,data:JSON.stringify({transition:{id:e.id}}),success:function(){return b.removeClass("btn-success").addClass("btn-inverse").addClass("disabled").html("Booked"),d.collection.get(c.id).set("status",{name:"Booked"},{silent:!0}),d.collection.trigger("change")}})}),!1},b.prototype.render=function(){var a=this;return this.collection.filter(function(a){return a.attributes.status.name==="Approved"}).sort(function(a,b){return a.get(config.customFields.departDate.id)>b.get(config.customFields.returnDate.id)}).each(function(b){return a.$el.append(a.template(b))})},b}(Backbone.View)});var d=function(a,b){return function(){return a.apply(b,arguments)}},e={}.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};c("router",[],function(){var a;return a=function(a){function b(){return this.addTrip=d(this.addTrip,this),b.__super__.constructor.apply(this,arguments)}return f(b,a),b.name="Router",b.prototype.routes={"":"addTrip",trips:"myTrips","trip/book":"bookTrip","trip/new":"addTrip","*path":"notFound"},b.prototype.initialize=function(a){return this.addTripView=a.addTripView},b.prototype.before=function(){return $(".flash .alert").remove()},b.prototype.activate=function(a){return $(".content").addClass("hidden"),$("#"+a).removeClass("hidden"),$(".topnav").removeClass("active"),$("."+a).addClass("active"),this},b.prototype.addTrip=function(){return this.activate("addtrip"),this.addTripView.focus()},b.prototype.myTrips=function(){return this.activate("mytrips")},b.prototype.bookTrip=function(){return this.activate("booktrip")},b.prototype.notFound=function(){return this.activate("404")},b}(Backbone.Router)}),c("app",["require","config","helpers","models/user_model","models/current_user_model","models/project_model","models/saved_trip_model","models/trip_model","collections/custom_fields_collection","collections/issue_types_collection","collections/trips_collection","collections/users_collection","views/add_trip_view","views/flash_view","views/my_trip_view","views/my_trips_view","views/pending_trips_view","router"],function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y;return s=a("config"),v=a("helpers"),o=a("models/user_model"),c=a("models/current_user_model"),j=a("models/project_model"),l=a("models/saved_trip_model"),m=a("models/trip_model"),d=a("collections/custom_fields_collection"),f=a("collections/issue_types_collection"),n=a("collections/trips_collection"),p=a("collections/users_collection"),b=a("views/add_trip_view"),e=a("views/flash_view"),g=a("views/my_trip_view"),h=a("views/my_trips_view"),i=a("views/pending_trips_view"),k=a("router"),window.config=s,t=new c,y=new n,x=new j,w=new f,u=new d,q=new b({collection:y}),y.fetch().done(function(){var a,b;return a=new h({collection:y}),b=new i({collection:y})}),r=new k({addTripView:q}),Backbone.history.start()}),b(["app"]),c("main",function(){})})()

src/main/resources/js/models/current_user_model.js

 var __hasProp = {}.hasOwnProperty,
   __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
 
-define(['../config', './user_model'], function(config, UserModel) {
-  var CurrentUserModel;
-  console.log(1, arguments);
+define(function(require) {
+  var CurrentUserModel, UserModel, config;
+  config = require('../config');
+  UserModel = require('./user_model');
   return CurrentUserModel = (function(_super) {
 
     __extends(CurrentUserModel, _super);
     };
 
     CurrentUserModel.prototype.setLoggedIn = function() {
-      $('.curruser').html(currentUser.get('profile').displayName);
-      return $('.avatar').attr('src', currentUser.get(&#