This commit is contained in:
Sven Fuchs 2012-06-23 17:44:28 +02:00
parent 2437f79845
commit f7aeda897b
27 changed files with 2192 additions and 524 deletions

View File

@ -1,3 +1,7 @@
# TODO add neuter for production
# https://github.com/wycats/rake-pipeline-web-filters/blob/master/lib/rake-pipeline-web-filters/neuter_filter.rb
# http://blog.nulayer.com/post/23294069876/building-simple-javascript-libraries-with-rake-pipeline?15bb0420
require 'rake-pipeline-web-filters'
output 'public/javascripts'

View File

@ -1,8 +1,42 @@
$.mockjax
url: '/repositories',
responseTime: 0,
responseText:
repositories: [
{ id: 1, owner: 'travis-ci', name: 'travis-core', slug: 'travis-ci/travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_result: 0 },
{ id: 2, owner: 'travis-ci', name: 'travis-assets', slug: 'travis-ci/travis-assets', build_ids: [3], last_build_id: 3, last_build_number: 3},
{ id: 3, owner: 'travis-ci', name: 'travis-hub', slug: 'travis-ci/travis-hub', build_ids: [4], last_build_id: 4, last_build_number: 4},
],
$.mockjax
url: '/travis-ci/travis-core',
responseTime: 0,
responseText:
repository: { id: 1, owner: 'travis-ci', name: 'travis-core', slug: 'travis-ci/travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_result: 0 }
$.mockjax
url: '/travis-ci/travis-assets',
responseTime: 0,
responseText:
repository: { id: 1, owner: 'travis-ci', name: 'travis-core', slug: 'travis-ci/travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_result: 0 }
$.mockjax
url: '/builds/1',
resposeTime: 0,
responseText:
build: { id: 1, repository_id: 'travis-ci/travis-core', commit_id: 1, job_ids: [1, 2], number: 1, event_type: 'push', config: { rvm: ['rbx', '1.9.3'] }, finished_at: '2012-06-20T00:21:20Z', duration: 35, result: 0 }
$.mockjax
url: '/builds/2',
resposeTime: 0,
responseText:
build: { id: 1, repository_id: 'travis-ci/travis-assets', commit_id: 1, job_ids: [1, 2], number: 1, event_type: 'push', config: { rvm: ['rbx'] }, finished_at: '2012-06-20T00:21:20Z', duration: 35, result: 0 }
@Travis = Em.Application.create()
require 'ext/jquery'
require 'locales'
require 'travis/data_store_adapter'
require 'travis/data_store/rest_adapter'
require 'helpers'
require 'models'
require 'views'
@ -13,14 +47,8 @@ require 'routes'
# Travis = window.Travis
Travis.store = DS.Store.extend(
revision: 4
adapter: Travis.FixtureAdapter.create()
adapter: Travis.RestAdapter.create()
# adapter: Travis.FixtureAdapter.create()
).create()
# apparently fixtures behave weird unless preloaded :/ should move to mockjax for testing
Travis.Repository.find()
Travis.Build.find()
Travis.Commit.find()
Travis.Job.find()
Travis.Artifact.find()
Travis.initialize()

View File

@ -1,23 +0,0 @@
window.Util = window.Util || {}
window.Util.counter = (function () {
var value = 0;
return {
getValue: function () {
return value;
},
increment: function (i) {
if (!i) i = 1;
return value += i;
},
decrement: function (i) {
if (!i) i = 1;
return value -= i;
},
reset: function (i) {
if (!i) i = 0;
return value = i;
}
}
}());

View File

@ -28,30 +28,30 @@ require 'travis/model'
@getPath('data.job_ids.length') > 1
).property('data.job_ids.length')
isFailureMatrix: (->
@getPath('allowedFailureJobs.length') > 0
).property('allowedFailureJobs.length')
# isFailureMatrix: (->
# @getPath('allowedFailureJobs.length') > 0
# ).property('allowedFailureJobs.length')
# TODO why does the hasMany association not work?
jobs: (->
Travis.Job.findMany(@getPath('data.job_ids'))
).property('data.job_ids.length')
# # TODO why does the hasMany association not work?
# jobs: (->
# Travis.Job.findMany(@getPath('data.job_ids') || [])
# ).property('data.job_ids.length')
requiredJobs: (->
@get('jobs').filter (job) -> job.get('allow_failure') != true
).property('jobs')
# requiredJobs: (->
# @get('jobs').filter (job) -> job.get('allow_failure') != true
# ).property('jobs')
allowedFailureJobs: (->
@get('jobs').filter (job) -> job.get 'allow_failure'
).property('jobs')
# allowedFailureJobs: (->
# @get('jobs').filter (job) -> job.get 'allow_failure'
# ).property('jobs')
configKeys: (->
config = @get('config')
return [] unless config
keys = $.keys($.only(config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala'))
headers = [I18n.t('build.job'), I18n.t('build.duration'), I18n.t('build.finished_at')]
$.map(headers.concat(keys), (key) -> return $.camelize(key))
).property('config')
# configKeys: (->
# config = @get('config')
# return [] unless config
# keys = $.keys($.only(config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala'))
# headers = [I18n.t('build.job'), I18n.t('build.duration'), I18n.t('build.finished_at')]
# $.map(headers.concat(keys), (key) -> return $.camelize(key))
# ).property('config')
tick: ->
@notifyPropertyChange 'duration'

View File

@ -11,8 +11,6 @@ require 'travis/model'
primaryKey: 'slug'
lastBuild: DS.belongsTo('Travis.Build')
builds: (->
Travis.Build.byRepositoryId @get('id'), event_type: 'push'
).property()
@ -22,13 +20,22 @@ require 'travis/model'
).property()
owner: (->
(@get('slug') || '').split('/')[0]
(@get('slug') || @_id).split('/')[0]
).property('owner', 'name'),
name: (->
(@get('slug') || '').split('/')[1]
(@get('slug') || @_id).split('/')[1]
).property('owner', 'name'),
# TODO this is used in router#serializeObject for the last_build links in the
# repositories list. should be in some item controller i guess, but i'm not
# sure how to use one with #each
lastBuild: (->
owner: @get('owner')
name: @get('name')
id: @get('last_build_id')
).property('last_build_id')
last_build_duration: (->
duration = @getPath('data.last_build_duration')
duration = Travis.Helpers.durationFrom(@get('last_build_started_at'), @get('last_build_finished_at')) unless duration
@ -67,10 +74,7 @@ require 'travis/model'
@find().forEach (repository) ->
repository.set 'selected', repository.get('id') is id
@Travis.Repository.FIXTURES = [
{ id: 1, owner: 'travis-ci', name: 'travis-core', slug: 'travis-ci/travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_result: 0 },
{ id: 2, owner: 'travis-ci', name: 'travis-assets', slug: 'travis-ci/travis-assets', build_ids: [3], last_build_id: 3, last_build_number: 3},
{ id: 3, owner: 'travis-ci', name: 'travis-hub', slug: 'travis-ci/travis-hub', build_ids: [4], last_build_id: 4, last_build_number: 4},
]
buildURL: (slug) ->
if slug then slug else 'repositories'

View File

@ -5,83 +5,98 @@ require 'hax0rs'
location: 'hash'
root: Em.Route.extend
viewCurrent: Ember.Route.transitionTo('current')
viewBuilds: Ember.Route.transitionTo('builds')
viewBuild: Ember.Route.transitionTo('build')
viewJob: Ember.Route.transitionTo('job')
index: Em.Route.extend
default: Em.Route.extend
route: '/'
viewCurrent: Ember.Route.transitionTo('current')
viewBuilds: Ember.Route.transitionTo('builds')
viewBuild: Ember.Route.transitionTo('build')
viewJob: Ember.Route.transitionTo('job')
connectOutlets: (router) ->
repositories = Travis.Repository.find()
router.connectLeft(repositories)
onceLoaded repositories, =>
repository = repositories.get('firstObject')
router.set('repositories', repositories)
index: Em.Route.extend
route: '/'
connectOutlets: (router) ->
repositories = router.get('repositories')
onceLoaded repositories, =>
repository = repositories.get('firstObject')
router.connectRepository repository
router.connectTabs repository
router.connectCurrent repository.get('lastBuild')
current: Em.Route.extend
route: '/:owner/:name'
serialize: (router, repository) ->
router.serializeRepository(repository)
deserialize: (router, params) ->
router.deserializeRepository(params)
connectOutlets: (router, repository) ->
router.connectRepository repository
router.connectTabs repository
router.connectCurrent repository.get('lastBuild')
current: Em.Route.extend
route: '/:owner/:name'
builds: Em.Route.extend
route: '/:owner/:name/builds'
serialize: (router, repository) ->
router.serializeRepository repository
serialize: (router, repository) ->
router.serializeRepository repository
connectOutlets: (router, repository) ->
repositories = Travis.Repository.find()
router.connectLeft(repositories)
onceLoaded repository, =>
router.connectRepository repository
router.connectTabs repository
router.connectCurrent repository.get('lastBuild')
deserialize: (router, params) ->
router.deserializeRepository(params)
builds: Em.Route.extend
route: '/:owner/:name/builds'
serialize: (router, repository) ->
router.serializeRepository repository
connectOutlets: (router, repository) ->
repositories = Travis.Repository.find()
router.connectLeft(repositories)
onceLoaded repository, =>
connectOutlets: (router, repository) ->
router.connectRepository repository
router.connectTabs repository
router.connectBuilds repository.get('builds')
build: Em.Route.extend
route: '/:owner/:name/builds/:id'
build: Em.Route.extend
route: '/:owner/:name/builds/:id'
serialize: (router, build) ->
router.serializeObject build
serialize: (router, build) ->
r = router.serializeObject build
console.log(r.owner, r.name, r.id)
r
connectOutlets: (router, build) ->
repositories = Travis.Repository.find()
repository = build.get('repository')
deserialize: (router, params) ->
router.deserializeBuild(params)
router.connectLeft(repositories)
onceLoaded repository, =>
router.connectRepository repository
router.connectTabs repository, build
router.connectBuild build
connectOutlets: (router, build) ->
build = Travis.Build.find(build.id) unless build instanceof Travis.Build
onceLoaded build, =>
repository = build.get('repository')
onceLoaded repository, =>
router.connectRepository repository
router.connectTabs repository, build
router.connectBuild build
job: Em.Route.extend
route: '/:owner/:name/jobs/:id'
job: Em.Route.extend
route: '/:owner/:name/jobs/:id'
serialize: (router, job) ->
router.serializeObject job
serialize: (router, job) ->
router.serializeObject job
connectOutlets: (router, job) ->
repositories = Travis.Repository.find()
repository = job.get('repository')
build = job.get('build')
deserialize: (router, params) ->
router.deserializeBuild(params)
router.connectLeft(repositories)
onceLoaded repository, build, =>
router.connectRepository repository
router.connectTabs repository, build, job
router.connectJob job
connectOutlets: (router, job) ->
# repositories = Travis.Repository.find()
# job = Travis.Job.find(job.id) unless job instanceof Travis.Job
# repository = job.get('repository')
# build = job.get('build')
# router.connectLeft(repositories)
# onceLoaded repository, build, =>
# router.connectRepository repository
# router.connectTabs repository, build, job
# router.connectJob job
connectLeft: (repositories) ->
@ -109,17 +124,30 @@ require 'hax0rs'
@get('repositoryController').connectOutlet outletName: 'tab', name: 'job', context: job
serializeRepository: (repository) ->
if repository instanceof DS.Model
repository.getProperties 'owner', 'name'
serializeRepository: (object) ->
if object instanceof DS.Model
slug = object.get('slug') || object._id # wat.
parts = slug.split('/')
{ owner: parts[0], name: parts[1] }
else
repository or {}
object || {}
serializeObject: (object) ->
if object instanceof DS.Model
repository = object.get('repository')
params = @serializeRepository(repository)
object.get('id') || debugger
$.extend params,
id: object.get('id')
else
object or {}
deserializeRepository: (params) ->
Travis.Repository.find("#{params.owner}/#{params.name}")
deserializeBuild: (params) ->
Travis.Build.find(params.id)
deserializeJob: (params) ->
Travis.Job.find(params.id)

View File

@ -1,46 +1,48 @@
<div {{bindAttr class="classes"}}>
<dl class="summary clearfix">
<div class="left">
<dt>{{t builds.name}}</dt>
<dd class="number"><a {{bindAttr href="urlBuild"}}>{{number}}</a></dd>
<dt class="finished_at_label">{{t builds.finished_at}}</dt>
<dd class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</dd>
<dt>{{t builds.duration}}</dt>
<dd class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</dd>
</div>
{{#if isLoaded}}
<div {{bindAttr class="classes"}}>
<dl class="summary clearfix">
<div class="left">
<dt>{{t builds.name}}</dt>
<dd class="number"><a {{bindAttr href="urlBuild"}}>{{number}}</a></dd>
<dt class="finished_at_label">{{t builds.finished_at}}</dt>
<dd class="finished_at timeago" {{bindAttr title="finished_at"}}>{{formatTime finished_at}}</dd>
<dt>{{t builds.duration}}</dt>
<dd class="duration" {{bindAttr title="started_at"}}>{{formatDuration duration}}</dd>
</div>
<div class="right">
<dt>{{t builds.commit}}</dt>
<dd class="commit-hash"><a {{bindAttr href="urlGithubCommit"}}>{{formatCommit commit}}</a></dd>
{{#if commit.compare_url}}
<dt>{{t builds.compare}}</dt>
<dd class="compare_view"><a {{bindAttr href="commit.compare_url"}}>{{pathFrom commit.compare_url}}</a></dd>
{{/if}}
{{#if commit.author_name}}
<dt>{{t builds.author}}</dt>
<dd class="author"><a {{bindAttr href="view.urlAuthor"}}>{{commit.author_name}}</a></dd>
{{/if}}
{{#if commit.committer_name}}
<dt>{{t builds.committer}}</dt>
<dd class="committer"><a {{bindAttr href="urlCommitter"}}>{{commit.committer_name}}</a></dd>
{{/if}}
</div>
<div class="right">
<dt>{{t builds.commit}}</dt>
<dd class="commit-hash"><a {{bindAttr href="urlGithubCommit"}}>{{formatCommit commit}}</a></dd>
{{#if commit.compare_url}}
<dt>{{t builds.compare}}</dt>
<dd class="compare_view"><a {{bindAttr href="commit.compare_url"}}>{{pathFrom commit.compare_url}}</a></dd>
{{/if}}
{{#if commit.author_name}}
<dt>{{t builds.author}}</dt>
<dd class="author"><a {{bindAttr href="view.urlAuthor"}}>{{commit.author_name}}</a></dd>
{{/if}}
{{#if commit.committer_name}}
<dt>{{t builds.committer}}</dt>
<dd class="committer"><a {{bindAttr href="urlCommitter"}}>{{commit.committer_name}}</a></dd>
{{/if}}
</div>
<dt>{{t builds.message}}</dt>
<dd class="commit-message">{{{formatMessage commit.message}}}</dd>
<dt>{{t builds.message}}</dt>
<dd class="commit-message">{{{formatMessage commit.message}}}</dd>
{{#if isMatrix}}
{{else}}
<dt>{{t builds.config}}</dt>
<dd class="config">{{formatConfig config}}</dd>
{{#if isMatrix}}
{{else}}
<dt>{{t builds.config}}</dt>
<dd class="config">{{formatConfig config}}</dd>
{{/if}}
</dl>
{{#if isLoaded}}
{{#if isMatrix}}
{{view Travis.JobsView}}
{{else}}
{{view Travis.LogView}}
{{/if}}
{{/if}}
</dl>
{{#if isLoaded}}
{{#if isMatrix}}
{{view Travis.JobsView}}
{{else}}
{{view Travis.LogView}}
{{/if}}
{{/if}}
</div>
</div>
{{/if}}

View File

@ -1,20 +1,22 @@
<ul>
{{#each content}}
<li>
<div class="wrapper">
<a {{action viewCurrent href=true}} class="slug">{{slug}}</a>
<a {{action viewBuild href=true context="lastBuild"}} class="build">#{{last_build_number}}</a>
</div>
<p class="summary">
<span class="duration_label">{{t repositories.duration}}:</span>
<abbr class="duration" {{bindAttr title="last_build_started_at"}}>{{formatDuration last_build_duration}}</abbr>,
<span class="finished_at_label">{{t repositories.finished_at}}:</span>
<abbr class="finished_at timeago" {{bindAttr title="last_build_finished_at"}}>{{formatTime last_build_finished_at}}</abbr>
</p>
{{#if description}}
<p class="description">{{description}}</p>
{{/if}}
<span class="indicator"></span>
</li>
{{/each}}
<ul>
{{#if content.lastObject.isLoaded}}
<ul>
{{#each content}}
<li>
<div class="wrapper">
<a {{action viewCurrent href=true}} class="slug">{{slug}}</a>
<a {{action viewBuild href=true context="lastBuild"}} class="build">#{{last_build_number}}</a>
</div>
<p class="summary">
<span class="duration_label">{{t repositories.duration}}:</span>
<abbr class="duration" {{bindAttr title="last_build_started_at"}}>{{formatDuration last_build_duration}}</abbr>,
<span class="finished_at_label">{{t repositories.finished_at}}:</span>
<abbr class="finished_at timeago" {{bindAttr title="last_build_finished_at"}}>{{formatTime last_build_finished_at}}</abbr>
</p>
{{#if description}}
<p class="description">{{description}}</p>
{{/if}}
<span class="indicator"></span>
</li>
{{/each}}
<ul>
{{/if}}

View File

@ -12,7 +12,7 @@ window.onceLoaded = ->
callback = objects.pop()
# sadly Ember.Enumerable.compact does not remove undefined values
objects = (if object then object else null for object in objects).compact()
objects = ((object || null) for object in objects).compact()
object = objects.shift()
if object

View File

@ -0,0 +1,30 @@
@Travis.FixtureAdapter = DS.Adapter.extend
find: (store, type, id) ->
fixtures = type.FIXTURES
Ember.assert "Unable to find fixtures for model type " + type.toString(), !!fixtures
return if fixtures.hasLoaded
setTimeout (->
store.loadMany type, fixtures
fixtures.hasLoaded = true
), 300
findMany: ->
@find.apply this, arguments
findAll: (store, type) ->
fixtures = type.FIXTURES
Ember.assert "Unable to find fixtures for model type " + type.toString(), !!fixtures
ids = fixtures.map (item, index, self) ->
item.id
store.loadMany type, ids, fixtures
findQuery: (store, type, params, array) ->
fixtures = type.FIXTURES
Ember.assert "Unable to find fixtures for model type " + type.toString(), !!fixtures
hashes = for fixture in fixtures
matches = for key, value of params
key == 'orderBy' || fixture[key] == value
if matches.reduce((a, b) -> a && b) then fixture else null
array.load(hashes.compact())

View File

@ -0,0 +1,76 @@
@Travis.RestAdapter = DS.RESTAdapter.extend
init: ->
@_super()
# TODO should be able to specify these as strings
@set 'mappings',
builds: Travis.Build,
commits: Travis.Commit,
jobs: Travis.Job
service_hooks: Travis.ServiceHook
plurals:
repository: 'repositories',
branch: 'branches'
find: (store, type, id) ->
url = '/' + type.buildURL(id)
console.log "find: #{url} (#{id})"
@ajax url, 'GET',
success: (json) ->
root = type.singularName()
@sideload(store, type, json, root)
store.load(type, json[root])
accepts:
json: 'application/vnd.travis-ci.2+json'
findMany: (store, type, ids) ->
url = '/' + type.buildURL()
console.log "findMany: #{url} (#{ids})"
@ajax url, 'GET',
data:
ids: ids
success: (json) ->
root = type.pluralName()
@sideload(store, type, json, root)
store.loadMany(type, json[root])
accepts:
json: 'application/vnd.travis-ci.2+json'
findAll: (store, type) ->
url = '/' + type.buildURL()
console.log "findAll: #{url}"
@ajax url, 'GET',
success: (json) ->
root = type.pluralName()
@sideload(store, type, json, root)
store.loadMany(type, json[root])
accepts:
json: 'application/vnd.travis-ci.2+json'
findQuery: (store, type, query, recordArray) ->
url = '/' + type.buildURL(id)
console.log "findQuery: #{url} (#{query})"
@ajax url, 'GET',
data: query,
success: (json) ->
root = type.pluralName()
@sideload(store, type, json, root)
recordArray.load(json[root])
accepts:
json: 'application/vnd.travis-ci.2+json'
updateRecord: (store, type, record) ->
id = get(record, record.get('primaryKey') || 'id')
url = '/' + type.buildURL(id)
data = root: record.toJSON()
@ajax url, 'PUT',
data: data
success: (json) ->
root = type.singularName()
@sideload(store, type, json, root)
store.didUpdateRecord(record, json && json[root])

View File

@ -1,127 +0,0 @@
@Travis.FixtureAdapter = DS.Adapter.extend
find: (store, type, id) ->
fixtures = type.FIXTURES
Ember.assert "Unable to find fixtures for model type " + type.toString(), !!fixtures
return if fixtures.hasLoaded
setTimeout (->
store.loadMany type, fixtures
fixtures.hasLoaded = true
), 300
findMany: ->
@find.apply this, arguments
findAll: (store, type) ->
fixtures = type.FIXTURES
Ember.assert "Unable to find fixtures for model type " + type.toString(), !!fixtures
ids = fixtures.map (item, index, self) ->
item.id
store.loadMany type, ids, fixtures
findQuery: (store, type, params, array) ->
fixtures = type.FIXTURES
Ember.assert "Unable to find fixtures for model type " + type.toString(), !!fixtures
hashes = for fixture in fixtures
matches = for key, value of params
key == 'orderBy' || fixture[key] == value
if matches.reduce((a, b) -> a && b) then fixture else null
array.load(hashes.compact())
@Travis.DataStoreAdapter = DS.RESTAdapter.extend
init: ->
@_super()
# TODO should be able to specify these as strings
@set 'mappings',
builds: Travis.Build,
commits: Travis.Commit,
jobs: Travis.Job
service_hooks: Travis.ServiceHook
plurals:
repository: 'repositories',
branch: 'branches'
updateRecord: (store, type, record) ->
id = get(record, record.get('primaryKey') || 'id')
root = @rootForType(type)
plural = @pluralize(root)
url = @buildURL(type.url || plural, id)
data = root: record.toJSON()
@ajax url, 'PUT',
data: data
success: (json) ->
@sideload(store, type, json, root)
store.didUpdateRecord(record, json && json[root])
find: (store, type, id) ->
root = @rootForType(type)
plural = @pluralize(root)
url = @buildURL(type.url || plural, id)
@ajax url, 'GET',
success: (json) ->
@sideload(store, type, json, root)
store.load(type, json[root])
accepts:
json: 'application/vnd.travis-ci.2+json'
findMany: (store, type, ids) ->
root = @rootForType(type)
plural = @pluralize(root)
url = @buildURL(type.url || plural)
@ajax url, 'GET',
data:
ids: ids
success: (json) ->
@sideload(store, type, json, plural)
store.loadMany(type, json[plural])
accepts:
json: 'application/vnd.travis-ci.2+json'
findAll: (store, type) ->
root = @rootForType(type)
plural = @pluralize(root)
url = @buildURL(type.url || plural)
@ajax url, 'GET',
success: (json) ->
@sideload(store, type, json, plural)
store.loadMany(type, json[plural])
accepts:
json: 'application/vnd.travis-ci.2+json'
findQuery: (store, type, query, recordArray) ->
root = @rootForType(type)
plural = @pluralize(root)
url = @buildURL(type.url || plural)
@ajax url, 'GET',
data: query,
success: (json) ->
@sideload(store, type, json, plural)
recordArray.load(json[plural])
accepts:
json: 'application/vnd.travis-ci.2+json'
rootForType: (type) ->
# sorry, but this seems very weird, really
# return type.url if (type.url)
parts = type.toString().split('.')
name = parts[parts.length - 1]
name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1)
buildURL: (record, suffix) ->
Ember.assert('Namespace URL (' + @namespace + ') must not start with slash', !@namespace || @namespace.toString().charAt(0) != '/')
Ember.assert('Record URL (' + record + ') must not start with slash', !record || record.toString().charAt(0) != '/')
Ember.assert('URL suffix (' + suffix + ') must not start with slash', !suffix || suffix.toString().charAt(0) != '/')
url = ['']
url.push(@namespace) if (@namespace != undefined)
url.push(record)
url.push(suffix) if (suffix != undefined)
url.join('/')

View File

@ -4,7 +4,7 @@
refresh: ->
id = @get('id')
Travis.app.store.adapter.find(Travis.app.store, @constructor, id) if id
Travis.store.adapter.find(Travis.store, @constructor, id) if id
update: (attrs) ->
$.each attrs, (key, value) =>
@ -13,5 +13,21 @@
@Travis.Model.reopenClass
load: (attrs) ->
Travis.app.store.load(this, attrs)
Travis.store.load(this, attrs)
buildURL: (suffix) ->
base = @url || @pluralName()
Ember.assert('Base URL (' + base + ') must not start with slash', !base || base.toString().charAt(0) != '/')
Ember.assert('URL suffix (' + suffix + ') must not start with slash', !suffix || suffix.toString().charAt(0) != '/')
url = [base]
url.push(suffix) if (suffix != undefined)
url.join('/')
singularName: ->
parts = @toString().split('.')
name = parts[parts.length - 1]
name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1)
pluralName: ->
Travis.store.adapter.pluralize(@singularName())

View File

@ -102,7 +102,7 @@ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
(function() {
var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
var Set = function() {
this.hash = {};
@ -138,7 +138,7 @@ Set.prototype = {
}
};
var ManyArrayState = Ember.State.extend({
var LoadedState = Ember.State.extend({
recordWasAdded: function(manager, record) {
var dirty = manager.dirty, observer;
dirty.add(record);
@ -167,7 +167,21 @@ var ManyArrayState = Ember.State.extend({
});
var states = {
clean: ManyArrayState.create({
loading: Ember.State.create({
isLoaded: false,
isDirty: false,
loadedRecords: function(manager, count) {
manager.decrement(count);
},
becameLoaded: function(manager) {
manager.transitionTo('clean');
}
}),
clean: LoadedState.create({
isLoaded: true,
isDirty: false,
recordWasAdded: function(manager, record) {
@ -181,7 +195,8 @@ var states = {
}
}),
dirty: ManyArrayState.create({
dirty: LoadedState.create({
isLoaded: true,
isDirty: true,
childWasSaved: function(manager, child) {
@ -199,12 +214,32 @@ var states = {
DS.ManyArrayStateManager = Ember.StateManager.extend({
manyArray: null,
initialState: 'clean',
initialState: 'loading',
states: states,
/**
This number is used to keep track of the number of outstanding
records that must be loaded before the array is considered
loaded. As results stream in, this number is decremented until
it becomes zero, at which case the `isLoaded` flag will be set
to true
*/
counter: 0,
init: function() {
this._super();
this.dirty = new Set();
this.counter = getPath(this, 'manyArray.length');
},
decrement: function(count) {
var counter = this.counter = this.counter - count;
Ember.assert("Somehow the ManyArray loaded counter went below 0. This is probably an ember-data bug. Please report it at https://github.com/emberjs/data/issues", counter >= 0);
if (counter === 0) {
this.send('becameLoaded');
}
}
});
@ -213,7 +248,7 @@ DS.ManyArrayStateManager = Ember.StateManager.extend({
(function() {
var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
var get = Ember.get, set = Ember.set, getPath = Ember.getPath, setPath = Ember.setPath;
DS.ManyArray = DS.RecordArray.extend({
init: function() {
@ -228,20 +263,24 @@ DS.ManyArray = DS.RecordArray.extend({
return getPath(this, 'stateManager.currentState.isDirty');
}).property('stateManager.currentState').cacheable(),
isLoaded: Ember.computed(function() {
return getPath(this, 'stateManager.currentState.isLoaded');
}).property('stateManager.currentState').cacheable(),
send: function(event, context) {
this.get('stateManager').send(event, context);
},
fetch: function() {
var clientIds = get(this, 'content'),
store = get(this, 'store'),
type = get(this, 'type');
var ids = clientIds.map(function(clientId) {
return store.clientIdToId[clientId];
});
store.fetchMany(type, ids);
store.fetchUnloadedClientIds(type, clientIds);
},
// Overrides Ember.Array's replace method to implement
replace: function(index, removed, added) {
replaceContent: function(index, removed, added) {
var parentRecord = get(this, 'parentRecord');
var pendingParent = parentRecord && !get(parentRecord, 'id');
var stateManager = get(this, 'stateManager');
@ -258,7 +297,10 @@ DS.ManyArray = DS.RecordArray.extend({
record.send('waitingOn', parentRecord);
}
this.assignInverse(record, parentRecord);
var oldParent = this.assignInverse(record, parentRecord);
record.get('transaction')
.relationshipBecameDirty(record, oldParent, parentRecord);
stateManager.send('recordWasAdded', record);
@ -271,7 +313,10 @@ DS.ManyArray = DS.RecordArray.extend({
for (var i = index; i < len; i++) {
// TODO: null out inverse FK
record = this.objectAt(i);
this.assignInverse(record, parentRecord, true);
var oldParent = this.assignInverse(record, parentRecord, true);
record.get('transaction')
.relationshipBecameDirty(record, parentRecord, null);
// If we put the child record into a pending state because
// we were waiting on the parent record to get an id, we
@ -289,7 +334,7 @@ DS.ManyArray = DS.RecordArray.extend({
assignInverse: function(record, parentRecord, remove) {
var associationMap = get(record.constructor, 'associations'),
possibleAssociations = associationMap.get(parentRecord.constructor),
possible, actual;
possible, actual, oldParent;
if (!possibleAssociations) { return; }
@ -303,7 +348,9 @@ DS.ManyArray = DS.RecordArray.extend({
}
if (actual) {
oldParent = get(record, actual.name);
set(record, actual.name, remove ? null : parentRecord);
return oldParent;
}
},
@ -334,7 +381,8 @@ DS.ManyArray = DS.RecordArray.extend({
(function() {
var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt,
removeObject = Ember.EnumerableUtils.removeObject;
/**
A transaction allows you to collect multiple records into a unit of work
@ -429,6 +477,12 @@ DS.Transaction = Ember.Object.extend({
deleted: Ember.Map.create(),
inflight: Ember.Map.create()
});
this.dirtyRelationships = {
byChild: Ember.Map.create(),
byNewParent: Ember.Map.create(),
byOldParent: Ember.Map.create()
};
},
/**
@ -668,6 +722,113 @@ DS.Transaction = Ember.Object.extend({
records.remove(record);
},
/**
@private
Called by a ManyArray when a new record is added to it. This
method will index a relationship description by the child
record, its old parent, and its new parent.
The store will provide this description to the adapter's
shouldCommit method, so it can determine whether any of
the records is pending another record. The store will also
provide a list of these descriptions to the adapter's commit
method.
@param {DS.Model} record the new child record
@param {DS.Model} oldParent the parent that the child is
moving from, or null
@param {DS.Model} newParent the parent that the child is
moving to, or null
*/
relationshipBecameDirty: function(child, oldParent, newParent) {
var relationships = this.dirtyRelationships, relationship;
var relationshipsForChild = relationships.byChild.get(child),
possibleRelationship,
needsNewEntries = true;
// If the child has any existing dirty relationships in this
// transaction, we need to collapse the old relationship
// into the new one. For example, if we change the parent of
// a child record before saving, there is no need to save the
// record that was its parent temporarily.
if (relationshipsForChild) {
// Loop through all of the relationships we know about that
// contain the same child as the new relationship.
for (var i=0, l=relationshipsForChild.length; i<l; i++) {
relationship = relationshipsForChild[i];
// If the parent of the child record has changed, there is
// no need to update the old parent that had not yet been saved.
//
// This case is two changes in a record's parent:
//
// A -> B
// B -> C
//
// In this case, there is no need to remember the A->B
// change. We can collapse both changes into:
//
// A -> C
//
// Another possible case is:
//
// A -> B
// B -> A
//
// In this case, we don't need to do anything. We can
// simply remove the original A->B change and call it
// a day.
if (relationship.newParent === oldParent) {
oldParent = relationship.oldParent;
this.removeRelationship(relationship);
// This is the case of A->B followed by B->A.
if (relationship.oldParent === newParent) {
needsNewEntries = false;
}
}
}
}
relationship = {
child: child,
oldParent: oldParent,
newParent: newParent
};
// If we didn't go A->B and then B->A, add new dirty relationship
// entries.
if (needsNewEntries) {
this.addRelationshipTo('byChild', child, relationship);
this.addRelationshipTo('byOldParent', oldParent, relationship);
this.addRelationshipTo('byNewParent', newParent, relationship);
}
},
removeRelationship: function(relationship) {
var relationships = this.dirtyRelationships;
removeObject(relationships.byOldParent.get(relationship.oldParent), relationship);
removeObject(relationships.byNewParent.get(relationship.newParent), relationship);
removeObject(relationships.byChild.get(relationship.child), relationship);
},
addRelationshipTo: function(type, record, description) {
var map = this.dirtyRelationships[type];
var relationships = map.get(record);
if (!relationships) {
relationships = [ description ];
map.set(record, relationships);
} else {
relationships.push(description);
}
},
/**
@private
@ -796,6 +957,13 @@ DS.Store = Ember.Object.extend({
this.clientIdToId = {};
this.recordArraysByClientId = {};
// Internally, we maintain a map of all unloaded IDs requested by
// a ManyArray. As the adapter loads hashes into the store, the
// store notifies any interested ManyArrays. When the ManyArray's
// total number of loading records drops to zero, it becomes
// `isLoaded` and fires a `didLoad` event.
this.loadingRecordArrays = {};
set(this, 'defaultTransaction', this.transaction());
return this._super();
@ -1060,78 +1228,121 @@ DS.Store = Ember.Object.extend({
/**
@private
Ask the adapter to fetch IDs that are not already loaded.
Given a type and array of `clientId`s, determines which of those
`clientId`s has not yet been loaded.
This method will convert `id`s to `clientId`s, filter out
`clientId`s that already have a data hash present, and pass
the remaining `id`s to the adapter.
@param {Class} type A model class
@param {Array} ids An array of ids
@param {Object} query
@returns {Array} An Array of all clientIds for the
specified ids.
In preparation for loading, this method also marks any unloaded
`clientId`s as loading.
*/
fetchMany: function(type, ids, query) {
var typeMap = this.typeMapFor(type),
idToClientIdMap = typeMap.idToCid,
neededClientIds: function(type, clientIds) {
var neededClientIds = [],
typeMap = this.typeMapFor(type),
dataCache = typeMap.cidToHash,
data = typeMap.cidToHash,
needed;
clientId;
var clientIds = Ember.A([]);
if (ids) {
needed = [];
ids.forEach(function(id) {
// Get the clientId for the given id
var clientId = idToClientIdMap[id];
// If there is no `clientId` yet
if (clientId === undefined) {
// Create a new `clientId`, marking its data hash
// as loading. Once the adapter returns the data
// hash, it will be updated
clientId = this.pushHash(LOADING, id, type);
needed.push(id);
// If there is a clientId, but its data hash is
// marked as unloaded (this happens when a
// hasMany association creates clientIds for its
// referenced ids before they were loaded)
} else if (clientId && data[clientId] === UNLOADED) {
// change the data hash marker to loading
dataCache[clientId] = LOADING;
needed.push(id);
}
// this method is expected to return a list of
// all of the clientIds for the specified ids,
// unconditionally add it.
clientIds.push(clientId);
}, this);
} else {
needed = null;
for (var i=0, l=clientIds.length; i<l; i++) {
clientId = clientIds[i];
if (dataCache[clientId] === UNLOADED) {
neededClientIds.push(clientId);
dataCache[clientId] = LOADING;
}
}
// If there are any needed ids, ask the adapter to load them
if ((needed && get(needed, 'length') > 0) || query) {
var adapter = get(this, '_adapter');
if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
}
return clientIds;
return neededClientIds;
},
/** @private
*/
findMany: function(type, ids, query) {
var clientIds = this.fetchMany(type, ids, query);
/**
@private
return this.createManyArray(type, clientIds);
This method is the entry point that associations use to update
themselves when their underlying data changes.
First, it determines which of its `clientId`s are still unloaded,
then converts the needed `clientId`s to IDs and invokes `findMany`
on the adapter.
*/
fetchUnloadedClientIds: function(type, clientIds) {
var neededClientIds = this.neededClientIds(type, clientIds);
this.fetchMany(type, neededClientIds);
},
/**
@private
This method takes a type and list of `clientId`s, converts the
`clientId`s into IDs, and then invokes the adapter's `findMany`
method.
It is used both by a brand new association (via the `findMany`
method) or when the data underlying an existing association
changes (via the `fetchUnloadedClientIds` method).
*/
fetchMany: function(type, clientIds) {
var clientIdToId = this.clientIdToId;
var neededIds = Ember.EnumerableUtils.map(clientIds, function(clientId) {
return clientIdToId[clientId];
});
var adapter = get(this, '_adapter');
if (adapter && adapter.findMany) { adapter.findMany(this, type, neededIds); }
else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
},
/**
@private
`findMany` is the entry point that associations use to generate a
new `ManyArray` for the list of IDs specified by the server for
the association.
Its responsibilities are:
* convert the IDs into clientIds
* determine which of the clientIds still need to be loaded
* create a new ManyArray whose content is *all* of the clientIds
* notify the ManyArray of the number of its elements that are
already loaded
* insert the unloaded clientIds into the `loadingRecordArrays`
bookkeeping structure, which will allow the `ManyArray` to know
when all of its loading elements are loaded from the server.
* ask the adapter to load the unloaded elements, by invoking
findMany with the still-unloaded IDs.
*/
findMany: function(type, ids) {
// 1. Convert ids to client ids
// 2. Determine which of the client ids need to be loaded
// 3. Create a new ManyArray whose content is ALL of the clientIds
// 4. Decrement the ManyArray's counter by the number of loaded clientIds
// 5. Put the ManyArray into our bookkeeping data structure, keyed on
// the needed clientIds
// 6. Ask the adapter to load the records for the unloaded clientIds (but
// convert them back to ids)
var clientIds = this.clientIdsForIds(type, ids);
var neededClientIds = this.neededClientIds(type, clientIds),
manyArray = this.createManyArray(type, Ember.A(clientIds)),
loadedCount = clientIds.length - neededClientIds.length,
loadingRecordArrays = this.loadingRecordArrays,
clientId, i, l;
manyArray.send('loadedRecords', loadedCount);
if (neededClientIds.length) {
for (i=0, l=neededClientIds.length; i<l; i++) {
clientId = neededClientIds[i];
if (loadingRecordArrays[clientId]) {
loadingRecordArrays[clientId].push(manyArray);
} else {
this.loadingRecordArrays[clientId] = [ manyArray ];
}
}
this.fetchMany(type, neededClientIds);
}
return manyArray;
},
findQuery: function(type, query) {
@ -1368,6 +1579,18 @@ DS.Store = Ember.Object.extend({
filter = get(array, 'filterFunction');
this.updateRecordArray(array, filter, type, clientId, dataProxy);
}, this);
// loop through all manyArrays containing an unloaded copy of this
// clientId and notify them that the record was loaded.
var manyArrays = this.loadingRecordArrays[clientId], manyArray;
if (manyArrays) {
for (var i=0, l=manyArrays.length; i<l; i++) {
manyArrays[i].send('loadedRecords', 1);
}
this.loadingRecordArrays[clientId] = null;
}
},
updateRecordArray: function(array, filter, type, clientId, dataProxy) {
@ -1453,6 +1676,24 @@ DS.Store = Ember.Object.extend({
return this.pushHash(UNLOADED, id, type);
},
/**
@private
This method works exactly like `clientIdForId`, but does not
require looking up the `typeMap` for every `clientId` and
invoking a method per `clientId`.
*/
clientIdsForIds: function(type, ids) {
var typeMap = this.typeMapFor(type),
idToClientIdMap = typeMap.idToCid;
return Ember.EnumerableUtils.map(ids, function(id) {
var clientId = idToClientIdMap[id];
if (clientId) { return clientId; }
return this.pushHash(UNLOADED, id, type);
}, this);
},
// ................
// . LOADING DATA .
// ................
@ -1869,7 +2110,7 @@ var waitingOn = function(manager, object) {
// super points to the class definition.
var Uncommitted = Ember.Mixin.create({
setProperty: setProperty,
setAssociation: setAssociation,
setAssociation: setAssociation
});
// These mixins are mixed into substates of the concrete
@ -2883,12 +3124,18 @@ DS.Model = Ember.Object.extend(Ember.Evented, {
cachedValue = this.cacheFor(name);
if (cachedValue) {
var key = association.options.key || name,
var key = association.options.key || get(this, 'namingConvention').keyToJSONKey(name),
ids = data.get(key) || [];
var clientIds = Ember.EnumerableUtils.map(ids, function(id) {
return store.clientIdForId(association.type, id);
});
var clientIds;
if(association.options.embedded) {
clientIds = store.loadMany(association.type, ids).clientIds;
} else {
clientIds = Ember.EnumerableUtils.map(ids, function(id) {
return store.clientIdForId(association.type, id);
});
}
set(cachedValue, 'content', Ember.A(clientIds));
cachedValue.fetch();
}
@ -3132,7 +3379,7 @@ var hasAssociation = function(type, options, one) {
if (arguments.length === 2) {
key = options.key || get(this, 'namingConvention').foreignKey(key);
this.send('setAssociation', { key: key, value: value === null ? null : get(value, 'clientId') });
this.send('setAssociation', { key: key, value: Ember.none(value) ? null : get(value, 'clientId') });
//data.setAssociation(key, get(value, 'clientId'));
// put the client id in `key` in the data hash
return value;
@ -3194,7 +3441,7 @@ var hasAssociation = function(type, options) {
key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
ids = findRecord(store, type, data, key);
association = store.findMany(type, ids);
association = store.findMany(type, ids || []);
set(association, 'parentRecord', this);
return association;
@ -3420,6 +3667,20 @@ Ember.onLoad('application', function(app) {
}
}
});
app.registerInjection({
name: "giveStoreToControllers",
injection: function(app, stateManager, property) {
if (property.match(/Controller$/)) {
var controllerName = property.charAt(0).toLowerCase() + property.substr(1);
var store = stateManager.get('store');
var controller = stateManager.get(controllerName);
controller.set('store', store);
}
}
});
});
})();
@ -3466,7 +3727,7 @@ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
DS.RESTAdapter = DS.Adapter.extend({
bulkCommit: false,
createRecord: function(store, type, record) {
var root = this.rootForType(type);
@ -3475,13 +3736,20 @@ DS.RESTAdapter = DS.Adapter.extend({
this.ajax(this.buildURL(root), "POST", {
data: data,
context: this,
success: function(json) {
this.sideload(store, type, json, root);
store.didCreateRecord(record, json[root]);
this.didCreateRecord(store, type, record, json);
}
});
},
didCreateRecord: function(store, type, record, json) {
var root = this.rootForType(type);
this.sideload(store, type, json, root);
store.didCreateRecord(record, json[root]);
},
createRecords: function(store, type, records) {
if (get(this, 'bulkCommit') === false) {
return this._super(store, type, records);
@ -3497,14 +3765,20 @@ DS.RESTAdapter = DS.Adapter.extend({
this.ajax(this.buildURL(root), "POST", {
data: data,
context: this,
success: function(json) {
this.sideload(store, type, json, plural);
store.didCreateRecords(type, records, json[plural]);
this.didCreateRecords(store, type, records, json);
}
});
},
didCreateRecords: function(store, type, records, json) {
var root = this.pluralize(this.rootForType(type));
this.sideload(store, type, json, root);
store.didCreateRecords(type, records, json[root]);
},
updateRecord: function(store, type, record) {
var id = get(record, 'id');
var root = this.rootForType(type);
@ -3514,13 +3788,20 @@ DS.RESTAdapter = DS.Adapter.extend({
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
this.sideload(store, type, json, root);
store.didUpdateRecord(record, json && json[root]);
this.didUpdateRecord(store, type, record, json);
}
});
},
didUpdateRecord: function(store, type, record, json) {
var root = this.rootForType(type);
this.sideload(store, type, json, root);
store.didUpdateRecord(record, json && json[root]);
},
updateRecords: function(store, type, records) {
if (get(this, 'bulkCommit') === false) {
return this._super(store, type, records);
@ -3536,25 +3817,37 @@ DS.RESTAdapter = DS.Adapter.extend({
this.ajax(this.buildURL(root, "bulk"), "PUT", {
data: data,
context: this,
success: function(json) {
this.sideload(store, type, json, plural);
store.didUpdateRecords(records, json[plural]);
this.didUpdateRecords(store, type, records, json);
}
});
},
didUpdateRecords: function(store, type, records, json) {
var root = this.pluralize(this.rootForType(type));
this.sideload(store, type, json, root);
store.didUpdateRecords(records, json[root]);
},
deleteRecord: function(store, type, record) {
var id = get(record, 'id');
var root = this.rootForType(type);
this.ajax(this.buildURL(root, id), "DELETE", {
context: this,
success: function(json) {
if (json) { this.sideload(store, type, json); }
store.didDeleteRecord(record);
this.didDeleteRecord(store, type, record, json);
}
});
},
didDeleteRecord: function(store, type, record, json) {
if (json) { this.sideload(store, type, json); }
store.didDeleteRecord(record);
},
deleteRecords: function(store, type, records) {
if (get(this, 'bulkCommit') === false) {
return this._super(store, type, records);
@ -3570,13 +3863,18 @@ DS.RESTAdapter = DS.Adapter.extend({
this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
data: data,
context: this,
success: function(json) {
if (json) { this.sideload(store, type, json); }
store.didDeleteRecords(records);
this.didDeleteRecords(store, type, records, json);
}
});
},
didDeleteRecords: function(store, type, records, json) {
if (json) { this.sideload(store, type, json); }
store.didDeleteRecords(records);
},
find: function(store, type, id) {
var root = this.rootForType(type);

View File

@ -0,0 +1,521 @@
/*!
* MockJax - jQuery Plugin to Mock Ajax requests
*
* Version: 1.5.0pre
* Released:
* Home: http://github.com/appendto/jquery-mockjax
* Author: Jonathan Sharp (http://jdsharp.com)
* License: MIT,GPL
*
* Copyright (c) 2011 appendTo LLC.
* Dual licensed under the MIT or GPL licenses.
* http://appendto.com/open-source-licenses
*/
(function($) {
var _ajax = $.ajax,
mockHandlers = [],
CALLBACK_REGEX = /=\?(&|$)/,
jsc = (new Date()).getTime();
// Parse the given XML string.
function parseXML(xml) {
if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
DOMParser = function() { };
DOMParser.prototype.parseFromString = function( xmlString ) {
var doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async = 'false';
doc.loadXML( xmlString );
return doc;
};
}
try {
var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
if ( $.isXMLDoc( xmlDoc ) ) {
var err = $('parsererror', xmlDoc);
if ( err.length == 1 ) {
throw('Error: ' + $(xmlDoc).text() );
}
} else {
throw('Unable to parse XML');
}
} catch( e ) {
var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
$(document).trigger('xmlParseError', [ msg ]);
return undefined;
}
return xmlDoc;
}
// Trigger a jQuery event
function trigger(s, type, args) {
(s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
}
// Check if the data field on the mock handler and the request match. This
// can be used to restrict a mock handler to being used only when a certain
// set of data is passed to it.
function isMockDataEqual( mock, live ) {
var identical = false;
// Test for situations where the data is a querystring (not an object)
if (typeof live === 'string') {
// Querystring may be a regex
return $.isFunction( mock.test ) ? mock.test(live) : mock == live;
}
$.each(mock, function(k, v) {
if ( live[k] === undefined ) {
identical = false;
return identical;
} else {
identical = true;
if ( typeof live[k] == 'object' ) {
return isMockDataEqual(mock[k], live[k]);
} else {
if ( $.isFunction( mock[k].test ) ) {
identical = mock[k].test(live[k]);
} else {
identical = ( mock[k] == live[k] );
}
return identical;
}
}
});
return identical;
}
// Check the given handler should mock the given request
function getMockForRequest( handler, requestSettings ) {
// If the mock was registered with a function, let the function decide if we
// want to mock this request
if ( $.isFunction(handler) ) {
return handler( requestSettings );
}
// Inspect the URL of the request and check if the mock handler's url
// matches the url for this ajax request
if ( $.isFunction(handler.url.test) ) {
// The user provided a regex for the url, test it
if ( !handler.url.test( requestSettings.url ) ) {
return null;
}
} else {
// Look for a simple wildcard '*' or a direct URL match
var star = handler.url.indexOf('*');
if (handler.url !== requestSettings.url && star === -1 ||
!new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace('*', '.+')).test(requestSettings.url)) {
return null;
}
}
// Inspect the data submitted in the request (either POST body or GET query string)
if ( handler.data && requestSettings.data ) {
if ( !isMockDataEqual(handler.data, requestSettings.data) ) {
// They're not identical, do not mock this request
return null;
}
}
// Inspect the request type
if ( handler && handler.type &&
handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) {
// The request type doesn't match (GET vs. POST)
return null;
}
return handler;
}
// If logging is enabled, log the mock to the console
function logMock( mockHandler, requestSettings ) {
var c = $.extend({}, $.mockjaxSettings, mockHandler);
if ( c.log && $.isFunction(c.log) ) {
c.log('MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url, $.extend({}, requestSettings));
}
}
// Process the xhr objects send operation
function _xhrSend(mockHandler, requestSettings, origSettings) {
// This is a substitute for < 1.4 which lacks $.proxy
var process = (function(that) {
return function() {
return (function() {
// The request has returned
this.status = mockHandler.status;
this.statusText = mockHandler.statusText;
this.readyState = 4;
// We have an executable function, call it to give
// the mock handler a chance to update it's data
if ( $.isFunction(mockHandler.response) ) {
mockHandler.response(origSettings);
}
// Copy over our mock to our xhr object before passing control back to
// jQuery's onreadystatechange callback
if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) {
this.responseText = JSON.stringify(mockHandler.responseText);
} else if ( requestSettings.dataType == 'xml' ) {
if ( typeof mockHandler.responseXML == 'string' ) {
this.responseXML = parseXML(mockHandler.responseXML);
} else {
this.responseXML = mockHandler.responseXML;
}
} else {
this.responseText = mockHandler.responseText;
}
if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) {
this.status = mockHandler.status;
}
if( typeof mockHandler.statusText === "string") {
this.statusText = mockHandler.statusText;
}
// jQuery < 1.4 doesn't have onreadystate change for xhr
if ( $.isFunction(this.onreadystatechange) ) {
if( mockHandler.isTimeout) {
this.status = -1;
}
this.onreadystatechange( mockHandler.isTimeout ? 'timeout' : undefined );
} else if ( mockHandler.isTimeout ) {
// Fix for 1.3.2 timeout to keep success from firing.
this.status = -1;
}
}).apply(that);
};
})(this);
if ( mockHandler.proxy ) {
// We're proxying this request and loading in an external file instead
_ajax({
global: false,
url: mockHandler.proxy,
type: mockHandler.proxyType,
data: mockHandler.data,
dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType,
complete: function(xhr, txt) {
mockHandler.responseXML = xhr.responseXML;
mockHandler.responseText = xhr.responseText;
mockHandler.status = xhr.status;
mockHandler.statusText = xhr.statusText;
this.responseTimer = setTimeout(process, mockHandler.responseTime || 0);
}
});
} else {
// type == 'POST' || 'GET' || 'DELETE'
if ( requestSettings.async === false ) {
// TODO: Blocking delay
process();
} else {
this.responseTimer = setTimeout(process, mockHandler.responseTime || 50);
}
}
}
// Construct a mocked XHR Object
function xhr(mockHandler, requestSettings, origSettings, origHandler) {
// Extend with our default mockjax settings
mockHandler = $.extend({}, $.mockjaxSettings, mockHandler);
if (typeof mockHandler.headers === 'undefined') {
mockHandler.headers = {};
}
if ( mockHandler.contentType ) {
mockHandler.headers['content-type'] = mockHandler.contentType;
}
return {
status: mockHandler.status,
statusText: mockHandler.statusText,
readyState: 1,
open: function() { },
send: function() {
origHandler.fired = true;
_xhrSend.call(this, mockHandler, requestSettings, origSettings);
},
abort: function() {
clearTimeout(this.responseTimer);
},
setRequestHeader: function(header, value) {
mockHandler.headers[header] = value;
},
getResponseHeader: function(header) {
// 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
if ( mockHandler.headers && mockHandler.headers[header] ) {
// Return arbitrary headers
return mockHandler.headers[header];
} else if ( header.toLowerCase() == 'last-modified' ) {
return mockHandler.lastModified || (new Date()).toString();
} else if ( header.toLowerCase() == 'etag' ) {
return mockHandler.etag || '';
} else if ( header.toLowerCase() == 'content-type' ) {
return mockHandler.contentType || 'text/plain';
}
},
getAllResponseHeaders: function() {
var headers = '';
$.each(mockHandler.headers, function(k, v) {
headers += k + ': ' + v + "\n";
});
return headers;
}
};
}
// Process a JSONP mock request.
function processJsonpMock( requestSettings, mockHandler, origSettings ) {
// Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
// because there isn't an easy hook for the cross domain script tag of jsonp
processJsonpUrl( requestSettings );
requestSettings.dataType = "json";
if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) {
createJsonpCallback(requestSettings, mockHandler);
// We need to make sure
// that a JSONP style response is executed properly
var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
parts = rurl.exec( requestSettings.url ),
remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
requestSettings.dataType = "script";
if(requestSettings.type.toUpperCase() === "GET" && remote ) {
var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings );
// Check if we are supposed to return a Deferred back to the mock call, or just
// signal success
if(newMockReturn) {
return newMockReturn;
} else {
return true;
}
}
}
return null;
}
// Append the required callback parameter to the end of the request URL, for a JSONP request
function processJsonpUrl( requestSettings ) {
if ( requestSettings.type.toUpperCase() === "GET" ) {
if ( !CALLBACK_REGEX.test( requestSettings.url ) ) {
requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") +
(requestSettings.jsonp || "callback") + "=?";
}
} else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) {
requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?";
}
}
// Process a JSONP request by evaluating the mocked response text
function processJsonpRequest( requestSettings, mockHandler, origSettings ) {
// Synthesize the mock request for adding a script tag
var callbackContext = origSettings && origSettings.context || requestSettings,
newMock = null;
// If the response handler on the moock is a function, call it
if ( mockHandler.response && $.isFunction(mockHandler.response) ) {
mockHandler.response(origSettings);
} else {
// Evaluate the responseText javascript in a global context
if( typeof mockHandler.responseText === 'object' ) {
$.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')');
} else {
$.globalEval( '(' + mockHandler.responseText + ')');
}
}
// Successful response
jsonpSuccess( requestSettings, mockHandler );
jsonpComplete( requestSettings, mockHandler );
// If we are running under jQuery 1.5+, return a deferred object
if(jQuery.Deferred){
newMock = new jQuery.Deferred();
if(typeof mockHandler.responseText == "object"){
newMock.resolve( mockHandler.responseText );
}
else{
newMock.resolve( jQuery.parseJSON( mockHandler.responseText ) );
}
}
return newMock;
}
// Create the required JSONP callback function for the request
function createJsonpCallback( requestSettings, mockHandler ) {
jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++);
// Replace the =? sequence both in the query string and the data
if ( requestSettings.data ) {
requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1");
}
requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1");
// Handle JSONP-style loading
window[ jsonp ] = window[ jsonp ] || function( tmp ) {
data = tmp;
jsonpSuccess( requestSettings, mockHandler );
jsonpComplete( requestSettings, mockHandler );
// Garbage collect
window[ jsonp ] = undefined;
try {
delete window[ jsonp ];
} catch(e) {}
if ( head ) {
head.removeChild( script );
}
};
}
// The JSONP request was successful
function jsonpSuccess(requestSettings, mockHandler) {
// If a local callback was specified, fire it and pass it the data
if ( requestSettings.success ) {
requestSettings.success.call( callbackContext, ( mockHandler.response ? mockHandler.response.toString() : mockHandler.responseText || ''), status, {} );
}
// Fire the global callback
if ( requestSettings.global ) {
trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] );
}
}
// The JSONP request was completed
function jsonpComplete(requestSettings, mockHandler) {
// Process result
if ( requestSettings.complete ) {
requestSettings.complete.call( callbackContext, {} , status );
}
// The request was completed
if ( requestSettings.global ) {
trigger( "ajaxComplete", [{}, requestSettings] );
}
// Handle the global AJAX counter
if ( requestSettings.global && ! --jQuery.active ) {
jQuery.event.trigger( "ajaxStop" );
}
}
// The core $.ajax replacement.
function handleAjax( url, origSettings ) {
var mockRequest, requestSettings, mockHandler;
// If url is an object, simulate pre-1.5 signature
if ( typeof url === "object" ) {
origSettings = url;
url = undefined;
} else {
// work around to support 1.5 signature
origSettings.url = url;
}
// Extend the original settings for the request
requestSettings = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
// Iterate over our mock handlers (in registration order) until we find
// one that is willing to intercept the request
for(var k = 0; k < mockHandlers.length; k++) {
if ( !mockHandlers[k] ) {
continue;
}
mockHandler = getMockForRequest( mockHandlers[k], requestSettings );
if(!mockHandler) {
// No valid mock found for this request
continue;
}
// Handle console logging
logMock( mockHandler, requestSettings );
if ( requestSettings.dataType === "jsonp" ) {
if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) {
// This mock will handle the JSONP request
return mockRequest;
}
}
// Removed to fix #54 - keep the mocking data object intact
//mockHandler.data = requestSettings.data;
mockHandler.cache = requestSettings.cache;
mockHandler.timeout = requestSettings.timeout;
mockHandler.global = requestSettings.global;
(function(mockHandler, requestSettings, origSettings, origHandler) {
mockRequest = _ajax.call($, $.extend(true, {}, origSettings, {
// Mock the XHR object
xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ) }
}));
})(mockHandler, requestSettings, origSettings, mockHandlers[k]);
return mockRequest;
}
// We don't have a mock request, trigger a normal request
return _ajax.apply($, [origSettings]);
}
// Public
$.extend({
ajax: handleAjax
});
$.mockjaxSettings = {
//url: null,
//type: 'GET',
log: function(msg) {
window['console'] && window.console.log && window.console.log(msg);
},
status: 200,
statusText: "OK",
responseTime: 500,
isTimeout: false,
contentType: 'text/plain',
response: '',
responseText: '',
responseXML: '',
proxy: '',
proxyType: 'GET',
lastModified: null,
etag: '',
headers: {
etag: 'IJF@H#@923uf8023hFO@I#H#',
'content-type' : 'text/plain'
}
};
$.mockjax = function(settings) {
var i = mockHandlers.length;
mockHandlers[i] = settings;
return i;
};
$.mockjaxClear = function(i) {
if ( arguments.length == 1 ) {
mockHandlers[i] = null;
} else {
mockHandlers = [];
}
};
$.mockjax.handler = function(i) {
if ( arguments.length == 1 ) {
return mockHandlers[i];
}
};
})(jQuery);

View File

@ -1 +0,0 @@
Ember.TEMPLATES['application']=Ember.Handlebars.compile("<div id=\"head\">\n <a href=\"/\">Travis CI</a>\n <a href=\"#\">#</a>\n</div>\n\n<div id=\"left\">\n {{outlet left}}\n</div>\n\n<div id=\"main\">\n {{outlet main}}\n</div>\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['list']=Ember.Handlebars.compile("<table id=\"builds\">\n <thead>\n <tr>\n <th>{{t builds.name}}</th>\n <th>{{t builds.commit}}</th>\n <th>{{t builds.message}}</th>\n <th>{{t builds.duration}}</th>\n <th>{{t builds.finished_at}}</th>\n </tr>\n </thead>\n\n {{#collection tagName=\"tbody\" contentBinding=\"content\" itemViewClass=\"Travis.BuildsItemView\" itemClassBinding=\"color\"}}\n {{#with view.content}}\n <td class=\"number\"><a {{action viewBuild href=true context=\"content\"}}>{{number}}</a></td>\n <td class=\"commit\"><a {{bindAttr href=\"urlGithubCommit\"}}>{{formatCommit commit}}</a> {{commit.sha}}</td>\n <td class=\"message\">{{{formatMessage commit.message short=\"true\"}}}</td>\n <td class=\"duration\" {{bindAttr title=\"started_at\"}}>{{formatDuration duration}}</td>\n <td class=\"finished_at timeago\" {{bindAttr title=\"finished_at\"}}>{{formatTime finished_at}}</td>\n {{/with}}\n {{/collection}}\n</table>\n\n<p>\n <button {{action showMore on=\"click\" target=\"builds\" isVisibleBinding=\"hasMore\"}}>\n {{t builds.show_more}}\n </button>\n</p>\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['show']=Ember.Handlebars.compile("<div {{bindAttr class=\"classes\"}}>\n <dl class=\"summary clearfix\">\n <div class=\"left\">\n <dt>{{t builds.name}}</dt>\n <dd class=\"number\"><a {{bindAttr href=\"urlBuild\"}}>{{number}}</a></dd>\n <dt class=\"finished_at_label\">{{t builds.finished_at}}</dt>\n <dd class=\"finished_at timeago\" {{bindAttr title=\"finished_at\"}}>{{formatTime finished_at}}</dd>\n <dt>{{t builds.duration}}</dt>\n <dd class=\"duration\" {{bindAttr title=\"started_at\"}}>{{formatDuration duration}}</dd>\n </div>\n\n <div class=\"right\">\n <dt>{{t builds.commit}}</dt>\n <dd class=\"commit-hash\"><a {{bindAttr href=\"urlGithubCommit\"}}>{{formatCommit commit}}</a></dd>\n {{#if commit.compare_url}}\n <dt>{{t builds.compare}}</dt>\n <dd class=\"compare_view\"><a {{bindAttr href=\"commit.compare_url\"}}>{{pathFrom commit.compare_url}}</a></dd>\n {{/if}}\n {{#if commit.author_name}}\n <dt>{{t builds.author}}</dt>\n <dd class=\"author\"><a {{bindAttr href=\"view.urlAuthor\"}}>{{commit.author_name}}</a></dd>\n {{/if}}\n {{#if commit.committer_name}}\n <dt>{{t builds.committer}}</dt>\n <dd class=\"committer\"><a {{bindAttr href=\"urlCommitter\"}}>{{commit.committer_name}}</a></dd>\n {{/if}}\n </div>\n\n <dt>{{t builds.message}}</dt>\n <dd class=\"commit-message\">{{{formatMessage commit.message}}}</dd>\n\n {{#if isMatrix}}\n {{else}}\n <dt>{{t builds.config}}</dt>\n <dd class=\"config\">{{formatConfig config}}</dd>\n {{/if}}\n </dl>\n\n {{#if isLoaded}}\n {{#if isMatrix}}\n {{view Travis.JobsView}}\n {{else}}\n {{view Travis.LogView}}\n {{/if}}\n {{/if}}\n</div>\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['list']=Ember.Handlebars.compile("<table id=\"jobs\">\n <caption>{{t jobs.build_matrix}}</caption>\n <thead>\n <tr>\n {{#each configKeys}}\n <th>{{this}}</th>\n {{/each}}\n </tr>\n </thead>\n <tbody>\n {{#each requiredJobs}}\n <tr {{bindAttr class=\"color\"}}>\n <td class=\"number\"><a {{action viewJob href=true context=this}}>#{{number}}</a></td>\n <td class=\"duration\" {{bindAttr title=\"started_at\"}}>{{formatDuration duration}}</td>\n <td class=\"finished_at timeago\" {{bindAttr title=\"finished_at\"}}>{{formatTime finished_at}}</td>\n {{#each configValues}}\n <td>{{this}}</td>\n {{/each}}\n </tr>\n {{/each}}\n </tbody>\n</table>\n\n{{#if isFailureMatrix}}\n <table id=\"allow_failure_builds\">\n <caption>\n {{t jobs.allowed_failures}}{{whats_this allow_failure_help}}\n </caption>\n <thead>\n <tr>\n {{#each configKeys}}\n <th>{{this}}</th>\n {{/each}}\n </tr>\n </thead>\n <tbody>\n {{#each allowedFailureJobs}}\n <tr {{bindAttr class=\"color\"}}>\n <td class=\"number\"><a {{action viewJob href=true}}>#{{number}}</a></td>\n <td class=\"duration\" {{bindAttr title=\"started_at\"}}>{{formatDuration duration}}</td>\n <td class=\"finished_at timeago\" {{bindAttr title=\"finished_at\"}}>{{formatTime finished_at}}</td>\n {{#each configValues}}\n <td>{{this}}</td>\n {{/each}}\n </tr>\n {{/each}}\n </tbody>\n </table>\n\n <div id=\"allow_failure_help\" class=\"context_help\">\n <div class=\"context_help_caption\">{{t \"jobs.allowed_failures\"}}</div>\n <div class=\"context_help_body\">\n <p>\n Allowed Failures are items in your build matrix that are allowed to\n fail without causing the entire build to be shown as failed. This lets you add\n in experimental and preparatory builds to test against versions or\n configurations that you are not ready to officially support.\n </p>\n <p>\n You can define allowed failures in the build matrix as follows:\n </p>\n <pre>\n matrix:\n allow_failures:\n - rvm: ruby-head\n </pre>\n </div>\n </div>\n{{/if}}\n\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['log']=Ember.Handlebars.compile("{{! ugh ... }}\n{{#with jobs.firstObject}}\n <pre class=\"log\">{{{formatLog log.body}}}</pre>\n\n {{#if sponsor.name}}\n <p class=\"sponsor\">\n {{t builds.messages.sponsored_by}}\n <a {{bindAttr href=\"sponsor.url\"}}>{{sponsor.name}}</a>\n </p>\n {{/if}}\n{{/with}}\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['show']=Ember.Handlebars.compile("<div {{bindAttr class=\"color\"}}>\n <dl class=\"summary clearfix\">\n <div class=\"left\">\n <dt>Job</dt>\n <dd class=\"number\"><a {{bindAttr href=\"urlJob\"}}>{{number}}</a></dd>\n <dt class=\"finished_at_label\">{{t jobs.finished_at}}</dt>\n <dd class=\"finished_at timeago\" {{bindAttr title=\"finished_at\"}}>{{formatTime finished_at}}</dd>\n <dt>{{t jobs.duration}}</dt>\n <dd class=\"duration\" {{bindAttr title=\"started_at\"}}>{{formatDuration duration}}</dd>\n </div>\n\n <div class=\"right\">\n <dt>{{t jobs.commit}}</dt>\n <dd class=\"commit-hash\"><a {{bindAttr href=\"urlGithubCommit\"}}>{{formatCommit commit}}</a></dd>\n {{#if commit.compare_url}}\n <dt>{{t jobs.compare}}</dt>\n <dd class=\"compare_view\"><a {{bindAttr href=\"commit.compare_url\"}}>{{pathFrom commit.compare_url}}</a></dd>\n {{/if}}\n {{#if commit.author_name}}\n <dt>{{t jobs.author}}</dt>\n <dd class=\"author\"><a {{bindAttr href=\"urlAuthor\"}}>{{commit.author_name}}</a></dd>\n {{/if}}\n {{#if commit.committer_name}}\n <dt>{{t jobs.committer}}</dt>\n <dd class=\"committer\"><a {{bindAttr href=\"urlCommitter\"}}>{{commit.committer_name}}</a></dd>\n {{/if}}\n </div>\n\n <dt>{{t jobs.message}}</dt>\n <dd class=\"commit-message\">{{formatMessage commit.message}}</dd>\n <dt>{{t jobs.config}}</dt>\n <dd class=\"config\">{{formatConfig config}}</dd>\n </dl>\n\n {{view Travis.LogView}}\n</div>\n\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['loading']=Ember.Handlebars.compile("loading stuff ...\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['list']=Ember.Handlebars.compile("{{#collection tagName=\"ul\" id=\"repositories\" contentBinding=\"content\" itemViewClass=\"Travis.RepositoriesItemView\" itemClassBinding=\"classes\"}}\n {{#with view.content}}\n <div class=\"wrapper\">\n <a {{action viewCurrent href=true context=\"content\"}} class=\"slug\">{{slug}}</a>\n <a {{action viewBuild href=true context=\"lastBuild\"}} class=\"build\">#{{last_build_number}}</a>\n <p class=\"summary\">\n <span class=\"duration_label\">{{t repositories.duration}}:</span>\n <abbr class=\"duration\" {{bindAttr title=\"last_build_started_at\"}}>{{formatDuration last_build_duration}}</abbr>,\n <span class=\"finished_at_label\">{{t repositories.finished_at}}:</span>\n <abbr class=\"finished_at timeago\" {{bindAttr title=\"last_build_finished_at\"}}>{{formatTime last_build_finished_at}}</abbr>\n </p>\n {{#if description}}\n <p class=\"description\">{{description}}</p>\n {{/if}}\n <span class=\"indicator\"></span>\n </div>\n {{/with}}\n{{/collection}}\n\n{{^collection contentBinding=\"repositories\" id=\"list\" class=\"loading\"}}\n <p></p>\n{{/collection}}\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['show']=Ember.Handlebars.compile("<h3>\n <a {{bindAttr href=\"urlGithub\"}}>{{slug}}</a>\n</h3>\n\n<p class=\"description\">{{description}}</p>\n\n<ul class=\"github-stats\">\n <li class=\"language\">{{last_build_language}}</li>\n <li><a class=\"watchers\" title=\"Watches\" {{bindAttr href=\"urlGithubWatchers\"}}>{{stats.watchers}}</a></li>\n <li><a class=\"forks\" title=\"Forks\" {{bindAttr href=\"urlGithubNetwork\"}}>{{stats.forks}}</a></li>\n</ul>\n\n{{outlet tabs}}\n\n<div class=\"tab\">\n {{outlet tab}}\n</div>\n");

View File

@ -1 +0,0 @@
Ember.TEMPLATES['tabs']=Ember.Handlebars.compile("<ul class=\"tabs\">\n <li><a {{action viewCurrent href=true context=\"repository\"}} class=\"current\">Current</a></li>\n <li><a {{action viewHistory href=true context=\"repository\"}} class=\"history\">History</a></li>\n {{#if build}}\n <li><a {{action viewBuild href=true context=\"build\"}} class=\"build\">Build #{{build.number}}</a></li>\n {{/if}}\n {{#if job}}\n <li><a {{action viewJob href=true context=\"job\"}} class=\"job\">Job #{{job.number}}</a></li>\n {{/if}}\n</ul>\n");

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff