diff --git a/.localeapp/log.yml b/.localeapp/log.yml
index 1c42b9c4..ea6770fd 100644
--- a/.localeapp/log.yml
+++ b/.localeapp/log.yml
@@ -1,3 +1,3 @@
---
-:polled_at: 1361792606
-:updated_at: 1361792606
+:polled_at: 1363093348
+:updated_at: 1363093348
diff --git a/.travis.yml b/.travis.yml
index a220d0f9..c99ba3cc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,7 @@ rvm:
before_script:
- "gem install travis-artifacts"
- "bundle exec rakep"
+ - "phantomjs --version"
env:
global:
@@ -16,14 +17,9 @@ env:
script: "script/ci"
after_script:
- - "ENV=production bundle exec rakep"
+ - "bundle exec rakep"
- "test $TEST_SUITE = \"ember\" && travis-artifacts upload --target-path assets/$TRAVIS_BRANCH --path public/scripts:scripts --path public/styles:styles"
-matrix:
- allow_failures:
- - env: "TEST_SUITE=ember"
- rvm: "1.9.3"
-
notifications:
irc: "irc.freenode.org#travis"
campfire:
diff --git a/Assetfile b/Assetfile
index 84d80092..da341d89 100644
--- a/Assetfile
+++ b/Assetfile
@@ -22,7 +22,7 @@ end
output 'public/scripts'
input assets.scripts do
match '**/*.hbs' do
- travis_handlebars :precompile => assets.production?
+ travis_handlebars :precompile => false # assets.production?
concat 'templates.js'
end
@@ -64,7 +64,7 @@ input assets.scripts do
if assets.production?
match 'min/app.js' do
strip_debug
- uglify squeeze: true
+ # uglify squeeze: true
concat 'app.js'
end
end
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..37eceb4e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,15 @@
+# Contributing to Travis-CI
+Issues for any Travis-CI should be submitted to https://github.com/travis-ci/travis-ci/issues
+
+## Security Issues
+***Any security issues should be submitted directly to [security@travis-ci.org](mailto:security@travis-ci.org)***
+
+## Reporting Issues
+- Explain what you expected to happen vs the actual results
+- Include a screenshot if it helps illustrate the issue. https://github.com/blog/1347-issue-attachments
+- What steps are required to reproduce the issue
+- An example build that shows the issue
+
+## Submitting a PR to Travis-Web
+
+See testing and setup notes in the base [README](https://github.com/travis-ci/travis-web)
diff --git a/Gemfile b/Gemfile
index 0ecca317..e8365a53 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,5 @@
-ruby '1.9.3' rescue nil
-
-source :rubygems
+source 'https://rubygems.org'
+ruby '1.9.3'
gem 'puma'
gem 'rack-ssl', '~> 1.3'
@@ -21,7 +20,7 @@ group :assets do
end
group :development, :test do
- gem 'rake', '~> 0.9.2'
+ gem 'rake'
gem 'localeapp'
gem 'handlebars'
gem 'localeapp-handlebars_i18n'
diff --git a/Gemfile.lock b/Gemfile.lock
index c5de1e09..91af9a57 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,60 +1,61 @@
GIT
remote: git://github.com/livingsocial/rake-pipeline.git
- revision: 50b8d77b703c96539a433ee53a680c43542aaa75
+ revision: 65b1e744defa208e313703d89f3453447cc103b2
specs:
rake-pipeline (0.8.0)
json
- rake (~> 0.9.0)
+ rake (~> 10.0.0)
thor
GIT
remote: git://github.com/wycats/rake-pipeline-web-filters.git
- revision: 1a6dc173776b188836aa2ce2ac35b61c7f7daafe
+ revision: fd8d838491bd6b8de0bab72d90115b9a4f2da8a1
specs:
rake-pipeline-web-filters (0.6.0)
rack
rake-pipeline (~> 0.6)
GEM
- remote: http://rubygems.org/
+ remote: https://rubygems.org/
specs:
POpen4 (0.1.4)
Platform (>= 0.4.0)
open4
Platform (0.4.0)
- backports (2.6.5)
- chunky_png (1.2.6)
- coderay (1.0.8)
+ backports (3.0.3)
+ chunky_png (1.2.7)
+ coderay (1.0.9)
coffee-script (2.2.0)
coffee-script-source
execjs
- coffee-script-source (1.4.0)
+ coffee-script-source (1.5.0)
commonjs (0.2.6)
compass (0.12.2)
chunky_png (~> 1.2)
fssm (>= 0.2.7)
sass (~> 3.1)
- diff-lcs (1.1.3)
+ diff-lcs (1.2.1)
eventmachine (1.0.0)
execjs (1.4.0)
multi_json (~> 1.0)
- foreman (0.60.2)
+ foreman (0.61.0)
thor (>= 0.13.6)
- fssm (0.2.9)
- gli (2.5.2)
- guard (1.5.4)
- listen (>= 0.4.2)
+ fssm (0.2.10)
+ gli (2.5.4)
+ guard (1.6.2)
+ listen (>= 0.6.0)
lumberjack (>= 1.0.2)
pry (>= 0.9.10)
+ terminal-table (>= 1.4.3)
thor (>= 0.14.6)
- handlebars (0.3.1)
+ handlebars (0.4.0)
commonjs (~> 0.2.3)
- therubyracer (~> 0.10.0)
- i18n (0.6.1)
- json (1.7.5)
- libv8 (3.3.10.4)
- listen (0.6.0)
- localeapp (0.6.8)
+ therubyracer (~> 0.11.1)
+ i18n (0.6.3)
+ json (1.7.7)
+ libv8 (3.11.8.13)
+ listen (0.7.3)
+ localeapp (0.6.9)
gli
i18n
json
@@ -64,46 +65,47 @@ GEM
localeapp
lumberjack (1.0.2)
method_source (0.8.1)
- mime-types (1.19)
- multi_json (1.5.0)
+ mime-types (1.21)
+ multi_json (1.6.1)
open4 (1.3.0)
- pry (0.9.10)
+ pry (0.9.12)
coderay (~> 1.0.5)
method_source (~> 0.8)
- slop (~> 3.3.1)
+ slop (~> 3.4)
puma (1.6.3)
rack (~> 1.2)
- rack (1.4.1)
+ rack (1.5.2)
rack-cache (1.2)
rack (>= 0.4)
rack-mobile-detect (0.4.0)
rack
rack-protection (1.3.2)
rack
- rack-ssl (1.3.2)
+ rack-ssl (1.3.3)
rack
rack-test (0.6.2)
rack (>= 1.0)
- rake (0.9.6)
+ rake (10.0.3)
rake-pipeline-i18n-filters (0.0.5)
rake-pipeline (~> 0.6)
- rb-fsevent (0.9.2)
- rerun (0.7.1)
+ rb-fsevent (0.9.3)
+ ref (1.0.2)
+ rerun (0.8.0)
listen
rest-client (1.6.7)
mime-types (>= 1.16)
- rspec (2.12.0)
- rspec-core (~> 2.12.0)
- rspec-expectations (~> 2.12.0)
- rspec-mocks (~> 2.12.0)
- rspec-core (2.12.2)
- rspec-expectations (2.12.0)
- diff-lcs (~> 1.1.3)
- rspec-mocks (2.12.0)
- sass (3.2.3)
- sinatra (1.3.3)
- rack (~> 1.3, >= 1.3.6)
- rack-protection (~> 1.2)
+ rspec (2.13.0)
+ rspec-core (~> 2.13.0)
+ rspec-expectations (~> 2.13.0)
+ rspec-mocks (~> 2.13.0)
+ rspec-core (2.13.0)
+ rspec-expectations (2.13.0)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.13.0)
+ sass (3.2.6)
+ sinatra (1.3.5)
+ rack (~> 1.4)
+ rack-protection (~> 1.3)
tilt (~> 1.3, >= 1.3.3)
sinatra-contrib (1.3.2)
backports (>= 2.0)
@@ -112,10 +114,12 @@ GEM
rack-test
sinatra (~> 1.3.0)
tilt (~> 1.3)
- slop (3.3.3)
- therubyracer (0.10.2)
- libv8 (~> 3.3.10)
- thor (0.16.0)
+ slop (3.4.3)
+ terminal-table (1.4.5)
+ therubyracer (0.11.4)
+ libv8 (~> 3.11.8.12)
+ ref
+ thor (0.17.0)
tilt (1.3.3)
uglifier (1.3.0)
execjs (>= 0.3.0)
@@ -140,7 +144,7 @@ DEPENDENCIES
rack-mobile-detect
rack-protection (~> 1.3)
rack-ssl (~> 1.3)
- rake (~> 0.9.2)
+ rake
rake-pipeline!
rake-pipeline-i18n-filters
rake-pipeline-web-filters!
diff --git a/assets/images/ui/log.fold.closed.3.png b/assets/images/ui/log.fold.closed.3.png
new file mode 100644
index 00000000..304b09e7
Binary files /dev/null and b/assets/images/ui/log.fold.closed.3.png differ
diff --git a/assets/scripts/app/app.coffee b/assets/scripts/app/app.coffee
index a8a50350..42641989 100644
--- a/assets/scripts/app/app.coffee
+++ b/assets/scripts/app/app.coffee
@@ -1,64 +1,4 @@
-require 'auth'
-require 'controllers'
-require 'helpers'
-require 'models'
-require 'pusher'
-require 'routes'
-require 'slider'
-require 'store'
-require 'tailing'
-require 'templates'
-require 'views'
-
-require 'config/locales'
-require 'data/sponsors'
-
-require 'travis/instrumentation'
# $.mockjaxSettings.log = false
# Ember.LOG_BINDINGS = true
# Ember.ENV.RAISE_ON_DEPRECATION = true
# Pusher.log = -> console.log(arguments)
-
-Travis.reopen
- App: Em.Application.extend
- autoinit: false
- currentUserBinding: 'auth.user'
- authStateBinding: 'auth.state'
-
- init: ->
- @_super.apply this, arguments
-
- @store = Travis.Store.create()
- @store.loadMany(Travis.Sponsor, Travis.SPONSORS)
-
- @slider = new Travis.Slider()
- @pusher = new Travis.Pusher(Travis.config.pusher_key)
- @tailing = new Travis.Tailing()
-
- @set('auth', Travis.Auth.create(app: this, endpoint: Travis.config.api_endpoint))
-
- storeAfterSignInPath: (path) ->
- @get('auth').storeAfterSignInPath(path)
-
- autoSignIn: (path) ->
- @get('auth').autoSignIn()
-
- signIn: ->
- @get('auth').signIn()
-
- signOut: ->
- @get('auth').signOut()
- @get('router').send('afterSignOut')
-
- receive: ->
- @store.receive.apply(@store, arguments)
-
- toggleSidebar: ->
- $('body').toggleClass('maximized')
- # TODO gotta force redraws here :/
- element = $('')
- $('#top .profile').append(element)
- Em.run.later (-> element.remove()), 10
- element = $('')
- $('#repo').append(element)
- Em.run.later (-> element.remove()), 10
diff --git a/assets/scripts/app/auth.coffee b/assets/scripts/app/auth.coffee
index ae0540d7..28b0d9e6 100644
--- a/assets/scripts/app/auth.coffee
+++ b/assets/scripts/app/auth.coffee
@@ -13,6 +13,8 @@
Travis.setLocale Travis.default_locale
@set('state', 'signed-out')
@set('user', undefined)
+ Travis.__container__.lookup('controller:currentUser').set('content', null)
+ Travis.__container__.lookup('router:main').send('afterSignOut')
signIn: ->
@set('state', 'signing-in')
@@ -31,7 +33,7 @@
if user && token && @validateUser(user)
{ user: user, token: token }
else
- console.log('dropping user, no token') unless token?
+ # console.log('dropping user, no token') if token?
storage.removeItem('travis.user')
storage.removeItem('travis.token')
null
@@ -43,25 +45,30 @@
if user[field]
true
else
- console.log("discarding user data, lacks #{field}")
+ # console.log("discarding user data, lacks #{field}")
false
setData: (data) ->
@storeData(data, Travis.sessionStorage)
@storeData(data, Travis.storage) unless @userDataFrom(Travis.storage)
- @set('user', @loadUser(data.user))
+ user = @loadUser(data.user)
+ # TODO: we should not use __container__ directly, how to do it better?
+ # A good answer seems to do auth in context of controller.
+ Travis.__container__.lookup('controller:currentUser').set('content', user)
+
@set('state', 'signed-in')
Travis.setLocale(data.user.locale || Travis.default_locale)
Travis.trigger('user:signed_in', data.user)
- @get('app.router').send('afterSignIn', @readAfterSignInPath())
+ Travis.__container__.lookup('router:main').send('afterSignIn', @readAfterSignInPath())
storeData: (data, storage) ->
storage.setItem('travis.token', data.token)
storage.setItem('travis.user', JSON.stringify(data.user))
loadUser: (user) ->
- @app.store.load(Travis.User, user)
- user = @app.store.find(Travis.User, user.id)
+ store = @app.store
+ store.load(Travis.User, user.id, user)
+ user = store.find(Travis.User, user.id)
user.get('permissions')
user
diff --git a/assets/scripts/app/controllers.coffee b/assets/scripts/app/controllers.coffee
index f0e1d3ac..475e997d 100644
--- a/assets/scripts/app/controllers.coffee
+++ b/assets/scripts/app/controllers.coffee
@@ -1,25 +1,40 @@
require 'helpers'
require 'travis/ticker'
-Travis.reopen
- Controller: Em.Controller.extend()
+Travis.Controller = Em.Controller.extend()
+Travis.TopController = Em.Controller.extend
+ needs: ['currentUser']
+ userBinding: 'controllers.currentUser'
- TopController: Em.Controller.extend
- userBinding: 'Travis.app.currentUser'
+Travis.ApplicationController = Em.Controller.extend
+ templateName: 'layouts/home'
- ApplicationController: Em.Controller.extend()
- MainController: Em.Controller.extend()
- StatsLayoutController: Em.Controller.extend()
- ProfileLayoutController: Em.Controller.extend()
- AuthLayoutController: Em.Controller.extend()
+ connectLayout: (name) ->
+ name = "layouts/#{name}"
+ if @get('templateName') != name
+ @set('templateName', name)
+
+Travis.MainController = Em.Controller.extend()
+Travis.StatsLayoutController = Em.Controller.extend()
+Travis.ProfileLayoutController = Em.Controller.extend()
+Travis.AuthLayoutController = Em.Controller.extend()
+
+Travis.AccountProfileController = Em.Controller.extend
+ needs: ['currentUser']
+ userBinding: 'controllers.currentUser'
require 'controllers/accounts'
+require 'controllers/build'
require 'controllers/builds'
require 'controllers/flash'
require 'controllers/home'
+require 'controllers/job'
require 'controllers/profile'
require 'controllers/repos'
require 'controllers/repo'
require 'controllers/running_jobs'
require 'controllers/sidebar'
require 'controllers/stats'
+require 'controllers/current_user'
+require 'controllers/account_index'
+
diff --git a/assets/scripts/app/controllers/account_index.coffee b/assets/scripts/app/controllers/account_index.coffee
new file mode 100644
index 00000000..b5dda57a
--- /dev/null
+++ b/assets/scripts/app/controllers/account_index.coffee
@@ -0,0 +1,10 @@
+Travis.AccountIndexController = Em.Controller.extend
+ needs: ['profile', 'currentUser']
+ hooksBinding: 'controllers.profile.hooks'
+ userBinding: 'controllers.currentUser'
+
+ sync: ->
+ @get('user').sync()
+
+ toggle: (hook) ->
+ hook.toggle()
diff --git a/assets/scripts/app/controllers/accounts.coffee b/assets/scripts/app/controllers/accounts.coffee
index 7bc7dff0..213d2c85 100644
--- a/assets/scripts/app/controllers/accounts.coffee
+++ b/assets/scripts/app/controllers/accounts.coffee
@@ -1,8 +1,5 @@
Travis.AccountsController = Ember.ArrayController.extend
tab: 'accounts'
- init: ->
- @_super()
-
findByLogin: (login) ->
@find (account) -> account.get('login') == login
diff --git a/assets/scripts/app/controllers/build.coffee b/assets/scripts/app/controllers/build.coffee
new file mode 100644
index 00000000..56740440
--- /dev/null
+++ b/assets/scripts/app/controllers/build.coffee
@@ -0,0 +1,24 @@
+Travis.BuildController = Ember.Controller.extend
+ needs: ['repo']
+ repoBinding: 'controllers.repo.repo'
+ buildBinding: 'controllers.repo.build'
+ commitBinding: 'build.commit'
+ lineNumberBinding: 'controllers.repo.lineNumber'
+
+ currentItemBinding: 'build'
+
+ loading: (->
+ @get('build.isLoading')
+ ).property('build.isLoading')
+
+ urlGithubCommit: (->
+ Travis.Urls.githubCommit(@get('repo.slug'), @get('commit.sha'))
+ ).property('repo.slug', 'commit.sha')
+
+ urlAuthor: (->
+ Travis.Urls.email(@get('commit.authorEmail'))
+ ).property('commit.authorEmail')
+
+ urlCommitter: (->
+ Travis.Urls.email(@get('commit.committerEmail'))
+ ).property('commit.committerEmail')
diff --git a/assets/scripts/app/controllers/builds.coffee b/assets/scripts/app/controllers/builds.coffee
index 3f945639..b18a4898 100644
--- a/assets/scripts/app/controllers/builds.coffee
+++ b/assets/scripts/app/controllers/builds.coffee
@@ -1,5 +1,19 @@
Travis.BuildsController = Em.ArrayController.extend
- # sortAscending: false
+ sortAscending: false
+ sortProperties: ['number']
- repo: 'parent.repo'
- contentBinding: 'parent.builds'
+ needs: ['repo']
+
+ repoBinding: 'controllers.repo.repo'
+ contentBinding: 'controllers.repo.builds'
+ tabBinding: 'controllers.repo.tab'
+ isLoadedBinding: 'content.isLoaded'
+
+ showMore: ->
+ id = @get('repo.id')
+ number = @get('lastObject.number')
+ @get('content').load Travis.Build.olderThanNumber(id, number, @get('tab'))
+
+ displayShowMoreButton: (->
+ @get('tab') != 'branches'
+ ).property('tab')
diff --git a/assets/scripts/app/controllers/current_user.coffee b/assets/scripts/app/controllers/current_user.coffee
new file mode 100644
index 00000000..548e06a5
--- /dev/null
+++ b/assets/scripts/app/controllers/current_user.coffee
@@ -0,0 +1,3 @@
+Travis.CurrentUserController = Em.ObjectController.extend
+ sync: ->
+ @get('content').sync()
diff --git a/assets/scripts/app/controllers/flash.coffee b/assets/scripts/app/controllers/flash.coffee
index 20e5b8a5..67ad6ae5 100644
--- a/assets/scripts/app/controllers/flash.coffee
+++ b/assets/scripts/app/controllers/flash.coffee
@@ -1,9 +1,12 @@
Travis.FlashController = Ember.ArrayController.extend
- broadcastBinding: 'Travis.app.currentUser.broadcasts'
+ needs: ['currentUser']
+ currentUserBinding: 'controllers.currentUser'
+
+ broadcastBinding: 'currentUser.broadcasts'
init: ->
- @set('flashes', Ember.A())
@_super.apply this, arguments
+ @set('flashes', Ember.A())
content: (->
@get('unseenBroadcasts').concat(@get('flashes'))
@@ -14,8 +17,8 @@ Travis.FlashController = Ember.ArrayController.extend
).property('broadcasts.isLoaded', 'broadcasts.length')
broadcasts: (->
- if Travis.app.get('currentUser') then Travis.Broadcast.find() else Ember.A()
- ).property('Travis.app.currentUser')
+ if @get('currentUser') then Travis.Broadcast.find() else Ember.A()
+ ).property('currentUser')
loadFlashes: (msgs) ->
for msg in msgs
diff --git a/assets/scripts/app/controllers/job.coffee b/assets/scripts/app/controllers/job.coffee
new file mode 100644
index 00000000..3f94343c
--- /dev/null
+++ b/assets/scripts/app/controllers/job.coffee
@@ -0,0 +1,21 @@
+Travis.JobController = Em.Controller.extend
+ needs: ['repo']
+
+ jobBinding: 'controllers.repo.job'
+ repoBinding: 'controllers.repo.repo'
+ commitBinding: 'job.commit'
+ lineNumberBinding: 'controllers.repo.lineNumber'
+
+ currentItemBinding: 'job'
+
+ urlGithubCommit: (->
+ Travis.Urls.githubCommit(@get('repo.slug'), @get('commit.sha'))
+ ).property('repo.slug', 'commit.sha')
+
+ urlAuthor: (->
+ Travis.Urls.email(@get('commit.authorEmail'))
+ ).property('commit.authorEmail')
+
+ urlCommitter: (->
+ Travis.Urls.email(@get('commit.committerEmail'))
+ ).property('commit.committerEmail')
diff --git a/assets/scripts/app/controllers/profile.coffee b/assets/scripts/app/controllers/profile.coffee
index 744214a0..fbac998a 100644
--- a/assets/scripts/app/controllers/profile.coffee
+++ b/assets/scripts/app/controllers/profile.coffee
@@ -1,21 +1,31 @@
Travis.ProfileController = Travis.Controller.extend
name: 'profile'
- userBinding: 'Travis.app.currentUser'
- accountsBinding: 'Travis.app.router.accountsController'
+
+ needs: ['currentUser', 'accounts']
+ userBinding: 'controllers.currentUser'
+ accountsBinding: 'controllers.accounts'
init: ->
+ @_super.apply this, arguments
+
self = this
Travis.on("user:synced", (->
self.reloadHooks()
))
account: (->
- login = @get('params.login') || Travis.app.get('currentUser.login')
+ login = @get('params.login') || @get('user.login')
account = @get('accounts').filter((account) -> account if account.get('login') == login)[0]
account.select() if account
account
).property('accounts.length', 'params.login')
+ sync: ->
+ @get('user').sync()
+
+ toggle: (hook) ->
+ hook.toggle()
+
activate: (action, params) ->
@setParams(params || @get('params'))
this["view#{$.camelize(action)}"]()
@@ -25,7 +35,7 @@ Travis.ProfileController = Travis.Controller.extend
@reloadHooks()
reloadHooks: ->
- @set('hooks', Travis.Hook.find(owner_name: @get('params.login') || Travis.app.get('currentUser.login')))
+ @set('hooks', Travis.Hook.find(owner_name: @get('params.login') || @get('user.login')))
viewUser: ->
@connectTab('user')
@@ -33,7 +43,6 @@ Travis.ProfileController = Travis.Controller.extend
connectTab: (tab) ->
viewClass = Travis["#{$.camelize(tab)}View"]
@set('tab', tab)
- @connectOutlet(outletName: 'pane', controller: this, viewClass: viewClass)
setParams: (params) ->
@set('params', {})
diff --git a/assets/scripts/app/controllers/repo.coffee b/assets/scripts/app/controllers/repo.coffee
index 671d0f70..bf5e0de7 100644
--- a/assets/scripts/app/controllers/repo.coffee
+++ b/assets/scripts/app/controllers/repo.coffee
@@ -1,20 +1,15 @@
Travis.RepoController = Travis.Controller.extend
bindings: []
+ needs: ['repos', 'currentUser']
+ currentUserBinding: 'controllers.currentUser'
+
+ isError: (-> @get('repo.isError') ).property('repo.isError')
+ slug: (-> @get('repo.slug') ).property('repo.slug')
+ isLoading: (-> @get('repo.isLoading') ).property('repo.isLoading')
init: ->
@_super.apply this, arguments
Ember.run.later(@updateTimes.bind(this), Travis.INTERVALS.updateTimes)
- @set 'builds', Em.ArrayProxy.create(Em.SortableMixin,
- isLoadedBinding: 'content.isLoaded'
- sortProperties: ['number']
- sortAscending: false
- content: []
- isLoadingBinding: 'content.isLoading'
- load: (records) ->
- content = @get('content')
- if content && content.load
- content.load(records)
- )
updateTimes: ->
if builds = @get('builds')
@@ -33,7 +28,7 @@ Travis.RepoController = Travis.Controller.extend
this["view#{$.camelize(action)}"]()
viewIndex: ->
- @_bind('repo', 'controllers.reposController.firstObject')
+ @_bind('repo', 'controllers.repos.firstObject')
@_bind('build', 'repo.lastBuild')
@connectTab('current')
@@ -43,15 +38,15 @@ Travis.RepoController = Travis.Controller.extend
viewBuilds: ->
@connectTab('builds')
- @_bind('builds.content', 'repo.builds')
+ @_bind('builds', 'repo.builds')
viewPullRequests: ->
@connectTab('pull_requests')
- @_bind('builds.content', 'repo.pullRequests')
+ @_bind('builds', 'repo.pullRequests')
viewBranches: ->
@connectTab('branches')
- @_bind('builds.content', 'repo.branches')
+ @_bind('builds', 'repo.branches')
viewEvents: ->
@connectTab('events')
@@ -64,12 +59,9 @@ Travis.RepoController = Travis.Controller.extend
@_bind('build', 'job.build')
@connectTab('job')
- repoObserver: (->
- repo = @get('repo')
- repo.select() if repo
- ).observes('repo.id')
-
connectTab: (tab) ->
+ # TODO: such implementation seems weird now, because we render
+ # in the renderTemplate function in routes
name = if tab == 'current' then 'build' else tab
viewClass = if name in ['builds', 'branches', 'pull_requests']
Travis.BuildsView
@@ -77,7 +69,6 @@ Travis.RepoController = Travis.Controller.extend
Travis["#{$.camelize(name)}View"]
@set('tab', tab)
- @connectOutlet(outletName: 'pane', controller: this, viewClass: viewClass)
_bind: (to, from) ->
@bindings.push Ember.oneWay(this, to, from)
@@ -85,3 +76,7 @@ Travis.RepoController = Travis.Controller.extend
_unbind: ->
binding.disconnect(this) for binding in @bindings
@bindings.clear()
+
+ urlGithub: (->
+ Travis.Urls.githubRepo(@get('repo.slug'))
+ ).property('repo.slug')
diff --git a/assets/scripts/app/controllers/repos.coffee b/assets/scripts/app/controllers/repos.coffee
index 06445ebd..cfc0c0da 100644
--- a/assets/scripts/app/controllers/repos.coffee
+++ b/assets/scripts/app/controllers/repos.coffee
@@ -3,10 +3,30 @@ require 'travis/limited_array'
Travis.ReposController = Ember.ArrayController.extend
defaultTab: 'recent'
isLoadedBinding: 'content.isLoaded'
+ needs: ['currentUser', 'repo']
+ currentUserBinding: 'controllers.currentUser'
+ selectedRepo: (->
+ # we need to observe also repo.content here, because we use
+ # ObjectProxy in repo controller
+ # TODO: get rid of ObjectProxy there
+ @get('controllers.repo.repo.content') || @get('controllers.repo.repo')
+ ).property('controllers.repo.repo', 'controllers.repo.repo.content')
init: ->
+ @_super.apply this, arguments
Ember.run.later(@updateTimes.bind(this), Travis.INTERVALS.updateTimes)
+ recentRepos: (->
+ Travis.Repo.find()
+ Travis.LimitedArray.create
+ content: Em.ArrayProxy.extend(Em.SortableMixin).create(
+ sortProperties: ['sortOrder']
+ content: Travis.Repo.withLastBuild()
+ isLoadedBinding: 'content.isLoaded'
+ )
+ limit: 30
+ ).property()
+
updateTimes: ->
if content = @get('content')
content.forEach (r) -> r.updateTimes()
@@ -19,18 +39,10 @@ Travis.ReposController = Ember.ArrayController.extend
this["view#{$.camelize(tab)}"](params)
viewRecent: ->
- content = Travis.LimitedArray.create
- content: Em.ArrayProxy.extend(Em.SortableMixin).create(
- sortProperties: ['sortOrder']
- content: Travis.Repo.find()
- isLoadedBinding: 'content.isLoaded'
- )
- limit: 30
- @set('content', content)
- # @set('content', Travis.Repo.find())
+ @set('content', @get('recentRepos'))
viewOwned: ->
- @set('content', Travis.Repo.accessibleBy(Travis.app.get('currentUser.login')))
+ @set('content', Travis.Repo.accessibleBy(@get('currentUser.login')))
viewSearch: (params) ->
@set('content', Travis.Repo.search(params.search))
diff --git a/assets/scripts/app/controllers/running_jobs.coffee b/assets/scripts/app/controllers/running_jobs.coffee
index 11534d9d..2a010233 100644
--- a/assets/scripts/app/controllers/running_jobs.coffee
+++ b/assets/scripts/app/controllers/running_jobs.coffee
@@ -1,8 +1,9 @@
Travis.RunningJobsController = Em.ArrayProxy.extend
Group: Em.Object.extend
- repo: (-> @get('jobs.firstObject.repo') ).property('jobs.firstObject.repo')
+ slug: (-> @get('jobs.firstObject.repoSlug') ).property('jobs.firstObject.repoSlug')
init: ->
+ @_super.apply this, arguments
@set 'jobs', []
@set 'sortedJobs', Em.ArrayProxy.extend(Em.SortableMixin,
@@ -39,7 +40,9 @@ Travis.RunningJobsController = Em.ArrayProxy.extend
init: ->
@_super.apply this, arguments
- @addedJobs @get('content') if @get('content')
+ jobs = Travis.Job.running()
+ @set 'content', jobs
+ @addedJobs jobs
contentArrayWillChange: (array, index, removedCount, addedCount) ->
@_super.apply this, arguments
diff --git a/assets/scripts/app/controllers/sidebar.coffee b/assets/scripts/app/controllers/sidebar.coffee
index c8a9f644..42bfd638 100644
--- a/assets/scripts/app/controllers/sidebar.coffee
+++ b/assets/scripts/app/controllers/sidebar.coffee
@@ -1,15 +1,32 @@
Travis.reopen
SidebarController: Em.ArrayController.extend
init: ->
+ @_super.apply this, arguments
@tickables = []
Travis.Ticker.create(target: this, interval: Travis.INTERVALS.sponsors)
tick: ->
tickable.tick() for tickable in @tickables
- QueuesController: Em.ArrayController.extend()
+ QueuesController: Em.ArrayController.extend
+ init: ->
+ @_super.apply this, arguments
+
+ queues = for queue in Travis.QUEUES
+ Travis.LimitedArray.create
+ content: Travis.Job.queued(queue.name), limit: 20
+ id: "queue_#{queue.name}"
+ name: queue.display
+ @set 'content', queues
+
+ showAll: (queue) ->
+ queue.showAll()
WorkersController: Em.ArrayController.extend
+ init: ->
+ @_super.apply this, arguments
+ @set 'content', Travis.Worker.find()
+
groups: (->
if content = @get 'arrangedContent'
groups = {}
@@ -53,3 +70,22 @@ Travis.reopen
end: ->
@start() + @get('perPage')
+Travis.DecksController = Travis.SponsorsController.extend
+ needs: ['sidebar']
+ perPage: 1
+
+ init: ->
+ @_super.apply this, arguments
+
+ @get('controllers.sidebar').tickables.push(this)
+ @set 'content', Travis.Sponsor.decks()
+
+Travis.LinksController = Travis.SponsorsController.extend
+ needs: ['sidebar']
+ perPage: 6
+
+ init: ->
+ @_super.apply this, arguments
+
+ @get('controllers.sidebar').tickables.push(this)
+ @set 'content', Travis.Sponsor.links()
diff --git a/assets/scripts/app/controllers/stats.coffee b/assets/scripts/app/controllers/stats.coffee
index b0b31704..3d6da596 100644
--- a/assets/scripts/app/controllers/stats.coffee
+++ b/assets/scripts/app/controllers/stats.coffee
@@ -2,7 +2,7 @@ Travis.StatsController = Travis.Controller.extend
name: 'stats'
init: ->
- @_super('top')
+ @_super.apply this, arguments
#@connectOutlet(outletName: 'main', controller: this, viewClass: Travis.StatsView)
activate: (action, params) ->
diff --git a/assets/scripts/app/helpers/helpers.coffee b/assets/scripts/app/helpers/helpers.coffee
index c3f51333..a0f28ec5 100644
--- a/assets/scripts/app/helpers/helpers.coffee
+++ b/assets/scripts/app/helpers/helpers.coffee
@@ -1,4 +1,3 @@
-require 'travis/log'
require 'config/emoij'
@Travis.Helpers =
@@ -40,16 +39,6 @@ require 'config/emoij'
message = message.split(/\n/)[0] if options.short
@_emojize(@_escape(message)).replace /\n/g, '
'
- formatLog: (log, repo, item) ->
- event = if item.constructor == Travis.Build
- 'showBuild'
- else
- 'showJob'
-
- url = Travis.app.get('router').urlForEvent(event, repo, item)
-
- Travis.Log.filter(log, url)
-
pathFrom: (url) ->
(url || '').split('/').pop()
@@ -84,7 +73,7 @@ require 'config/emoij'
string
_nowUtc: ->
- @_toUtc new Date()
+ @_toUtc Travis.currentDate()
_toUtc: (date) ->
Date.UTC date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()
diff --git a/assets/scripts/app/models.coffee b/assets/scripts/app/models.coffee
index 28064194..8eab6753 100644
--- a/assets/scripts/app/models.coffee
+++ b/assets/scripts/app/models.coffee
@@ -1,6 +1,5 @@
require 'models/extensions'
require 'models/account'
-require 'models/artifact'
require 'models/broadcast'
require 'models/branch'
require 'models/build'
@@ -8,6 +7,7 @@ require 'models/commit'
require 'models/event'
require 'models/hook'
require 'models/job'
+require 'models/log'
require 'models/repo'
require 'models/sponsor'
require 'models/user'
diff --git a/assets/scripts/app/models/artifact.coffee b/assets/scripts/app/models/artifact.coffee
deleted file mode 100644
index 26b87921..00000000
--- a/assets/scripts/app/models/artifact.coffee
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'travis/model'
-
-@Travis.Artifact = Em.Object.extend
- version: 1 # used to refresh log on requeue
- body: null
- isLoaded: false
-
- init: ->
- @_super.apply this, arguments
-
- @addObserver 'job.id', @fetchBody
- @fetchBody()
-
- @set 'queue', Ember.A([])
- @set 'parts', Ember.ArrayProxy.create(content: [])
-
- @addObserver 'body', @fetchWorker
- @fetchWorker()
-
- id: (->
- @get('job.id')
- ).property('job.id')
-
- clear: ->
- @set('body', '')
- @incrementProperty('version')
-
- fetchBody: ->
- if jobId = @get('job.id')
- @removeObserver 'job.id', @fetchBody
-
- self = this
- Travis.ajax.ajax "/jobs/#{jobId}/log.txt?cors_hax=true", 'GET',
- dataType: 'text'
- contentType: 'text/plain'
- success: (data, textStatus, xhr) ->
- if xhr.status == 204
- logUrl = xhr.getResponseHeader('X-Log-Location')
-
- # For some reason not all browsers can fetch this header
- unless logUrl
- logUrl = self.s3Url("/jobs/#{jobId}/log.txt")
-
- $.ajax
- url: logUrl
- type: 'GET'
- success: (data) ->
- self.fetchedBody(data)
- else
- self.fetchedBody(data)
-
- s3Url: (path) ->
- endpoint = Travis.config.api_endpoint
- staging = if endpoint.match(/-staging/) then '-staging' else ''
- host = Travis.config.api_endpoint.replace(/^https?:\/\//, '').split('.').slice(-2).join('.')
- "https://s3.amazonaws.com/archive#{staging}.#{host}#{path}"
-
-
- fetchedBody: (body) ->
- @set 'body', body
- @set 'isLoaded', true
-
- append: (body) ->
- if @get('isInitialized')
- @get('parts').pushObject body
- @set('body', @get('body') + body)
- else
- @get('queue').pushObject(body)
-
- recordDidLoad: (->
- if @get('isLoaded')
- if (body = @get 'body') && @get('parts.length') == 0
- @get('parts').pushObject body
-
- @set 'isInitialized', true
-
- queue = @get('queue')
- if queue.get('length') > 0
- @append queue.toArray().join('')
- ).observes('isLoaded')
-
- fetchWorker: ->
- if !@get('workerName') && (body = @get('body'))
- line = body.split("\n")[0]
- if line && (match = line.match /Using worker: (.*)/)
- if worker = match[1]
- worker = worker.trim().split(':')[0]
- @set('workerName', worker)
- @removeObserver 'body', @fetchWorker
diff --git a/assets/scripts/app/models/build.coffee b/assets/scripts/app/models/build.coffee
index 089b111e..03b20781 100644
--- a/assets/scripts/app/models/build.coffee
+++ b/assets/scripts/app/models/build.coffee
@@ -2,32 +2,33 @@ require 'travis/model'
@Travis.Build = Travis.Model.extend Travis.DurationCalculations,
eventType: DS.attr('string')
- repoId: DS.attr('number', key: 'repository_id')
+ repoId: DS.attr('number')
commitId: DS.attr('number')
state: DS.attr('string')
number: DS.attr('number')
branch: DS.attr('string')
message: DS.attr('string')
- _duration: DS.attr('number', key: 'duration')
- startedAt: DS.attr('string', key: 'started_at')
- finishedAt: DS.attr('string', key: 'finished_at')
+ _duration: DS.attr('number')
+ _config: DS.attr('object')
+ startedAt: DS.attr('string')
+ finishedAt: DS.attr('string')
- repo: DS.belongsTo('Travis.Repo', key: 'repository_id')
+ repo: DS.belongsTo('Travis.Repo')
commit: DS.belongsTo('Travis.Commit')
- jobs: DS.hasMany('Travis.Job', key: 'job_ids')
+ jobs: DS.hasMany('Travis.Job')
config: (->
- Travis.Helpers.compact(@get('data.config'))
- ).property('data.config')
+ Travis.Helpers.compact(@get('_config'))
+ ).property('_config')
isPullRequest: (->
@get('eventType') == 'pull_request'
).property('eventType')
isMatrix: (->
- @get('data.job_ids.length') > 1
- ).property('data.job_ids.length')
+ @get('jobs.length') > 1
+ ).property('jobs.length')
isFinished: (->
@get('state') in ['passed', 'failed', 'errored', 'canceled']
@@ -73,6 +74,11 @@ require 'travis/model'
branches: (options) ->
@find repository_id: options.repoId, branches: true
- olderThanNumber: (id, build_number) ->
+ olderThanNumber: (id, build_number, type) ->
+ console.log type
# TODO fix this api and use some kind of pagination scheme
- @find(url: "/builds", repository_id: id, after_number: build_number)
+ options = { repository_id: id, after_number: build_number }
+ if type?
+ options.event_type = type.replace(/s$/, '') # poor man's singularize
+
+ @find(options)
diff --git a/assets/scripts/app/models/commit.coffee b/assets/scripts/app/models/commit.coffee
index 467d5006..9e3be52f 100644
--- a/assets/scripts/app/models/commit.coffee
+++ b/assets/scripts/app/models/commit.coffee
@@ -13,4 +13,4 @@ require 'travis/model'
pullRequestTitle: DS.attr('string')
pullRequestNumber: DS.attr('number')
- build: DS.belongsTo('Travis.Build', key: 'buildId')
+ build: DS.belongsTo('Travis.Build')
diff --git a/assets/scripts/app/models/job.coffee b/assets/scripts/app/models/job.coffee
index 40313156..98334b06 100644
--- a/assets/scripts/app/models/job.coffee
+++ b/assets/scripts/app/models/job.coffee
@@ -1,7 +1,7 @@
require 'travis/model'
@Travis.Job = Travis.Model.extend Travis.DurationCalculations,
- repoId: DS.attr('number', key: 'repository_id')
+ repoId: DS.attr('number')
buildId: DS.attr('number')
commitId: DS.attr('number')
logId: DS.attr('number')
@@ -11,14 +11,23 @@ require 'travis/model'
number: DS.attr('string')
startedAt: DS.attr('string')
finishedAt: DS.attr('string')
- allowFailure: DS.attr('boolean', key: 'allow_failure')
+ allowFailure: DS.attr('boolean')
repositorySlug: DS.attr('string')
- repo: DS.belongsTo('Travis.Repo', key: 'repository_id')
- build: DS.belongsTo('Travis.Build', key: 'build_id')
- commit: DS.belongsTo('Travis.Commit', key: 'commit_id')
+ repo: DS.belongsTo('Travis.Repo')
+ build: DS.belongsTo('Travis.Build')
+ commit: DS.belongsTo('Travis.Commit')
+
+ # this is a fake relationship just to get rid
+ # of ember data's bug: https://github.com/emberjs/data/issues/758
+ # TODO: remove when this issue is fixed
+ fakeBuild: DS.belongsTo('Travis.Build')
+
+ _config: DS.attr('object')
+
log: ( ->
- Travis.Artifact.create(job: this)
+ @set('isLogAccessed', true)
+ Travis.Log.create(job: this)
).property()
repoSlug: (->
@@ -30,15 +39,17 @@ require 'travis/model'
).property('repoSlug', 'repoId')
config: (->
- Travis.Helpers.compact(@get('data.config'))
- ).property('data.config')
+ Travis.Helpers.compact(@get('_config'))
+ ).property('_config')
isFinished: (->
@get('state') in ['passed', 'failed', 'errored', 'canceled']
).property('state')
clearLog: ->
- @get('log').clear() if @get('log.isLoaded')
+ # This is needed if we don't want to fetch log just to clear it
+ if @get('isLogAccessed')
+ @get('log').clear()
sponsor: (->
worker = @get('log.workerName')
@@ -70,17 +81,22 @@ require 'travis/model'
requeue: ->
Travis.ajax.post '/requests', job_id: @get('id')
- appendLog: (text) ->
- if log = @get('log')
- log.append(text)
+ appendLog: (part) ->
+ @get('log').append part
subscribe: ->
- if id = @get('id')
- Travis.app.pusher.subscribe "job-#{id}"
+ return if @get('subscribed')
+ @set('subscribed', true)
+ Travis.pusher.subscribe "job-#{@get('id')}"
+
+ unsubscribe: ->
+ return unless @get('subscribed')
+ @set('subscribed', false)
+ Travis.pusher.unsubscribe "job-#{@get('id')}"
onStateChange: (->
- if @get('state') == 'finished' && Travis.app
- Travis.app.pusher.unsubscribe "job-#{@get('id')}"
+ if @get('state') == 'finished' && Travis.pusher
+ Travis.pusher.unsubscribe "job-#{@get('id')}"
).observes('state')
isAttributeLoaded: (key) ->
@@ -98,16 +114,16 @@ require 'travis/model'
@Travis.Job.reopenClass
queued: (queue) ->
@find()
- Travis.app.store.filter this, (job) ->
+ Travis.store.filter this, (job) ->
queued = ['created', 'queued'].indexOf(job.get('state')) != -1
# TODO: why queue is sometimes just common instead of build.common?
queued && (!queue || job.get('queue') == "builds.#{queue}" || job.get('queue') == queue)
running: ->
@find(state: 'started')
- Travis.app.store.filter this, (job) ->
+ Travis.store.filter this, (job) ->
job.get('state') == 'started'
findMany: (ids) ->
- Travis.app.store.findMany this, ids
+ Travis.store.findMany this, ids
diff --git a/assets/scripts/app/models/log.coffee b/assets/scripts/app/models/log.coffee
new file mode 100644
index 00000000..de96b6f6
--- /dev/null
+++ b/assets/scripts/app/models/log.coffee
@@ -0,0 +1,75 @@
+require 'travis/model'
+require 'travis/chunk_buffer'
+
+@Travis.Log = Em.Object.extend
+ version: 0 # used to refresh log on requeue
+ isLoaded: false
+ length: 0
+
+ init: ->
+ @setParts()
+ @fetch()
+
+ setParts: ->
+ #@set 'parts', Ember.ArrayProxy.create(content: [])
+ @set 'parts', Travis.ChunkBuffer.create(content: [])
+
+ fetch: ->
+ console.log 'log model: fetching log' if Log.DEBUG
+ handlers =
+ json: (json) => @loadParts(json['log']['parts'])
+ text: (text) => @loadText(text)
+ Travis.Log.Request.create(id: id, handlers: handlers).run() if id = @get('job.id')
+
+ clear: ->
+ @setParts()
+ @incrementProperty('version')
+
+ append: (part) ->
+ @get('parts').pushObject(part)
+
+ loadParts: (parts) ->
+ console.log 'log model: load parts' if Log.DEBUG
+ @append(part) for part in parts
+ @set('isLoaded', true)
+
+ loadText: (text) ->
+ console.log 'log model: load text' if Log.DEBUG
+ number = -1
+ @append(number: 1, content: text)
+ @set('isLoaded', true)
+
+Travis.Log.Request = Em.Object.extend
+ HEADERS:
+ accept: 'application/json; chunked=true; version=2, text/plain; version=2'
+
+ run: ->
+ Travis.ajax.ajax "/jobs/#{@id}/log?cors_hax=true", 'GET',
+ dataType: 'text'
+ headers: @HEADERS
+ success: (body, status, xhr) => @handle(body, status, xhr)
+
+ handle: (body, status, xhr) ->
+ if xhr.status == 204
+ $.ajax(url: @redirectTo(xhr), type: 'GET', success: @handlers.text)
+ else if @isJson(xhr, body)
+ @handlers.json(JSON.parse(body))
+ else
+ @handlers.text(body)
+
+ redirectTo: (xhr) ->
+ # Firefox can't see the Location header on the xhr response due to the wrong
+ # status code 204. Should be some redirect code but that doesn't work with CORS.
+ xhr.getResponseHeader('Location') || @s3Url()
+
+ s3Url: ->
+ endpoint = Travis.config.api_endpoint
+ staging = if endpoint.match(/-staging/) then '-staging' else ''
+ host = endpoint.replace(/^https?:\/\//, '').split('.').slice(-2).join('.')
+ "https://s3.amazonaws.com/archive#{staging}.#{host}#{path}/jobs/#{@id}/log.txt"
+
+ isJson: (xhr, body) ->
+ # Firefox can't see the Content-Type header on the xhr response due to the wrong
+ # status code 204. Should be some redirect code but that doesn't work with CORS.
+ type = xhr.getResponseHeader('Content-Type') || ''
+ type.indexOf('json') > -1 || body.slice(0, 8) == '{"log":{'
diff --git a/assets/scripts/app/models/repo.coffee b/assets/scripts/app/models/repo.coffee
index ce37b7ea..34118023 100644
--- a/assets/scripts/app/models/repo.coffee
+++ b/assets/scripts/app/models/repo.coffee
@@ -9,6 +9,7 @@ require 'travis/model'
lastBuildState: DS.attr('string')
lastBuildStartedAt: DS.attr('string')
lastBuildFinishedAt: DS.attr('string')
+ _lastBuildDuration: DS.attr('number')
lastBuild: DS.belongsTo('Travis.Build')
@@ -21,17 +22,14 @@ require 'travis/model'
).property('lastBuildId', 'lastBuildNumber')
allBuilds: (->
- allBuilds = DS.RecordArray.create
- type: Travis.Build
- content: Ember.A([])
- store: @get('store')
- @get('store').registerRecordArray(allBuilds, Travis.Build);
- allBuilds
+ Travis.Build.find()
).property()
builds: (->
id = @get('id')
builds = Travis.Build.byRepoId id, event_type: 'push'
+
+ # TODO: move to controller
array = Travis.ExpandableRecordArray.create
type: Travis.Build
content: Ember.A([])
@@ -40,7 +38,7 @@ require 'travis/model'
array.load(builds)
id = @get('id')
- array.observe(@get('allBuilds'), (build) -> build.get('repo.id') == id && !build.get('isPullRequest') )
+ array.observe(@get('allBuilds'), (build) -> build.get('isLoaded') && build.get('eventType') && build.get('repo.id') == id && !build.get('isPullRequest') )
array
).property()
@@ -56,7 +54,7 @@ require 'travis/model'
array.load(builds)
id = @get('id')
- array.observe(@get('allBuilds'), (build) -> @get('repositoryId') == id && build.get('isPullRequest') )
+ array.observe(@get('allBuilds'), (build) -> build.get('isLoaded') && build.get('eventType') && build.get('repo.id') == id && build.get('isPullRequest') )
array
).property()
@@ -78,10 +76,10 @@ require 'travis/model'
).property('slug')
lastBuildDuration: (->
- duration = @get('data.last_build_duration')
+ duration = @get('_lastBuildDuration')
duration = Travis.Helpers.durationFrom(@get('lastBuildStartedAt'), @get('lastBuildFinishedAt')) unless duration
duration
- ).property('data.last_build_duration', 'lastBuildStartedAt', 'lastBuildFinishedAt')
+ ).property('_lastBuildDuration', 'lastBuildStartedAt', 'lastBuildFinishedAt')
sortOrder: (->
# cuz sortAscending seems buggy when set to false
@@ -118,6 +116,9 @@ require 'travis/model'
search: (query) ->
@find(search: query, orderBy: 'name')
+ withLastBuild: ->
+ @filter( (repo) -> repo.get('lastBuildId') )
+
bySlug: (slug) ->
repo = $.select(@find().toArray(), (repo) -> repo.get('slug') == slug)
if repo.length > 0 then repo else @find(slug: slug)
diff --git a/assets/scripts/app/models/sponsor.coffee b/assets/scripts/app/models/sponsor.coffee
index 82b9564b..201173fb 100644
--- a/assets/scripts/app/models/sponsor.coffee
+++ b/assets/scripts/app/models/sponsor.coffee
@@ -4,10 +4,11 @@ require 'travis/model'
type: DS.attr('string')
url: DS.attr('string')
link: DS.attr('string')
+ _image: DS.attr('string')
image: (->
- "/images/sponsors/#{@get('data.image')}"
- ).property('data.image')
+ "/images/sponsors/#{@get('_image')}"
+ ).property('_image')
Travis.Sponsor.reopenClass
decks: ->
diff --git a/assets/scripts/app/models/user.coffee b/assets/scripts/app/models/user.coffee
index 4128b8d2..a6f2779f 100644
--- a/assets/scripts/app/models/user.coffee
+++ b/assets/scripts/app/models/user.coffee
@@ -2,7 +2,7 @@ require 'travis/ajax'
require 'travis/model'
@Travis.User = Travis.Model.extend
- _name: DS.attr('string', key: 'name')
+ _name: DS.attr('string')
email: DS.attr('string')
login: DS.attr('string')
token: DS.attr('string')
@@ -22,9 +22,11 @@ require 'travis/model'
).property('login', '_name')
init: ->
- @poll() if @get('isSyncing')
@_super()
+ # TODO: the next line fails, check this
+ #@poll() if @get('isSyncing')
+
Ember.run.next this, ->
transaction = @get('store').transaction()
transaction.add this
diff --git a/assets/scripts/app/models/worker.coffee b/assets/scripts/app/models/worker.coffee
index 3458d852..737bf113 100644
--- a/assets/scripts/app/models/worker.coffee
+++ b/assets/scripts/app/models/worker.coffee
@@ -4,10 +4,7 @@ require 'travis/model'
state: DS.attr('string')
name: DS.attr('string')
host: DS.attr('string')
-
- payload: (->
- @get('data.payload')
- ).property('data.payload')
+ payload: DS.attr('object')
number: (->
@get('name').match(/\d+$/)[0]
diff --git a/assets/scripts/app/pusher.coffee b/assets/scripts/app/pusher.coffee
index daa054ca..019c70ff 100644
--- a/assets/scripts/app/pusher.coffee
+++ b/assets/scripts/app/pusher.coffee
@@ -24,10 +24,12 @@ $.extend Travis.Pusher.prototype,
@pusher.subscribeAll()
subscribe: (channel) ->
+ console.log("subscribing to #{channel}")
channel = @prefix(channel)
@pusher.subscribe(channel).bind_all((event, data) => @receive(event, data)) unless @pusher?.channel(channel)
unsubscribe: (channel) ->
+ console.log("unsubscribing from #{channel}")
channel = @prefix(channel)
@pusher.unsubscribe(channel) if @pusher?.channel(channel)
@@ -45,7 +47,7 @@ $.extend Travis.Pusher.prototype,
Travis.Job.find(data.job.id).clearLog()
Ember.run.next ->
- Travis.app.store.receive(event, data)
+ Travis.store.receive(event, data)
normalize: (event, data) ->
switch event
diff --git a/assets/scripts/app/routes.coffee b/assets/scripts/app/routes.coffee
index c0cd5130..c7cd5df0 100644
--- a/assets/scripts/app/routes.coffee
+++ b/assets/scripts/app/routes.coffee
@@ -1,374 +1,287 @@
require 'travis/location'
+require 'travis/line_number_parser'
+Ember.Router.reopen
+ location: (if testMode? then Ember.NoneLocation.create() else Travis.Location.create())
+
+ handleURL: (url) ->
+ url = url.replace(/#.*?$/, '')
+ try
+ @_super(url)
+ catch error
+ @_super('/not-found')
+
+# TODO: don't reopen Ember.Route to add events, there should be
+# a better way (like "parent" resource for everything inside map)
Ember.Route.reopen
- enter: (router) ->
- @_super(router)
- _gaq.push(['_trackPageview', @absoluteRoute(router)]) if @get('isLeafRoute') && _gaq?
+ events:
+ afterSignIn: (path) ->
+ @routeTo(path)
-defaultRoute = Ember.Route.extend
- route: '/'
- index: 1000
+ afterSignOut: ->
+ @routeTo('/')
-lineNumberRoute = Ember.Route.extend
- route: '#L:number'
- index: 1
- connectOutlets: (router) ->
- router.saveLineNumberHash()
-
- dynamicSegmentPattern: "([0-9]+)"
-
-Travis.Router = Ember.Router.extend
- location: 'travis'
- # enableLogging: true
- enableLogging: false
- initialState: 'loading'
-
- showRoot: Ember.Route.transitionTo('root.home.show')
- showStats: Ember.Route.transitionTo('root.stats')
-
- showRepo: Ember.Route.transitionTo('root.home.repo.show')
- showBuilds: Ember.Route.transitionTo('root.home.repo.builds.index')
- showBuild: Ember.Route.transitionTo('root.home.repo.builds.show')
- showPullRequests: Ember.Route.transitionTo('root.home.repo.pullRequests')
- showBranches: Ember.Route.transitionTo('root.home.repo.branches')
- showEvents: Ember.Route.transitionTo('root.home.repo.events')
- showJob: Ember.Route.transitionTo('root.home.repo.job')
-
- showProfile: Ember.Route.transitionTo('root.profile')
- showAccount: Ember.Route.transitionTo('root.profile.account')
- showUserProfile: Ember.Route.transitionTo('root.profile.account.profile')
-
- saveLineNumberHash: (path) ->
- Ember.run.next this, ->
- path = path || @get('location').getURL()
- if match = path.match(/#L\d+$/)
- @set 'repoController.lineNumberHash', match[0]
-
- reload: ->
- console.log 'Triggering reload'
- url = @get('location').getURL()
- @transitionTo('loading')
- # Without ember next @route sometimes hit the place where HistoryLocation
- # does not have any state set up yet, so it's best to defer it a little bit.
- Ember.run.next this, ->
- @route(url)
+ routeTo: (path) ->
+ return unless path
+ @router.handleURL(path)
+ @router.location.setURL(path)
signedIn: ->
- !!Travis.app.get('auth.user')
+ @controllerFor('currentUser').get('content')
- needsAuth: (path) ->
- path.indexOf('/profile') == 0
-
- afterSignOut: ->
- @authorize('/')
-
- loading: Ember.Route.extend
- routePath: (router, path) ->
- router.saveLineNumberHash(path)
- router.authorize(path)
- Travis.app.autoSignIn() unless router.signedIn()
+ redirect: ->
+ if @get('needsAuth')
+ @authorize(@router.location.getURL())
+ else
+ @_super.apply this, arguments
+ Travis.autoSignIn() unless @signedIn()
authorize: (path) ->
- if !@signedIn() && @needsAuth(path)
- Travis.app.storeAfterSignInPath(path)
- @transitionTo('root.auth')
+ if !@signedIn()
+ Travis.storeAfterSignInPath(path)
+ @transitionTo('auth')
+
+Travis.Router.reopen
+ transitionTo: ->
+ this.container.lookup('controller:repo').set('lineNumber', null)
+
+ @_super.apply this, arguments
+
+
+Travis.Router.map ->
+ @resource 'index', path: '/', ->
+ @route 'current', path: '/'
+ @resource 'repo', path: '/:owner/:name', ->
+ @route 'index', path: '/'
+ @resource 'build', path: '/builds/:build_id'
+ @resource 'job', path: '/jobs/:job_id'
+ @resource 'builds', path: '/builds'
+ @resource 'pullRequests', path: '/pull_requests'
+ @resource 'branches', path: '/branches'
+
+ @route 'stats', path: '/stats'
+ @route 'auth', path: '/auth'
+ @route 'notFound', path: '/not-found'
+
+ @resource 'profile', path: '/profile', ->
+ @route 'index', path: '/'
+ @resource 'account', path: '/:login', ->
+ @route 'index', path: '/'
+ @route 'profile', path: '/profile'
+
+Travis.ApplicationRoute = Ember.Route.extend Travis.LineNumberParser,
+ setupController: ->
+ @_super.apply this, arguments
+
+ this.controllerFor('repo').set('lineNumber', @fetchLineNumber())
+
+Travis.IndexCurrentRoute = Ember.Route.extend
+ renderTemplate: ->
+ @render 'repo'
+ @render 'build', outlet: 'pane', into: 'repo'
+
+ setupController: ->
+ @container.lookup('controller:repo').activate('index')
+
+Travis.AbstractBuildsRoute = Ember.Route.extend
+ renderTemplate: ->
+ @render 'builds', outlet: 'pane', into: 'repo'
+
+ setupController: ->
+ @container.lookup('controller:repo').activate(@get('contentType'))
+
+Travis.BuildsRoute = Travis.AbstractBuildsRoute.extend(contentType: 'builds')
+Travis.PullRequestsRoute = Travis.AbstractBuildsRoute.extend(contentType: 'pull_requests')
+Travis.BranchesRoute = Travis.AbstractBuildsRoute.extend(contentType: 'branches')
+
+Travis.BuildRoute = Ember.Route.extend
+ renderTemplate: ->
+ @render 'build', outlet: 'pane', into: 'repo'
+
+ serialize: (model, params) ->
+ id = if model.get then model.get('id') else model
+
+ { build_id: id }
+
+ setupController: (controller, model) ->
+ model = Travis.Build.find(model) if model && !model.get
+
+ repo = @container.lookup('controller:repo')
+ repo.set('build', model)
+ repo.activate('build')
+
+Travis.JobRoute = Ember.Route.extend
+ renderTemplate: ->
+ @render 'job', outlet: 'pane', into: 'repo'
+
+ serialize: (model, params) ->
+ id = if model.get then model.get('id') else model
+
+ { job_id: id }
+
+ setupController: (controller, model) ->
+ model = Travis.Job.find(model) if model && !model.get
+
+ repo = @container.lookup('controller:repo')
+ repo.set('job', model)
+ repo.activate('job')
+
+Travis.RepoIndexRoute = Ember.Route.extend
+ setupController: (controller, model) ->
+ @container.lookup('controller:repo').activate('current')
+
+ renderTemplate: ->
+ @render 'build', outlet: 'pane', into: 'repo'
+
+Travis.RepoRoute = Ember.Route.extend
+ renderTemplate: ->
+ @render 'repo'
+
+ setupController: (controller, model) ->
+ # TODO: if repo is just a data hash with id and slug load it
+ # as incomplete record
+ model = Travis.Repo.find(model.id) if model && !model.get
+ controller.set('repo', model)
+
+ serialize: (repo) ->
+ slug = if repo.get then repo.get('slug') else repo.slug
+ [owner, name] = slug.split('/')
+ { owner: owner, name: name }
+
+ deserialize: (params) ->
+ slug = "#{params.owner}/#{params.name}"
+ content = Ember.Object.create slug: slug, isLoaded: false, isLoading: true
+ proxy = Ember.ObjectProxy.create(content: content)
+
+ repos = Travis.Repo.bySlug(slug)
+
+ observer = ->
+ if repos.get 'isLoaded'
+ repos.removeObserver 'isLoaded', observer
+ proxy.set 'isLoading', false
+
+ if repos.get('length') == 0
+ # isError is also used in DS.Model, but maybe we should use something
+ # more focused like notFound later
+ proxy.set 'isError', true
+ else
+ proxy.set 'content', repos.objectAt(0)
+
+ if repos.length
+ proxy.set('content', repos[0])
else
- @transitionTo('root')
- @route(path)
+ repos.addObserver 'isLoaded', observer
- root: Ember.Route.extend
- route: '/'
- loading: Ember.State.extend()
- afterSignIn: (-> )
+ proxy
- auth: Ember.Route.extend
- route: '/auth'
- customRegexp: /^\/?auth($|\/)/
- connectOutlets: (router) ->
- router.get('applicationView').connectLayout 'simple'
- $('body').attr('id', 'auth')
- router.get('applicationController').connectOutlet('top', 'top')
- router.get('applicationController').connectOutlet('main', 'signin')
+Travis.IndexRoute = Ember.Route.extend
+ renderTemplate: ->
+ $('body').attr('id', 'home')
- afterSignIn: (router, path) ->
- router.route(path || '/')
+ @render 'repos', outlet: 'left'
+ @render 'sidebar', outlet: 'right'
+ @render 'top', outlet: 'top'
+ @render 'flash', outlet: 'flash'
- stats: Ember.Route.extend
- route: '/stats'
- customRegexp: /^\/?stats($|\/)/
- connectOutlets: (router) ->
- router.get('applicationView').connectLayout 'simple'
- $('body').attr('id', 'stats')
- router.get('applicationController').connectOutlet 'top', 'top'
- router.get('applicationController').connectOutlet 'main', 'stats'
+ setupController: (controller)->
+ @container.lookup('controller:repos').activate()
+ @container.lookup('controller:application').connectLayout 'home'
- profile: Ember.Route.extend
- initialState: 'index'
- route: '/profile'
+Travis.StatsRoute = Ember.Route.extend
+ renderTemplate: ->
+ $('body').attr('id', 'stats')
- connectOutlets: (router) ->
- router.get('applicationView').connectLayout 'profile'
- $('body').attr('id', 'profile')
- router.get('accountsController').set('content', Travis.Account.find())
- router.get('applicationController').connectOutlet 'top', 'top'
- router.get('applicationController').connectOutlet 'left', 'accounts'
- router.get('applicationController').connectOutlet 'flash', 'flash'
+ @render 'top', outlet: 'top'
+ @render 'stats'
- index: Ember.Route.extend
- route: '/'
- connectOutlets: (router) ->
- router.get('applicationController').connectOutlet 'main', 'profile'
- router.get('profileController').activate 'hooks'
+ setupController: ->
+ @container.lookup('controller:application').connectLayout('simple')
- account: Ember.Route.extend
- initialState: 'index'
- route: '/:login'
+Travis.NotFoundRoute = Ember.Route.extend
+ renderTemplate: ->
+ $('body').attr('id', 'not-found')
- connectOutlets: (router, account) ->
- if account
- params = { login: account.get('login') }
- router.get('profileController').setParams(params)
- else
- router.send 'showProfile'
+ @render 'top', outlet: 'top'
+ @render 'not_found'
- deserialize: (router, params) ->
- controller = router.get('accountsController')
+ setupController: ->
+ @container.lookup('controller:application').connectLayout('simple')
- unless controller.get 'content'
- controller.set('content', Travis.Account.find())
+Travis.ProfileRoute = Ember.Route.extend
+ needsAuth: true
- account = controller.findByLogin(params.login)
+ setupController: ->
+ @container.lookup('controller:application').connectLayout('profile')
+ @container.lookup('controller:accounts').set('content', Travis.Account.find())
- if account
- account
- else
- deferred = $.Deferred()
+ renderTemplate: ->
+ $('body').attr('id', 'profile')
- observer = ->
- if account = controller.findByLogin(params.login)
- controller.removeObserver 'content.length', observer
- deferred.resolve account
- controller.addObserver 'content.length', observer
+ @render 'top', outlet: 'top'
+ @render 'accounts', outlet: 'left'
+ @render 'flash', outlet: 'flash'
+ @render 'profile'
- deferred.promise()
+Travis.ProfileIndexRoute = Ember.Route.extend
+ setupController: ->
+ @container.lookup('controller:profile').activate 'hooks'
- serialize: (router, account) ->
- if account
- { login: account.get('login') }
- else
- {}
+ renderTemplate: ->
+ @render 'hooks', outlet: 'pane', into: 'profile', controller: 'profile'
- index: Ember.Route.extend
- route: '/'
- connectOutlets: (router) ->
- router.get('profileController').activate 'hooks'
+Travis.AccountRoute = Ember.Route.extend
+ setupController: (controller, account) ->
+ profileController = @container.lookup('controller:profile')
+ profileController.activate 'hooks'
- profile: Ember.Route.extend
- route: '/profile'
+ if account
+ params = { login: account.get('login') }
+ profileController.setParams(params)
- connectOutlets: (router) ->
- router.get('profileController').activate 'user'
+ deserialize: (params) ->
+ controller = @container.lookup('controller:accounts')
+ account = controller.findByLogin(params.login)
- home: Ember.Route.extend
- route: '/'
- connectOutlets: (router) ->
- router.get('applicationView').connectLayout 'home'
- $('body').attr('id', 'home')
- router.get('applicationController').connectOutlet 'left', 'repos'
- router.get('applicationController').connectOutlet 'right', 'sidebar'
- router.get('applicationController').connectOutlet 'top', 'top'
- router.get('applicationController').connectOutlet 'main', 'repo'
- router.get('applicationController').connectOutlet 'flash', 'flash'
- router.get('reposController').activate()
- router.get('repoController').set('repos', router.get('reposController'))
+ if account
+ account
+ else
+ content = Ember.Object.create(login: params.login)
+ proxy = Ember.ObjectProxy.create(content: content)
- show: Ember.Route.extend
- route: '/'
- connectOutlets: (router) ->
- router.get('repoController').activate('index')
+ observer = ->
+ if account = controller.findByLogin(params.login)
+ controller.removeObserver 'content.length', observer
+ proxy.set('content', account)
+ controller.addObserver 'content.length', observer
- initialState: 'default'
- default: defaultRoute
- lineNumber: lineNumberRoute
+ proxy
- showWithLineNumber: Ember.Route.extend
- route: '/#/L:number'
- connectOutlets: (router) ->
- router.get('repoController').activate('index')
+ serialize: (account) ->
+ if account
+ { login: account.get('login') }
+ else
+ {}
- repo: Ember.Route.extend
- route: '/:owner/:name'
- dynamicSegmentPattern: "([^/#]+)"
+Travis.AccountIndexRoute = Ember.Route.extend
+ setupController: ->
+ @container.lookup('controller:profile').activate 'hooks'
- connectOutlets: (router, repo) ->
- if repo && repo.constructor != Travis.Repo
- repo = Travis.Repo.find(repo.id)
- router.get('repoController').set 'repo', repo
+ renderTemplate: ->
+ @render 'hooks', outlet: 'pane', into: 'profile'
- deserialize: (router, params) ->
- slug = "#{params.owner}/#{params.name}"
- repos = Travis.Repo.bySlug(slug)
- deferred = $.Deferred()
+Travis.AccountProfileRoute = Ember.Route.extend
+ setupController: ->
+ @container.lookup('controller:profile').activate 'user'
- observer = ->
- if repos.get 'isLoaded'
- repos.removeObserver 'isLoaded', observer
- deferred.resolve repos.objectAt(0)
+ renderTemplate: ->
+ @render 'user', outlet: 'pane', into: 'profile'
- if repos.length
- deferred.resolve repos[0]
- else
- repos.addObserver 'isLoaded', observer
+Travis.AuthRoute = Ember.Route.extend
+ renderTemplate: ->
+ $('body').attr('id', 'auth')
- deferred.promise()
+ @render 'top', outlet: 'top'
+ @render 'auth.signin'
- serialize: (router, repo) ->
- if typeof repo == 'string'
- [owner, name] = repo.split '/'
- { owner: owner, name: name }
- else if repo && repo.constructor == Travis.Repo
- { owner: repo.get('owner'), name: repo.get('name') }
- else if repo && repo.id && repo.slug
- [owner, name] = repo.slug.split '/'
- { owner: owner, name: name }
- else
- # TODO: it would be nice to handle 404 somehow
- {}
-
- show: Ember.Route.extend
- route: '/'
- connectOutlets: (router) ->
- router.get('repoController').activate('current')
-
- initialState: 'default'
- default: defaultRoute
- lineNumber: lineNumberRoute
-
- builds: Ember.Route.extend
- route: '/builds'
-
- index: Ember.Route.extend
- route: '/'
- connectOutlets: (router, repo) ->
- router.get('repoController').activate 'builds'
-
- show: Ember.Route.extend
- route: '/:build_id'
- connectOutlets: (router, build) ->
- unless build.get
- # TODO: apparently when I use id in url, it will pass it
- # here, why doesn't it use deserialize?
- build = Travis.Build.find(build)
- router.get('repoController').set 'build', build
- router.get('repoController').activate 'build'
-
- serialize: (router, build) ->
- if build.get
- { build_id: build.get('id') }
- else
- { build_id: build }
-
- deserialize: (router, params) ->
- # Something is wrong here. If I don't use deferred, id is not
- # initialized and url ends up being /jobs/null
- # This should not be needed, as id should be immediately set on the
- # record.
- # TODO: find out why it happens
- build = Travis.Build.find params.build_id
-
- if build.get 'id'
- build
- else
- deferred = $.Deferred()
-
- observer = ->
- if build.get 'id'
- build.removeObserver 'id', observer
- deferred.resolve build
-
- build.addObserver 'id', observer
-
- deferred.promise()
-
- # TODO: this is not dry, but for some weird
- # reason Mixins don't play nice with Ember.Route
- initialState: 'default'
- default: defaultRoute
- lineNumber: lineNumberRoute
- dynamicSegmentPattern: "([^/#]+)"
-
- logRedirect: Ember.Route.extend
- route: '/log.txt'
- connectOutlets: (router) ->
- build = router.get('repoController').get 'build'
-
- observer = ->
- if logId = build.get('jobs.firstObject.log.id')
- window.location = Travis.Urls.plainTextLog(logId)
-
- build.removeObserver('jobs.firstObject.log.id', observer)
-
- build.addObserver('jobs.firstObject.log.id', observer)
-
- pullRequests: Ember.Route.extend
- route: '/pull_requests'
- connectOutlets: (router, repo) ->
- router.get('repoController').activate 'pull_requests'
-
- branches: Ember.Route.extend
- route: '/branches'
- connectOutlets: (router, repo) ->
- router.get('repoController').activate 'branches'
-
- events: Ember.Route.extend
- route: '/events'
- connectOutlets: (router, repo) ->
- router.get('repoController').activate 'events'
-
- job: Ember.Route.extend
- route: '/jobs/:job_id'
- dynamicSegmentPattern: "([^/#]+)"
- connectOutlets: (router, job) ->
- unless job.get
- # In case I use id
- job = Travis.Job.find(job)
- router.get('repoController').set 'job', job
- router.get('repoController').activate 'job'
-
- serialize: (router, job) ->
- if job.get
- { job_id: job.get('id') }
- else
- { job_id: job }
-
- deserialize: (router, params) ->
- job = Travis.Job.find params.job_id
-
- if job.get 'id'
- job
- else
- deferred = $.Deferred()
-
- observer = ->
- if job.get 'id'
- job.removeObserver 'id', observer
- deferred.resolve job
- job.addObserver 'id', observer
- deferred.promise()
-
- initialState: 'default'
- default: defaultRoute
- lineNumber: lineNumberRoute
-
- logRedirect: Ember.Route.extend
- route: '/log.txt'
- connectOutlets: (router, job) ->
- job = router.get('repoController').get 'job'
-
- observer = ->
- if logId = job.get('log.id')
- window.location = Travis.Urls.plainTextLog(logId)
-
- job.removeObserver('log.id', observer)
-
- job.addObserver('log.id', observer)
+ setupController: ->
+ @container.lookup('controller:application').connectLayout('simple')
diff --git a/assets/scripts/app/store.coffee b/assets/scripts/app/store.coffee
index 2ad698cb..92519e07 100644
--- a/assets/scripts/app/store.coffee
+++ b/assets/scripts/app/store.coffee
@@ -1,30 +1,23 @@
require 'store/rest_adapter'
-DATA_PROXY =
- get: (name) ->
- @savedData[name]
+coerceId = (id) -> if id == null then null else id+''
Travis.Store = DS.Store.extend
- revision: 4
+ revision: 12
adapter: Travis.RestAdapter.create()
init: ->
@_super.apply this, arguments
@_loadedData = {}
+ @clientIdToComplete = {}
- load: (type, id, hash) ->
+ load: (type, data, prematerialized) ->
result = @_super.apply this, arguments
- if result && result.clientId
+ if result && result.clientId && @clientIdToComplete[result.clientId] == undefined
# I assume that everything that goes through load is complete record
# representation, incomplete hashes from pusher go through merge()
- record = @findByClientId type, result.clientId
- record.set 'incomplete', false
- record.set 'complete', true
- # setting both incomplete and complete may be weird, but it's easier to
- # work with both values. I need to check if record has already been completed
- # and in order to do that, without having 'complete', I would need to check
- # for incomplete == false, which looks worse
+ @clientIdToComplete[result.clientId] = true
result
@@ -34,38 +27,26 @@ Travis.Store = DS.Store.extend
array.set('isLoaded', true) for array in @typeMapFor(type).recordArrays
result
- merge: (type, id, hash) ->
- if hash == undefined
- hash = id
- primaryKey = type.proto().primaryKey
- Ember.assert("A data hash was loaded for a record of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", hash[primaryKey])
- id = hash[primaryKey]
+ merge: (type, data, incomplete) ->
+ id = coerceId data.id
- typeMap = @typeMapFor(type)
- dataCache = typeMap.cidToHash
- clientId = typeMap.idToCid[id]
- recordCache = @get('recordCache')
-
- if clientId != undefined
- if (data = dataCache[clientId]) && (typeof data == 'object')
- for key, value of hash
- if ( descriptor = Object.getOwnPropertyDescriptor(data, key) ) && descriptor.set
- Ember.set(data, key, value)
- else
- data[key] = value
- else
- dataCache[clientId] = hash
-
- if record = recordCache[clientId]
- record.send('didChangeData')
+ typeMap = @typeMapFor(type)
+ clientId = typeMap.idToCid[id]
+ record = @recordCache[clientId]
+ if record
+ @get('adapter').merge(this, record, data)
else
- clientId = @pushHash(hash, id, type)
+ if (savedData = @clientIdToData[clientId]) && savedData.id?
+ $.extend(savedData, data)
+ else
+ result = @load(type, data, {id: data.id})
- if clientId
- DATA_PROXY.savedData = hash
- @updateRecordArrays(type, clientId, DATA_PROXY)
+ if result && result.clientId
+ clientId = result.clientId
+ if incomplete
+ @clientIdToComplete[result.clientId] = false
- { id: id, clientId: clientId }
+ { clientId: clientId, id: id }
isInStore: (type, id) ->
!!@typeMapFor(type).idToCid[id]
@@ -76,6 +57,7 @@ Travis.Store = DS.Store.extend
mappings = @adapter.get('mappings')
type = mappings[name]
+
if event == 'build:started' && data.build.commit
# TODO: commit should be a sideload record on build, not mixed with it
build = data.build
@@ -96,8 +78,10 @@ Travis.Store = DS.Store.extend
if event == 'job:log'
- if job = @find(Travis.Job, data['job']['id'])
- job.appendLog(data['job']['_log'])
+ console.log 'store: received job:log event', data if Log.DEBUG
+ data = data.job
+ job = @find(Travis.Job, data.id)
+ job.appendLog(number: parseInt(data.number), content: data._log)
else if data[type.singularName()]
@_loadOne(this, type, data)
else if data[type.pluralName()]
@@ -113,14 +97,29 @@ Travis.Store = DS.Store.extend
if type == Travis.Build && (json.repository || json.repo)
@loadIncomplete(Travis.Repo, json.repository || json.repo)
- @loadIncomplete(type, json[root])
+ result = @loadIncomplete(type, json[root])
+ if result.id
+ @find(type, result.id)
addLoadedData: (type, clientId, hash) ->
id = hash.id
@_loadedData[type.toString()] ||= {}
loadedData = (@_loadedData[type][clientId] ||= [])
- for key of hash
- loadedData.pushObject key unless loadedData.contains(key)
+
+ serializer = @get('adapter.serializer')
+
+ Ember.get(type, 'attributes').forEach( (name, meta) ->
+ value = @extractAttribute(type, hash, name)
+ if value != undefined
+ loadedData.pushObject name unless loadedData.contains(name)
+ , serializer)
+
+ Ember.get(type, 'relationshipsByName').forEach( (name, relationship) ->
+ key = @_keyForBelongsTo(type, relationship.key)
+ value = @extractBelongsTo(type, hash, key)
+ if value != undefined
+ loadedData.pushObject name unless loadedData.contains(name)
+ , serializer)
isDataLoadedFor: (type, clientId, key) ->
if recordsData = @_loadedData[type.toString()]
@@ -130,39 +129,48 @@ Travis.Store = DS.Store.extend
loadIncomplete: (type, hash, options) ->
options ?= {}
- id = hash.id
+ id = coerceId hash.id
typeMap = @typeMapFor(type)
- dataCache = typeMap.cidToHash
+ cidToData = @clientIdToData
clientId = typeMap.idToCid[id]
- if dataCache[clientId] && options.skipIfExists
+ if clientId && cidToData[clientId] && options.skipIfExists
return
- result = @merge(type, hash)
-
+ result = @merge(type, hash, true)
if result && result.clientId
@addLoadedData(type, result.clientId, hash)
- record = @findByClientId(type, result.clientId)
- unless record.get('complete')
- record.loadedAsIncomplete()
+ #@_updateRelationships(type, hash)
- @_updateAssociations(type, type.singularName(), hash)
+ result
- record
+ materializeRecord: (type, clientId, id) ->
+ record = @_super.apply this, arguments
+
+ if @clientIdToComplete[clientId] != undefined && !@clientIdToComplete[clientId]
+ record.set 'incomplete', true
+ else
+ record.set 'incomplete', false
+
+ record
_loadMany: (store, type, json) ->
root = type.pluralName()
@adapter.sideload(store, type, json, root)
@loadMany(type, json[root])
- _updateAssociations: (type, name, data) ->
- Em.get(type, 'associationsByName').forEach (key, meta) =>
+ _updateRelationships: (type, data) ->
+ Em.get(type, 'relationshipsByName').forEach (key, meta) =>
if meta.kind == 'belongsTo'
id = data["#{key}_id"]
if clientId = @typeMapFor(meta.type).idToCid[id]
if parent = this.findByClientId(meta.type, clientId, id)
dataProxy = parent.get('data')
- if ids = dataProxy.get("#{name}_ids")
- ids.pushObject(data.id) unless data.id in ids
- parent.send('didChangeData');
+ if ids = dataProxy['hasMany'][type.pluralName()]
+ unless data.id in ids
+ state = parent.get('stateManager.currentState.path')
+ unless state == "rootState.loaded.materializing"
+ parent.send('materializingData')
+ ids.pushObject(data.id)
+ parent.notifyPropertyChange('data')
diff --git a/assets/scripts/app/store/rest_adapter.coffee b/assets/scripts/app/store/rest_adapter.coffee
index 66aaba02..b240e3d4 100644
--- a/assets/scripts/app/store/rest_adapter.coffee
+++ b/assets/scripts/app/store/rest_adapter.coffee
@@ -1,7 +1,67 @@
require 'travis/ajax'
require 'models'
-@Travis.RestAdapter = DS.RESTAdapter.extend
+DS.JSONTransforms['object'] = {
+ deserialize: (serialized) -> serialized
+ serialize: (deserialized) -> deserialized
+}
+
+Travis.Serializer = DS.RESTSerializer.extend
+ # The next 3 methods specify the behavior of adding records to dirty sets
+ # (ie. which records will be treated as dirty on the next commit). We don't
+ # allow to change most of the records on the client, so for anything except
+ # the User, we ignore dirtyiness.
+ dirtyRecordsForAttributeChange: (dirtySet, record) ->
+ if record.constructor == Travis.User
+ @_super.apply this, arguments
+
+ dirtyRecordsForBelongsToChange: (dirtySet, record) ->
+ if record.constructor == Travis.User
+ @_super.apply this, arguments
+
+ dirtyRecordsForHasManyChange: (dirtySet, record) ->
+ if record.constructor == Travis.User
+ @_super.apply this, arguments
+
+ merge: (record, serialized) ->
+ data = record.get('data')
+
+ # TODO: write test that ensures that we go to materializingData
+ # only if we can
+ state = record.get('stateManager.currentState.path')
+ unless state == "rootState.loaded.materializing"
+ record.send('materializingData')
+
+ record.eachAttribute( (name, attribute) ->
+ value = @extractAttribute(record.constructor, serialized, name)
+ if value != undefined
+ value = @deserializeValue(value, attribute.type)
+ if value != data.attributes[name]
+ record.materializeAttribute(name, value)
+ record.notifyPropertyChange(name)
+ , this)
+
+ record.eachRelationship( (name, relationship) ->
+ if relationship.kind == 'belongsTo'
+ key = @_keyForBelongsTo(record.constructor, relationship.key)
+ value = @extractBelongsTo(record.constructor, serialized, key)
+
+ if value != undefined && data.belongsTo[name] != value
+ record.materializeBelongsTo name, value
+ record.notifyPropertyChange(name)
+ else if relationship.kind == 'hasMany'
+ key = @_keyForHasMany(record.constructor, relationship.key)
+ value = @extractHasMany(record.constructor, serialized, key)
+
+ if value != undefined
+ record.materializeHasMany name, value
+ record.notifyPropertyChange(name)
+ , this)
+
+ record.notifyPropertyChange('data')
+
+Travis.RestAdapter = DS.RESTAdapter.extend
+ serializer: Travis.Serializer
mappings:
broadcasts: Travis.Broadcast
repositories: Travis.Repo
@@ -38,3 +98,53 @@ require 'models'
return
else
@_super.apply this, arguments
+
+ merge: (store, record, serialized) ->
+ @get('serializer').merge(record, serialized)
+
+ didFindRecord: (store, type, payload, id) ->
+ if (type == Travis.Build || type == Travis.Job) && payload.commit?
+ payload.commits = payload.commit
+ delete payload.commit
+
+ @_super.apply this, arguments
+
+ didSaveRecord: (store, type, record, payload) ->
+ # API sometimes return { result: true } response
+ # which does not play nice with ember-data. For now
+ # let's just change payload to have serialized record
+ # included, but ideally it should be fixed in the API
+ # to be consistent across all the endpoints.
+ if payload?.result == true
+ payload = {}
+ payload[type.singularName()] = record.serialize()
+
+ @_super(store, type, record, payload)
+
+Travis.RestAdapter.map 'Travis.Commit', {}
+
+Travis.RestAdapter.map 'Travis.Build', {
+ repoId: { key: 'repository_id' }
+ repo: { key: 'repository_id' }
+ _duration: { key: 'duration' }
+ jobs: { key: 'job_ids' }
+ _config: { key: 'config' }
+}
+
+Travis.RestAdapter.map 'Travis.Repo', {
+ _lastBuildDuration: { key: 'last_build_duration' }
+}
+
+Travis.RestAdapter.map 'Travis.Job', {
+ repoId: { key: 'repository_id' }
+ repo: { key: 'repository_id' }
+ _config: { key: 'config' }
+}
+
+Travis.RestAdapter.map 'Travis.User', {
+ _name: { key: 'name' }
+}
+
+Travis.RestAdapter.map 'Travis.Sponsor', {
+ _image: { key: 'image' }
+}
diff --git a/assets/scripts/app/tailing.coffee b/assets/scripts/app/tailing.coffee
index 9c195fd4..fab3fec8 100644
--- a/assets/scripts/app/tailing.coffee
+++ b/assets/scripts/app/tailing.coffee
@@ -12,7 +12,7 @@ $.extend Travis.Tailing.prototype,
@positionButton()
Ember.run.later(@run.bind(this), @options.timeout) if @active()
- toggle: (event) ->
+ toggle: ->
if @active() then @stop() else @start()
active: ->
diff --git a/assets/scripts/app/templates/auth/signin.hbs b/assets/scripts/app/templates/auth/signin.hbs
index a5609048..867fa2b5 100644
--- a/assets/scripts/app/templates/auth/signin.hbs
+++ b/assets/scripts/app/templates/auth/signin.hbs
@@ -6,6 +6,6 @@
{{else}}
- Please sign in with GitHub. + Please sign in with GitHub.
{{/if}} diff --git a/assets/scripts/app/templates/builds/list.hbs b/assets/scripts/app/templates/builds/list.hbs index 6dcdcd4f..d0a928d2 100644 --- a/assets/scripts/app/templates/builds/list.hbs +++ b/assets/scripts/app/templates/builds/list.hbs @@ -1,4 +1,4 @@ -{{#if builds.isLoaded}} +{{#if content.isLoaded}}{{#if id}} - + {{#linkTo "build" repo this}} {{number}} - + {{/linkTo}} {{/if}} | @@ -42,7 +42,7 @@ | {{commit.committerName}} | - {{#if commit.pullRequestNumber}} + {{#if view.isPullRequestsList}}#{{commit.pullRequestNumber}} @@ -59,9 +59,11 @@ {{/each}} |
- {{view view.ShowMoreButton}} -
+ {{#if displayShowMoreButton}} ++ {{view view.ShowMoreButton}} +
+ {{/if}} {{else}}{{t builds.messages.sponsored_by}} {{sponsor.name}} diff --git a/assets/scripts/app/templates/jobs/pre.hbs b/assets/scripts/app/templates/jobs/pre.hbs index 72cb78d1..9f1a7dac 100644 --- a/assets/scripts/app/templates/jobs/pre.hbs +++ b/assets/scripts/app/templates/jobs/pre.hbs @@ -1,4 +1,14 @@ -
- - -+
+ This log is too long to be displayed. Please reduce the verbosity of your + build or download the the raw log. +
+ {{/if}} +
- {{t layouts.top.github_login}}
- {{view.userName}}
+ {{t layouts.top.github_login}}
+ {{#linkTo "profile.index" class="signed-in"}}
{{view.userName}}{{/linkTo}}
{{t layouts.top.signing_in}}
Repositories: {{view.account.reposCount}} diff --git a/assets/scripts/app/templates/profile/tabs.hbs b/assets/scripts/app/templates/profile/tabs.hbs index 05b4bacd..8f770575 100644 --- a/assets/scripts/app/templates/profile/tabs.hbs +++ b/assets/scripts/app/templates/profile/tabs.hbs @@ -1,13 +1,17 @@
Last synchronized from GitHub: {{formatTime user.syncedAt}} - + Sync now
@@ -24,7 +24,7 @@{{description}}
- - {{view Travis.RepoShowStatsView}} - {{view Travis.RepoShowTabsView}} - {{view Travis.RepoShowToolsView}} - {{/with}} - + {{#if isError}} + The repository at {{slug}} was not found. {{else}} - Loading - {{/if}} + {{#if repo.isLoaded}} + {{#with repo}} +{{description}}
+ + {{view Travis.RepoShowTabsView}} + {{view Travis.RepoShowToolsView}} + {{/with}} + +Log was too long to display. Download the the raw version to get the full log.
") - diff --git a/assets/scripts/app/views/left.coffee b/assets/scripts/app/views/left.coffee index ced9bac4..f79cf82a 100644 --- a/assets/scripts/app/views/left.coffee +++ b/assets/scripts/app/views/left.coffee @@ -10,9 +10,9 @@ classOwned: (-> classes = [] classes.push('active') if @get('tab') == 'owned' - classes.push('display') if Travis.app.get('currentUser') + classes.push('display') if @get('controller.currentUser') classes.join(' ') - ).property('tab', 'Travis.currentUser') + ).property('tab', 'controller.currentUser') classSearch: (-> 'active' if @get('tab') == 'search' diff --git a/assets/scripts/app/views/log.coffee b/assets/scripts/app/views/log.coffee new file mode 100644 index 00000000..1fab0507 --- /dev/null +++ b/assets/scripts/app/views/log.coffee @@ -0,0 +1,272 @@ +require 'log' +require 'travis/ordered_log' + +Log.DEBUG = true + +Travis.UnorderedLogEngineMixin = Ember.Mixin.create + setupEngine: -> + console.log 'log view: create engine' if Log.DEBUG + @limit = new Log.Limit + @scroll = new Log.Scroll + @engine = Log.create(listeners: [new Log.FragmentRenderer, new Log.Folds, @scroll]) + @observeParts() + @numberLineOnHover() + + destroyEngine: -> + parts = @get('log.parts') + parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') + + observeParts: -> + parts = @get('log.parts') + parts.addArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') + parts = parts.slice(0) + @partsDidChange(parts, 0, null, parts.length) + + partsDidChange: (parts, start, _, added) -> + console.log 'log view: parts did change' if Log.DEBUG + for part, i in parts.slice(start, start + added) + # console.log "limit in log view: #{@get('limited')}" + break if @get('limited') + @engine.set(part.number, part.content) + @propertyDidChange('limited') + + lineNumberDidChange: (-> + @scroll.set(number) if !@get('isDestroyed') && number = @get('controller.lineNumber') + ).observes('controller.lineNumber') + + limited: (-> + @limit && @limit.limited + ).property() + +Travis.OrderedLogEngineMixin = Ember.Mixin.create + setupEngine: -> + @set('logManager', Travis.OrderedLog.create(target: this)) + + @get('logManager').append @get('log.parts') + + @get('log.parts').addArrayObserver this, + didChange: 'partsDidChange' + willChange: 'noop' + + destroyEngine: (view) -> + @get('logManager').destroy() + @get('log.parts').removeArrayObserver this, + didChange: 'partsDidChange' + willChange: 'noop' + + partsDidChange: (parts, index, removedCount, addedCount) -> + addedParts = parts.slice(index, index + addedCount) + @get('logManager').append addedParts + + lineNumberDidChange: (-> + if number = @get('controller.lineNumber') + @tryScrollingToHashLineNumber(number) + ).observes('controller.lineNumber') + + scrollTo: (id) -> + # and this is even more weird, when changing hash in URL in firefox + # to other value, for example #L10, it actually scrolls just #main + # element... this is probably some CSS issue, I don't have time to + # investigate at the moment + # TODO: fix this + $('#main').scrollTop 0 + + # weird, html works in chrome, body in firefox + $('html,body').scrollTop $(id).offset().top + + @set 'controller.lineNumber', null + + tryScrollingToHashLineNumber: (number) -> + id = "#L#{number}" + checker = => + return if @get('isDestroyed') + + if $(id).length + @scrollTo(id) + else + setTimeout checker, 100 + + checker() + + appendLog: (payloads) -> + url = @get('logUrl') + + leftOut = [] + cut = false + fragment = document.createDocumentFragment() + + # TODO: refactor this loop, it's getting messy + for payload in payloads + line = payload.content + number = payload.number + + if payload.logWasCut + cut = true + else + unless payload.append + pathWithNumber = "#{url}#L#{number}" + p = document.createElement('p') + p.innerHTML = "#{line}" + line = p + + if payload.fold && !payload.foldContinuation + div = document.createElement('div') + div.appendChild line + div.className = "fold #{payload.fold} show-first-line" + line = div + + if payload.replace + if link = fragment.querySelector("#L#{number}") + link.parentElement.innerHTML = line.innerHTML + else + this.$("#L#{number}").parent().replaceWith line + else if payload.append + if link = fragment.querySelector("#L#{number}") + link.parentElement.innerHTML += line + else + this.$("#L#{number}").parent().append line + else if payload.foldContinuation + folds = fragment.querySelectorAll(".fold.#{payload.fold}") + if fold = folds[folds.length - 1] + fold.appendChild line + else + this.$("#log .fold.#{payload.fold}:last").append line + else + fragment.appendChild(line) + + if payload.openFold + folds = fragment.querySelectorAll(".fold.#{payload.openFold}") + if fold = folds[folds.length - 1] + fold = $(fold) + else + fold = this.$(".fold.#{payload.openFold}:last") + + fold.removeClass('show-first-line').addClass('open') + + if payload.foldEnd + folds = fragment.querySelectorAll(".fold.#{payload.fold}") + if fold = folds[folds.length - 1] + fold = $(fold) + else + fold = this.$(".fold.#{payload.fold}:last") + + fold.removeClass('show-first-line') + + this.$('#log')[0].appendChild fragment + if cut + url = Travis.Urls.plainTextLog(@get('log.id')) + this.$("#log").append $("Log was too long to display. Download the the raw version to get the full log.
") + +Travis.reopen + LogView: Travis.View.extend + templateName: 'jobs/log' + logBinding: 'job.log' + contextBinding: 'job' + + didInsertElement: -> + job = @get('job') + job.subscribe() if job && !job.get('isFinished') + + willDestroyElement: -> + job = @get('job') + job.unsubscribe() if job + + toTop: () -> + $(window).scrollTop(0) + + PreView: Em.View.extend(Travis.OrderedLogEngineMixin, { + templateName: 'jobs/pre' + + didInsertElement: -> + console.log 'log view: did insert' if Log.DEBUG + @_super.apply this, arguments + @setupEngine() + @lineNumberDidChange() + + willDestroyElement: -> + console.log 'log view: will destroy' if Log.DEBUG + @destroyEngine() + + versionDidChange: (-> + @rerender() if @get('inDOM') + ).observes('log.version') + + logDidChange: (-> + console.log 'log view: log did change: rerender' if Log.DEBUG + @rerender() if @get('inDOM') + ).observes('log') + + + plainTextLogUrl: (-> + Travis.Urls.plainTextLog(id) if id = @get('log.job.id') + ).property('job.log.id') + + toggleTailing: -> + Travis.tailing.toggle() + event.preventDefault() + + numberLineOnHover: -> + $('#log').on 'mouseenter', 'a', -> + $(this).attr('href', '#L' + ($(this.parentNode).prevAll('p:visible').length + 1)) + + click: -> + if (href = $(event.target).attr('href')) && matches = href?.match(/#L(\d+)$/) + @lineNumberClicked(matches[1]) + event.stopPropagation() + false + else + target = $(event.target) + target.closest('.fold').toggleClass('open') + + logUrl: (-> + if item = @get('controller.currentItem') + if repo = item.get('repo') + name = if item.constructor == Travis.Build + 'build' + else + 'job' + + Travis.__container__.lookup('router:main').generate(name, repo, item) + ).property('controller.currentItem.repo', 'controller.currentItem') + + lineNumberClicked: (number) -> + path = @get('logUrl') + "#L#{number}" + window.history.replaceState({ path: path }, null, path); + @set('controller.lineNumber', number) + + noop: -> # TODO required? + }) + +Log.Scroll = -> +Log.Scroll.prototype = $.extend new Log.Listener, + set: (number) -> + return unless number + @number = number + @tryScroll() + + insert: (log, line, pos) -> + @tryScroll() if @number + true + + tryScroll: -> + if element = $("#log p:visible")[@number - 1] + $('#main').scrollTop(0) + $('html, body').scrollTop($(element).offset()?.top) # weird, html works in chrome, body in firefox + @highlight(element) + @number = undefined + + highlight: (element) -> + $('#log p.highlight').removeClass('highlight') + $(element).addClass('highlight') + +Log.Logger = -> +Log.Logger.prototype = $.extend new Log.Listener, + receive: (log, num, string) -> + @log("rcv #{num} #{JSON.stringify(string)}") + true + insert: (log, element, pos) -> + @log("ins #{element.id}, #{if pos.before then 'before' else 'after'}: #{pos.before || pos.after || '?'}, #{JSON.stringify(element)}") + remove: (log, element) -> + @log("rem #{element.id}") + log: (line) -> + console.log(line) diff --git a/assets/scripts/app/views/profile.coffee b/assets/scripts/app/views/profile.coffee index 8af689c7..234fbde5 100644 --- a/assets/scripts/app/views/profile.coffee +++ b/assets/scripts/app/views/profile.coffee @@ -1,4 +1,4 @@ -@Travis.reopen +Travis.reopen ProfileView: Travis.View.extend templateName: 'profile/show' accountBinding: 'controller.account' @@ -11,7 +11,7 @@ templateName: 'profile/tabs' tabBinding: 'controller.tab' - activate: (event) -> + activate: -> @get('controller').activate(event.target.name) classHooks: (-> @@ -63,7 +63,7 @@ { key: 'de', name: 'Deutsch' } ] ).property() - - change: (event) -> + + change: -> return unless $('#locale').val() @get('user').updateLocale($('#locale').val()) diff --git a/assets/scripts/app/views/repo/list.coffee b/assets/scripts/app/views/repo/list.coffee index b4f5ae26..c94bd6f6 100644 --- a/assets/scripts/app/views/repo/list.coffee +++ b/assets/scripts/app/views/repo/list.coffee @@ -13,7 +13,9 @@ repoBinding: 'content' classNames: ['repo'] classNameBindings: ['color', 'selected'] - selectedBinding: 'repo.selected' + selected: (-> + @get('content') == @get('controller.selectedRepo') + ).property('controller.selectedRepo') color: (-> Travis.Helpers.colorForState(@get('repo.lastBuildState')) @@ -22,9 +24,10 @@ ReposListTabsView: Travis.View.extend templateName: 'repos/list/tabs' tabBinding: 'controller.tab' + currentUserBinding: 'controller.currentUser.id' - activate: (event) -> - @get('controller').activate(event.target.name) + activate: (name) -> + @get('controller').activate(name) classRecent: (-> 'active' if @get('tab') == 'recent' @@ -33,13 +36,13 @@ classOwned: (-> classes = [] classes.push('active') if @get('tab') == 'owned' - classes.push('display-inline') if Travis.app.get('currentUser') + classes.push('display-inline') if @get('currentUser') classes.join(' ') - ).property('tab', 'Travis.app.currentUser') + ).property('tab', 'currentUser') classSearch: (-> 'active' if @get('tab') == 'search' ).property('tab') - toggleInfo: (event) -> + toggleInfo: -> $('#repos').toggleClass('open') diff --git a/assets/scripts/app/views/repo/show.coffee b/assets/scripts/app/views/repo/show.coffee index b9ede214..0c0e317d 100644 --- a/assets/scripts/app/views/repo/show.coffee +++ b/assets/scripts/app/views/repo/show.coffee @@ -1,35 +1,15 @@ -@Travis.reopen +Travis.reopen RepoView: Travis.View.extend templateName: 'repos/show' - reposBinding: 'controller.repos' - repoBinding: 'controller.repo' + reposBinding: 'controllers.repos' - class: (-> - 'loading' unless @get('repo.isLoaded') - ).property('repo.isLoaded') + classNameBindings: ['controller.isLoading:loading'] isEmpty: (-> @get('repos.isLoaded') && @get('repos.length') == 0 ).property('repos.isLoaded', 'repos.length') - urlGithub: (-> - Travis.Urls.githubRepo(@get('repo.slug')) - ).property('repo.slug'), - - RepoShowStatsView: Travis.View.extend - templateName: 'repos/show/stats' - repoBinding: 'parentView.repo' - statsBinding: 'repo.stats' - - urlGithubWatchers: (-> - Travis.Urls.githubWatchers(@get('repo.slug')) - ).property('repo.slug'), - - urlGithubNetwork: (-> - Travis.Urls.githubNetwork(@get('repo.slug')) - ).property('repo.slug'), - ReposEmptyView: Travis.View.extend template: '' @@ -81,13 +61,15 @@ buildBinding: 'controller.build' jobBinding: 'controller.job' tabBinding: 'controller.tab' + currentUserBinding: 'controller.currentUser' closeMenu: -> + console.log 'closeMenu' $('.menu').removeClass('display') - menu: (event) -> + menu: -> @popupCloseAll() - element = $('#tools .menu').toggleClass('display') + $('#tools .menu').toggleClass('display') event.stopPropagation() requeue: -> @@ -104,7 +86,7 @@ @closeMenu() @get('job').cancel() - statusImages: (event) -> + statusImages: -> @set('active', true) @closeMenu() @popupCloseAll() @@ -115,7 +97,7 @@ view.appendTo($('body')) event.stopPropagation() - regenerateKeyPopup: (event) -> + regenerateKeyPopup: -> if @get('canRegenerateKey') @set('active', true) @closeMenu() @@ -134,13 +116,12 @@ regenerateKey: -> @popupCloseAll() - self = this - @get('repo').regenerateKey - success: -> - self.popup('regeneration-success') + (@get('repo.content') || @get('repo')).regenerateKey + success: => + @popup('regeneration-success') error: -> - Travis.app.router.flashController.loadFlashes([{ error: 'Travis encountered an error while trying to regenerate the key, please try again.'}]) + Travis.lookup('controller:flash').loadFlashes([{ error: 'Travis encountered an error while trying to regenerate the key, please try again.'}]) displayRequeueBuild: (-> @get('isBuildTab') && @get('build.isFinished') @@ -208,6 +189,6 @@ ).property('tab') hasPermission: (-> - if permissions = Travis.app.get('currentUser.permissions') - permissions.contains @get('repo.id') - ).property('Travis.app.currentUser.permissions.length', 'repo.id') + if permissions = @get('currentUser.permissions') + permissions.contains parseInt(@get('repo.id')) + ).property('currentUser.permissions.length', 'repo.id') diff --git a/assets/scripts/app/views/sidebar.coffee b/assets/scripts/app/views/sidebar.coffee index dbee7c7c..782171f8 100644 --- a/assets/scripts/app/views/sidebar.coffee +++ b/assets/scripts/app/views/sidebar.coffee @@ -4,64 +4,38 @@ DecksView: Em.View.extend templateName: "sponsors/decks" - controller: Travis.SponsorsController.create - perPage: 1 - - didInsertElement: -> - controller = @get 'controller' - unless controller.get('content') - Travis.app.get('router.sidebarController').tickables.push(controller) - controller.set 'content', Travis.Sponsor.decks() + init: -> @_super.apply this, arguments + @set 'controller', @get('controller').container.lookup('controller:decks') LinksView: Em.View.extend templateName: "sponsors/links" - controller: Travis.SponsorsController.create - perPage: 6 - - didInsertElement: -> - controller = @get 'controller' - unless controller.get('content') - controller.set 'content', Travis.Sponsor.links() - Travis.app.get('router.sidebarController').tickables.push(controller) + init: -> @_super.apply this, arguments + @set 'controller', @get('controller').container.lookup('controller:links') WorkersView: Em.View.extend templateName: 'workers/list' - controller: Travis.WorkersController.create() - - didInsertElement: -> - @set 'controller.content', Travis.Worker.find() + init: -> @_super.apply this, arguments + @set 'controller', @get('controller').container.lookup('controller:workers') QueuesView: Em.View.extend templateName: 'queues/list' - controller: Em.ArrayController.create() - - showAll: (event) -> - queue = event.context - queue.showAll() - - didInsertElement: -> - queues = for queue in Travis.QUEUES - Travis.LimitedArray.create - content: Travis.Job.queued(queue.name), limit: 20 - id: "queue_#{queue.name}" - name: queue.display - @set 'controller.content', queues + init: -> @_super.apply this, arguments + @set 'controller', @get('controller').container.lookup('controller:queues') RunningJobsView: Em.View.extend templateName: 'jobs/running' elementId: 'running-jobs' - controller: Travis.RunningJobsController.create() + init: -> + @_super.apply this, arguments + @set 'controller', @get('controller').container.lookup('controller:runningJobs') groupsBinding: 'controller.sortedGroups' jobsBinding: 'controller' - didInsertElement: -> - @get('controller').set 'content', Travis.Job.running() - GroupView: Em.View.extend templateName: 'jobs/running/group' tagName: 'li' @@ -74,7 +48,7 @@ WorkersView: Travis.View.extend - toggleWorkers: (event) -> + toggleWorkers: -> handle = $(event.target).toggleClass('open') if handle.hasClass('open') $('#workers li').addClass('open') @@ -82,10 +56,12 @@ $('#workers li').removeClass('open') WorkersListView: Travis.View.extend - toggle: (event) -> - $(event.target).closest('li').toggleClass('open') + toggle: -> + this.$().find('> li').toggleClass('open') WorkersItemView: Travis.View.extend + classNameBindings: ['worker.state'] + display: (-> name = (@get('worker.name') || '').replace('travis-', '') state = @get('worker.state') @@ -96,6 +72,5 @@ "#{name}: #{state}" ).property('worker.state') - QueueItemView: Travis.View.extend tagName: 'li' diff --git a/assets/scripts/app/views/signin.coffee b/assets/scripts/app/views/signin.coffee index afb75fb6..39e75ef6 100644 --- a/assets/scripts/app/views/signin.coffee +++ b/assets/scripts/app/views/signin.coffee @@ -3,5 +3,5 @@ templateName: 'auth/signin' signingIn: (-> - Travis.app.get('authState') == 'signing-in' - ).property('Travis.app.authState') + Travis.get('authState') == 'signing-in' + ).property('Travis.authState') diff --git a/assets/scripts/app/views/top.coffee b/assets/scripts/app/views/top.coffee index a3049dc7..bb674845 100644 --- a/assets/scripts/app/views/top.coffee +++ b/assets/scripts/app/views/top.coffee @@ -25,9 +25,9 @@ classProfile: (-> classes = ['profile menu'] classes.push('active') if @get('tab') == 'profile' - classes.push(Travis.app.get('authState')) + classes.push(Travis.get('authState')) classes.join(' ') - ).property('tab', 'Travis.app.authState') + ).property('tab', 'Travis.authState') showProfile: -> $('#top .profile ul').show() diff --git a/assets/scripts/data/sponsors.coffee b/assets/scripts/data/sponsors.coffee index 032a3e86..bb97bf07 100644 --- a/assets/scripts/data/sponsors.coffee +++ b/assets/scripts/data/sponsors.coffee @@ -1,48 +1,49 @@ @Travis.SPONSORS = [ - { type: 'platinum', url: "http://www.wooga.com", image: "wooga-205x130.png" } - { type: 'platinum', url: "http://bendyworks.com", image: "bendyworks-205x130.png" } - { type: 'platinum', url: "http://cloudcontrol.com", image: "cloudcontrol-205x130.png" } - { type: 'platinum', url: "http://xing.de", image: "xing-205x130.png" } + { id: '1', type: 'platinum', url: "http://www.wooga.com", image: "wooga-205x130.png" } + { id: '2', type: 'platinum', url: "http://bendyworks.com", image: "bendyworks-205x130.png" } + { id: '3', type: 'platinum', url: "http://cloudcontrol.com", image: "cloudcontrol-205x130.png" } + { id: '4', type: 'platinum', url: "http://xing.de", image: "xing-205x130.png" } - { type: 'gold', url: "http://heroku.com", image: "heroku-205x60.png" } - { type: 'gold', url: "http://soundcloud.com", image: "soundcloud-205x60.png" } - { type: 'gold', url: "http://nedap.com", image: "nedap-205x60.png" } - { type: 'gold', url: "http://mongohq.com", image: "mongohq-205x60.png" } - { type: 'gold', url: "http://zweitag.de", image: "zweitag-205x60.png" } - { type: 'gold', url: "http://kanbanery.com", image: "kanbanery-205x60.png" } - { type: 'gold', url: "http://ticketevolution.com", image: "ticketevolution-205x60.jpg" } - { type: 'gold', url: "http://plan.io/travis", image: "planio-205x60.png" } + { id: '5', type: 'gold', url: "http://heroku.com", image: "heroku-205x60.png" } + { id: '6', type: 'gold', url: "http://soundcloud.com", image: "soundcloud-205x60.png" } + { id: '7', type: 'gold', url: "http://nedap.com", image: "nedap-205x60.png" } + { id: '8', type: 'gold', url: "http://mongohq.com", image: "mongohq-205x60.png" } + { id: '9', type: 'gold', url: "http://zweitag.de", image: "zweitag-205x60.png" } + { id: '10', type: 'gold', url: "http://kanbanery.com", image: "kanbanery-205x60.png" } + { id: '11', type: 'gold', url: "http://ticketevolution.com", image: "ticketevolution-205x60.jpg" } + { id: '12', type: 'gold', url: "http://plan.io/travis", image: "planio-205x60.png" } - { type: 'silver', link: "Cobot: The one tool to run your coworking space" } - { type: 'silver', link: "JumpstartLab: We build developers" } - { type: 'silver', link: "Evil Martians: Agile Ruby on Rails development" } - { type: 'silver', link: "Zendesk: Love your helpdesk" } - { type: 'silver', link: "Stripe: Payments for developers" } - { type: 'silver', link: "Basho: We make Riak!" } - { type: 'silver', link: "Relevance: We deliver software solutions" } - { type: 'silver', link: "Mindmatters: Software für Menschen" } - { type: 'silver', link: "Amen: The best and worst of everything" } - { type: 'silver', link: "Site5: Premium Web Hosting Solutions" } - { type: 'silver', link: "Crowd Interactive: Leading Rails consultancy in Mexico" } - { type: 'silver', link: "Atomic Object: Work with really smart people" } - { type: 'silver', link: "Codeminer: smart services for your startup" } - { type: 'silver', link: "Cloudant: grow into your data layer, not out of it" } - { type: 'silver', link: "Gidsy: Explore, organize & book unique things to do!" } - { type: 'silver', link: "5apps: Package & deploy HTML5 apps automatically" } - { type: 'silver', link: "Meltmedia: We are Interactive Superheroes" } - { type: 'silver', link: "Fingertips offers design and development services" } - { type: 'silver', link: "Engine Yard: Build epic apps, let us handle the rest" } - { type: 'silver', link: "Malwarebytes: Defeat Malware once and for all." } - { type: 'silver', link: "Readmill: The best reading app on the iPad." } - { type: 'silver', link: "Medidata: clinical tech improving quality of life" } - { type: 'silver', link: "ESM: Japan's best agile Ruby/Rails consultancy" } - { type: 'silver', link: "Twitter: instantly connects people everywhere" } - { type: 'silver', link: "AGiLE ANiMAL: we <3 Travis CI." } - { type: 'silver', link: "Tupalo: Discover, review & share local businesses." } - { type: 'silver', link: "Pivotal Labs"} - { type: 'silver', link: "Fiksu"} - { type: 'silver', link: "Sauce Labs"} - { type: 'silver', link: "Mogotest: Never be embarrassed by a visually broken site again."} + { id: '13', type: 'silver', link: "Cobot: The one tool to run your coworking space" } + { id: '14', type: 'silver', link: "JumpstartLab: We build developers" } + { id: '15', type: 'silver', link: "Evil Martians: Agile Ruby on Rails development" } + { id: '16', type: 'silver', link: "Zendesk: Love your helpdesk" } + { id: '17', type: 'silver', link: "Stripe: Payments for developers" } + { id: '18', type: 'silver', link: "Basho: We make Riak!" } + { id: '19', type: 'silver', link: "Relevance: We deliver software solutions" } + { id: '20', type: 'silver', link: "Mindmatters: Software für Menschen" } + { id: '21', type: 'silver', link: "Amen: The best and worst of everything" } + { id: '22', type: 'silver', link: "Site5: Premium Web Hosting Solutions" } + { id: '23', type: 'silver', link: "Crowd Interactive: Leading Rails consultancy in Mexico" } + { id: '24', type: 'silver', link: "Atomic Object: Work with really smart people" } + { id: '25', type: 'silver', link: "Codeminer: smart services for your startup" } + { id: '26', type: 'silver', link: "Cloudant: grow into your data layer, not out of it" } + { id: '27', type: 'silver', link: "Gidsy: Explore, organize & book unique things to do!" } + { id: '28', type: 'silver', link: "5apps: Package & deploy HTML5 apps automatically" } + { id: '29', type: 'silver', link: "Meltmedia: We are Interactive Superheroes" } + { id: '30', type: 'silver', link: "Fingertips offers design and development services" } + { id: '31', type: 'silver', link: "Engine Yard: Build epic apps, let us handle the rest" } + { id: '32', type: 'silver', link: "Malwarebytes: Defeat Malware once and for all." } + { id: '33', type: 'silver', link: "Readmill: The best reading app on the iPad." } + { id: '34', type: 'silver', link: "Medidata: clinical tech improving quality of life" } + { id: '35', type: 'silver', link: "ESM: Japan's best agile Ruby/Rails consultancy" } + { id: '36', type: 'silver', link: "Twitter: instantly connects people everywhere" } + { id: '37', type: 'silver', link: "AGiLE ANiMAL: we <3 Travis CI." } + { id: '38', type: 'silver', link: "Tupalo: Discover, review & share local businesses." } + { id: '39', type: 'silver', link: "Pivotal Labs"} + { id: '40', type: 'silver', link: "Fiksu"} + { id: '41', type: 'silver', link: "Sauce Labs"} + { id: '42', type: 'silver', link: "Mogotest: Never be embarrassed by a visually broken site again."} + { id: '43', type: 'silver', link: "BusyConf: Conferences and Events Made Easy"} ] @Travis.WORKERS = { diff --git a/assets/scripts/lib/travis/ajax.coffee b/assets/scripts/lib/travis/ajax.coffee index 5af00cfd..0a694674 100644 --- a/assets/scripts/lib/travis/ajax.coffee +++ b/assets/scripts/lib/travis/ajax.coffee @@ -1,6 +1,6 @@ jQuery.support.cors = true -@Travis.ajax = Em.Object.create +Travis.ajax = Em.Object.create DEFAULT_OPTIONS: accepts: json: 'application/vnd.travis-ci.2+json' @@ -33,14 +33,14 @@ jQuery.support.cors = true success = options.success || (->) options.success = (data) => - Travis.app.router.flashController.loadFlashes(data.flash) if Travis.app?.router && data.flash - delete data.flash + Travis.lookup('controller:flash').loadFlashes(data.flash) if data?.flash + delete data.flash if data? success.apply(this, arguments) error = options.error || (->) options.error = (data) => - Travis.app.router.flashController.pushObject(data.flash) if data.flash - delete data.flash + Travis.lookup('controller:flash').pushObject(data.flash) if data?.flash + delete data.flash if data? error.apply(this, arguments) $.ajax($.extend(options, Travis.ajax.DEFAULT_OPTIONS)) diff --git a/assets/scripts/lib/travis/chunk_buffer.coffee b/assets/scripts/lib/travis/chunk_buffer.coffee new file mode 100644 index 00000000..f2e6d8fc --- /dev/null +++ b/assets/scripts/lib/travis/chunk_buffer.coffee @@ -0,0 +1,92 @@ +get = Ember.get + +Travis.ChunkBuffer = Em.ArrayProxy.extend + timeout: 5000 + checkTimeoutFrequency: 1000 + start: 1 + next: 1 + + init: -> + @_super.apply this, arguments + + @lastInsert = 0 + + @set('next', @get('start')) + + @checkTimeout() + + if @get('content.length') + @get('queue.content').pushObjects @get('content').toArray() + + arrangedContent: (-> + [] + ).property('content') + + addObject: (obj) -> + @get('content').pushObject(obj) + + removeObject: (obj) -> + @get('content').removeObject(obj) + + replaceContent: (idx, amt, objects) -> + @get('content').replace(idx, amt, objects) + + queue: (-> + Em.ArrayProxy.extend(Em.SortableMixin, { + content: [] + sortProperties: ['number'] + sortAscending: true + }).create() + ).property() + + contentArrayDidChange: (array, index, removedCount, addedCount) -> + @_super.apply this, arguments + + if addedCount + queue = @get('queue') + addedObjects = array.slice(index, index + addedCount) + console.log 'Added log parts with numbers:', addedObjects.map( (element) -> get(element, 'number') )+'', 'current', @get('next') + queue.pushObjects addedObjects + @check() + @inserted() + + check: -> + queue = @get('queue') + next = @get('next') + + arrangedContent = @get('arrangedContent') + toPush = [] + + while queue.get('firstObject.number') <= next + element = queue.shiftObject() + if get(element, 'number') == next + toPush.pushObject get(element, 'content') + next += 1 + + if toPush.length + arrangedContent.pushObjects toPush + + @set('next', next) + + inserted: -> + now = @now() + @lastInsert = now + + checkTimeout: -> + now = @now() + if now - @lastInsert > @get('timeout') + @giveUpOnMissingParts() + @set 'runLaterId', Ember.run.later(this, @checkTimeout, @get('checkTimeoutFrequency')) + + willDestroy: -> + Ember.run.cancel @get('runLaterId') + @_super.apply this, arguments + + now: -> + (new Date()).getTime() + + giveUpOnMissingParts: -> + if number = @get('queue.firstObject.number') + console.log 'Giving up on missing parts in the buffer, switching to:', number + @set('next', number) + @check() diff --git a/assets/scripts/lib/travis/expandable_record_array.coffee b/assets/scripts/lib/travis/expandable_record_array.coffee index 268f766e..7153b891 100644 --- a/assets/scripts/lib/travis/expandable_record_array.coffee +++ b/assets/scripts/lib/travis/expandable_record_array.coffee @@ -33,10 +33,9 @@ Travis.ExpandableRecordArray = DS.RecordArray.extend @pushObject object pushObject: (record) -> - ids = @get 'content' - id = record.get 'id' - clientId = record.get 'clientId' + content = @get 'content' + id = record.get 'id' + clientId = record.get 'clientId' + reference = @get('store').referenceForClientId(clientId) - return if ids.contains clientId - - ids.pushObject clientId + @addReference reference diff --git a/assets/scripts/lib/travis/limited_array.coffee b/assets/scripts/lib/travis/limited_array.coffee index b9f1a6d8..c6baaddc 100644 --- a/assets/scripts/lib/travis/limited_array.coffee +++ b/assets/scripts/lib/travis/limited_array.coffee @@ -54,7 +54,7 @@ Travis.LimitedArray = Em.ArrayProxy.extend if addedCount if index < limit addedObjects = array.slice(index, index + addedCount) - @replaceContent(index, 0, addedObjects) + @get('arrangedContent').replace(index, 0, addedObjects) @balanceArray() diff --git a/assets/scripts/lib/travis/line_number_parser.coffee b/assets/scripts/lib/travis/line_number_parser.coffee new file mode 100644 index 00000000..d77020fe --- /dev/null +++ b/assets/scripts/lib/travis/line_number_parser.coffee @@ -0,0 +1,5 @@ +Travis.LineNumberParser = Ember.Mixin.create + fetchLineNumber: -> + url = @container.lookup('router:main').get('url') + if match = url.match(/#L(\d+)$/) + match[1] diff --git a/assets/scripts/lib/travis/location.coffee b/assets/scripts/lib/travis/location.coffee index b49de069..034fd83f 100644 --- a/assets/scripts/lib/travis/location.coffee +++ b/assets/scripts/lib/travis/location.coffee @@ -1,16 +1,4 @@ Travis.Location = Ember.HistoryLocation.extend - onUpdateURL: (callback) -> - guid = Ember.guidFor(this) - - Ember.$(window).bind 'popstate.ember-location-'+guid, (e) -> - callback(location.pathname + location.hash) - getURL: -> location = @get('location') location.pathname + location.hash - - initState: -> - @replaceState(@getURL()); - Ember.set(this, 'history', window.history) - -Ember.Location.implementations['travis'] = Travis.Location diff --git a/assets/scripts/lib/travis/model.coffee b/assets/scripts/lib/travis/model.coffee index dada8467..4587033e 100644 --- a/assets/scripts/lib/travis/model.coffee +++ b/assets/scripts/lib/travis/model.coffee @@ -1,15 +1,24 @@ @Travis.Model = DS.Model.extend - primaryKey: 'id' - id: DS.attr('number') - init: -> @loadedAttributes = [] @_super.apply this, arguments - refresh: -> - if id = @get('id') - store = @get('store') - store.adapter.find store, @constructor, id + getAttr: (key, options) -> + @needsCompletionCheck(key) + @_super.apply this, arguments + + getBelongsTo: (key, type, meta) -> + @needsCompletionCheck(key) + @_super.apply this, arguments + + getHasMany: (key, type, meta) -> + @needsCompletionCheck(key) + @_super.apply this, arguments + + needsCompletionCheck: (key) -> + if key && (@constructor.isAttribute(key) || @constructor.isRelationship(key)) && + @get('incomplete') && !@isAttributeLoaded(key) + @loadTheRest(key) update: (attrs) -> $.each attrs, (key, value) => @@ -17,14 +26,7 @@ this isAttributeLoaded: (name) -> - key = null - if meta = Ember.get(this.constructor, 'attributes').get(name) - key = meta.key(this.constructor) - else if meta = Ember.get(this.constructor, 'associationsByName').get(name) - key = meta.options.key || @get('namingConvention').foreignKey(name) - - if key - @get('store').isDataLoadedFor(this.constructor, @get('clientId'), key) + @get('store').isDataLoadedFor(this.constructor, @get('clientId'), name) isComplete: (-> if @get 'incomplete' @@ -40,33 +42,32 @@ # undefined key return if !key || key == 'undefined' - message = "Load missing fields for #{@constructor.toString()} because of missing key '#{key}', cid: #{@get('clientId')}" + message = "Load missing fields for #{@constructor.toString()} because of missing key '#{key}', cid: #{@get('clientId')}, id: #{@get('id')}" if @constructor.isAttribute('state') && key != 'state' message += ", in state: #{@get('state')}" console.log message return if @get('isCompleting') @set 'isCompleting', true - @refresh() + unless @get('stateManager.currentState.path').match /^rootState.loaded.materializing/ + @reload() + @set 'incomplete', false select: -> @constructor.select(@get('id')) - loadedAsIncomplete: () -> - @set 'incomplete', true - @Travis.Model.reopenClass find: -> if arguments.length == 0 - Travis.app.store.findAll(this) + Travis.store.findAll(this) else @_super.apply(this, arguments) filter: (callback) -> - Travis.app.store.filter(this, callback) + Travis.store.filter(this, callback) load: (attrs) -> - Travis.app.store.load(this, attrs) + Travis.store.load(this, attrs) select: (id) -> @find().forEach (record) -> @@ -86,8 +87,18 @@ name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1) pluralName: -> - Travis.app.store.adapter.pluralize(@singularName()) + Travis.store.adapter.pluralize(@singularName()) isAttribute: (name) -> - Ember.get(this, 'attributes').has(name) || - Ember.get(this, 'associationsByName').has(name) + Ember.get(this, 'attributes').has(name) + + isRelationship: (name) -> + Ember.get(this, 'relationshipsByName').has(name) + + isHasManyRelationship: (name) -> + if relationship = Ember.get(this, 'relationshipsByName').get(name) + relationship.kind == 'hasMany' + + isBelongsToRelationship: (name) -> + if relationship = Ember.get(this, 'relationshipsByName').get(name) + relationship.kind == 'belongsTo' diff --git a/assets/scripts/lib/travis/log.coffee b/assets/scripts/lib/travis/ordered_log.coffee similarity index 99% rename from assets/scripts/lib/travis/log.coffee rename to assets/scripts/lib/travis/ordered_log.coffee index e4d5b88a..65c33fcd 100644 --- a/assets/scripts/lib/travis/log.coffee +++ b/assets/scripts/lib/travis/ordered_log.coffee @@ -5,7 +5,7 @@ FOLDS = [ Em.Object.create(name: 'bundle', startPattern: /^\$ bundle install/, endPattern: /^(<\/span>)?\$/) ] -@Travis.Log = Em.Object.extend +@Travis.OrderedLog = Em.Object.extend init: -> @set 'folds', [] @set 'line', 1 diff --git a/assets/scripts/lib/travis/ticker.coffee b/assets/scripts/lib/travis/ticker.coffee index b85506d0..81f14517 100644 --- a/assets/scripts/lib/travis/ticker.coffee +++ b/assets/scripts/lib/travis/ticker.coffee @@ -11,6 +11,4 @@ @schedule() schedule: -> - Ember.run.later((=> @tick()), @get('interval') || Travis.app.TICK_INTERVAL) - - + Ember.run.later((=> @tick()), @get('interval') || Travis.TICK_INTERVAL) diff --git a/assets/scripts/spec/build_spec.coffee b/assets/scripts/spec/build_spec.coffee index d650cc58..8c42674c 100644 --- a/assets/scripts/spec/build_spec.coffee +++ b/assets/scripts/spec/build_spec.coffee @@ -1,13 +1,13 @@ describe 'on the "build" state', -> beforeEach -> - app 'travis-ci/travis-core/builds/1' + app '/travis-ci/travis-core/builds/1' + + console.log 'wait for repos' waitFor reposRendered runs -> + console.log 'wait for build' waitFor buildRendered - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'displays the expected stuff', -> listsRepos [ { slug: 'travis-ci/travis-hub', build: { number: 4, url: '/travis-ci/travis-hub/builds/4', duration: '1 min', finishedAt: '-' } } @@ -52,14 +52,11 @@ describe 'on the "build" state', -> describe 'on the "current" state', -> beforeEach -> - app 'travis-ci/travis-core' + app '/travis-ci/travis-core' waitFor reposRendered runs -> waitFor buildRendered - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'correctly updates values on pusher build:started event', -> payload = build: @@ -72,8 +69,8 @@ describe 'on the "current" state', -> finished_at: '2012-07-02T00:02:55Z' event_type: 'push' result: 1 - commit_message: 'commit message 3' - commit: '1234567' + message: 'commit message 3' + commit: 'foo1234' state: 'started' repository: id: 1 @@ -81,10 +78,19 @@ describe 'on the "current" state', -> last_build_id: 11 Em.run -> - Travis.app.receive 'build:started', payload + Travis.receive 'build:started', payload - waits(100) runs -> displaysSummaryBuildLink '/travis-ci/travis-core/builds/11', '3' + displaysSummary + type: 'build' + id: 11 + repo: 'travis-ci/travis-core' + commit: 'foo1234' + branch: 'master' + compare: '0123456..1234567' + finishedAt: 'less than a minute ago' + duration: '55 sec' + message: 'commit message 3' diff --git a/assets/scripts/spec/builds_spec.coffee b/assets/scripts/spec/builds_spec.coffee index 22124e29..0229547b 100644 --- a/assets/scripts/spec/builds_spec.coffee +++ b/assets/scripts/spec/builds_spec.coffee @@ -1,11 +1,8 @@ describe 'on the "builds" state', -> beforeEach -> - app 'travis-ci/travis-core/builds' + app '/travis-ci/travis-core/builds' waitFor buildsRendered - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'displays the expected stuff', -> listsRepos [ { slug: 'travis-ci/travis-hub', build: { number: 4, url: '/travis-ci/travis-hub/builds/4', duration: '1 min', finishedAt: '-' } } diff --git a/assets/scripts/spec/current_spec.coffee b/assets/scripts/spec/current_spec.coffee index eea24c0b..d3ab3167 100644 --- a/assets/scripts/spec/current_spec.coffee +++ b/assets/scripts/spec/current_spec.coffee @@ -3,9 +3,6 @@ describe 'on the "current" state', -> app 'travis-ci/travis-core' waitFor buildRendered - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'displays the expected stuff', -> listsRepos [ { slug: 'travis-ci/travis-hub', build: { number: 4, url: '/travis-ci/travis-hub/builds/4', duration: '1 min', finishedAt: '-' } } diff --git a/assets/scripts/spec/event_spec.coffee b/assets/scripts/spec/event_spec.coffee index 8b78af65..ddde0844 100644 --- a/assets/scripts/spec/event_spec.coffee +++ b/assets/scripts/spec/event_spec.coffee @@ -1,7 +1,4 @@ describe 'events', -> - afterEach -> - window.history.pushState({}, null, '/spec.html') - describe 'an event adding a repository', -> beforeEach -> app 'travis-ci/travis-core' @@ -24,7 +21,7 @@ describe 'events', -> responseText: payload Em.run -> - Travis.app.receive 'build:started', + Travis.receive 'build:started', build: id: 10 repository: @@ -57,13 +54,12 @@ describe 'events', -> started_at: '2012-07-02T00:02:00Z' finished_at: '2012-07-02T00:02:55Z' event_type: 'push' - result: 1 message: 'commit message 3' commit: '1234567' - state: 'started' + state: 'failed' Em.run -> - Travis.app.receive 'build:started', payload + Travis.receive 'build:started', payload waits(100) runs -> @@ -74,43 +70,9 @@ describe 'events', -> describe 'an event adding a job', -> beforeEach -> app 'travis-ci/travis-core' - waitFor jobsRendered + waitFor jobsRendered, 'jobs should be rendered' runs -> - waitFor queuesRendered - - it 'adds a job to the jobs matrix', -> - payload = - job: - id: 15 - repository_id: 1 - build_id: 1 - commit_id: 1 - log_id: 1 - number: '1.4' - duration: 55 - started_at: '2012-07-02T00:02:00Z' - finished_at: '2012-07-02T00:02:55Z' - config: { rvm: 'jruby' } - - $.mockjax - url: '/jobs/15' - responseTime: 0 - responseText: payload - - Em.run -> - Travis.app.receive 'job:started', - job: - id: 15 - repository_id: 1 - build_id: 1 - commit_id: 1 - - waits(100) - runs -> - listsJob - table: $('#jobs') - row: 3 - item: { id: 15, number: '1.4', repo: 'travis-ci/travis-core', finishedAt: 'less than a minute ago', duration: '55 sec', rvm: 'jruby' } + waitFor queuesRendered, 'queues should be rendered' it 'adds a job to the jobs queue', -> payload = @@ -118,7 +80,7 @@ describe 'events', -> id: 12 repository_id: 1 number: '1.4' - queue: 'builds.common' + queue: 'builds.linux' $.mockjax url: '/jobs/12' @@ -126,24 +88,25 @@ describe 'events', -> responseText: payload Em.run -> - Travis.app.receive 'job:started', + Travis.receive 'job:started', job: id: 12 repository_id: 1 + repository_slug: 'travis-ci/travis-core' number: '1.4' - queue: 'builds.common' + queue: 'builds.linux' state: 'created' - waits(100) + waits(1000) runs -> listsQueuedJob - name: 'common' + name: 'linux' row: 3 item: { number: '1.4', repo: 'travis-ci/travis-core' } it 'updates only keys that are available', -> Em.run -> - Travis.app.receive 'job:started', + Travis.receive 'job:started', job: id: 1 build_id: 1 @@ -174,7 +137,7 @@ describe 'events', -> responseText: payload Em.run -> - Travis.app.receive 'worker:created', + Travis.receive 'worker:created', worker: id: 10 name: 'ruby-3' @@ -194,7 +157,7 @@ describe 'events', -> app '/travis-ci/travis-core' waitFor workersRendered - it 'does not update repository if it\'s already in store', -> + it 'does not update repository if it\'s already in the store', -> payload = worker: id: 1 @@ -208,7 +171,7 @@ describe 'events', -> last_build_number: '999' Em.run -> - Travis.app.receive 'worker:updated', payload + Travis.receive 'worker:updated', payload waits(100) runs -> diff --git a/assets/scripts/spec/index_spec.coffee b/assets/scripts/spec/index_spec.coffee index beaa6903..df96a419 100644 --- a/assets/scripts/spec/index_spec.coffee +++ b/assets/scripts/spec/index_spec.coffee @@ -3,9 +3,6 @@ describe 'on the "index" state', -> app 'travis-ci/travis-core' waitFor buildRendered - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'displays the expected stuff', -> listsRepos [ { slug: 'travis-ci/travis-hub', build: { number: 4, url: '/travis-ci/travis-hub/builds/4', duration: '1 min', finishedAt: '-' } } diff --git a/assets/scripts/spec/job_spec.coffee b/assets/scripts/spec/job_spec.coffee index c0ab3312..ab2f5955 100644 --- a/assets/scripts/spec/job_spec.coffee +++ b/assets/scripts/spec/job_spec.coffee @@ -1,13 +1,16 @@ describe 'on the "job" state', -> beforeEach -> + $.mockjax + url: '/jobs/1/log?cors_hax=true' + responseTime: 0 + responseText: 'log 1' + + app 'travis-ci/travis-core/jobs/1' waitFor jobRendered runs -> waitFor hasText('#tab_build', 'Build #1') - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'displays the expected stuff', -> listsRepos [ { slug: 'travis-ci/travis-hub', build: { number: 4, url: '/travis-ci/travis-hub/builds/4', duration: '1 min', finishedAt: '-' } } @@ -15,26 +18,28 @@ describe 'on the "job" state', -> { slug: 'travis-ci/travis-assets', build: { number: 3, url: '/travis-ci/travis-assets/builds/3', duration: '30 sec', finishedAt: 'a day ago' } } ] - displaysRepository - href: 'http://github.com/travis-ci/travis-core' + waits 100 + runs -> + displaysRepository + href: 'http://github.com/travis-ci/travis-core' - displaysSummary - id: 1 - type: 'job' - repo: 'travis-ci/travis-core' - commit: '1234567' - branch: 'master' - compare: '0123456..1234567' - finishedAt: '3 minutes ago' - duration: '30 sec' - message: 'commit message 1' + displaysSummary + id: 1 + type: 'job' + repo: 'travis-ci/travis-core' + commit: '1234567' + branch: 'master' + compare: '0123456..1234567' + finishedAt: '3 minutes ago' + duration: '30 sec' + message: 'commit message 1' - displaysTabs - current: { href: '/travis-ci/travis-core' } - builds: { href: '/travis-ci/travis-core/builds' } - build: { href: '/travis-ci/travis-core/builds/1' } - job: { href: '/travis-ci/travis-core/jobs/1', active: true } + displaysTabs + current: { href: '/travis-ci/travis-core' } + builds: { href: '/travis-ci/travis-core/builds' } + build: { href: '/travis-ci/travis-core/builds/1' } + job: { href: '/travis-ci/travis-core/jobs/1', active: true } - displaysLog [ - 'log 1' - ] + displaysLog [ + 'log 1' + ] diff --git a/assets/scripts/spec/sidebar_spec.coffee b/assets/scripts/spec/sidebar_spec.coffee index 327fba12..d2c326c6 100644 --- a/assets/scripts/spec/sidebar_spec.coffee +++ b/assets/scripts/spec/sidebar_spec.coffee @@ -5,11 +5,8 @@ describe 'the sidebar', -> runs -> waitFor hasText('#tab_build', 'Build #1') - afterEach -> - window.history.pushState({}, null, '/spec.html') - it 'displays the expected stuff', -> listsQueues [ - { name: 'common', item: { number: '5.1', repo: 'travis-ci/travis-core' } } - { name: 'common', item: { number: '5.2', repo: 'travis-ci/travis-core' } } + { name: 'linux', item: { number: '5.1', repo: 'travis-ci/travis-core' } } + { name: 'linux', item: { number: '5.2', repo: 'travis-ci/travis-core' } } ] diff --git a/assets/scripts/spec/spec_helper.coffee b/assets/scripts/spec/spec_helper.coffee index e49fa538..5e469126 100644 --- a/assets/scripts/spec/spec_helper.coffee +++ b/assets/scripts/spec/spec_helper.coffee @@ -1,55 +1,15 @@ minispade.require 'app' -@reset = -> - Em.run -> - if Travis.app - if Travis.app.store - Travis.app.store.destroy() - Travis.app.destroy() - delete Travis.app - delete Travis.store - - waits(500) # TODO not sure what we need to wait for here - $('#application').remove() - $('body').append( $('') ) - @app = (url) -> - reset() - Em.run -> - Travis.run(rootElement: $('#application')) - waitFor -> Travis.app - # TODO: so much waiting here, I'm sure we can minimize this - runs -> - url = "/#{url}" if url && !url.match(/^\//) - Travis.app.router.route(url) - waits 500 - runs -> - foo = 'bar' + # TODO: this should wait till app is initialized, not some + # arbitrary amount of time + waits(50) + runs -> + Travis.reset() + url = "/#{url}" unless url.match /^\// + Travis.__container__.lookup('router:main').handleURL(url) -_Date = Date -@Date = (date) -> - new _Date(date || '2012-07-02T00:03:00Z') -@Date.UTC = _Date.UTC -# hacks for missing features in webkit -unless Function::bind - Function::bind = (oThis) -> - - # closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable") if typeof this isnt "function" - aArgs = Array::slice.call(arguments, 1) - fToBind = this - fNOP = -> - - fBound = -> - fToBind.apply (if this instanceof fNOP and oThis then this else oThis), aArgs.concat(Array::slice.call(arguments_)) - - fNOP.prototype = @.prototype - fBound.prototype = new fNOP() - fBound - -window.history.state = {} -oldPushState = window.history.pushState -window.history.pushState = (state, title, href) -> - window.history.state = state - oldPushState.apply this, arguments +now = -> new Date('2012-07-02T00:03:00Z') +$.timeago.settings.nowFunction = -> now().getTime() +Travis.currentDate = now diff --git a/assets/scripts/spec/support/conditions.coffee b/assets/scripts/spec/support/conditions.coffee index 5491f98b..67a60462 100644 --- a/assets/scripts/spec/support/conditions.coffee +++ b/assets/scripts/spec/support/conditions.coffee @@ -9,6 +9,6 @@ @buildsRendered = notEmpty('#builds .number') @jobRendered = notEmpty('#summary .number') @jobsRendered = notEmpty('#jobs .number') -@queuesRendered = notEmpty('#queue_common li') +@queuesRendered = notEmpty('#queue_linux li') @workersRendered = notEmpty('.worker') diff --git a/assets/scripts/spec/support/expectations.coffee b/assets/scripts/spec/support/expectations.coffee index 6365fdc7..edf80ddc 100644 --- a/assets/scripts/spec/support/expectations.coffee +++ b/assets/scripts/spec/support/expectations.coffee @@ -41,9 +41,8 @@ expect(element.text()).toEqual data.message @displaysLog = (lines) -> - ix = 0 - log = $.map(lines, (line) -> ix += 1; "#{ix}#{line}").join("\n") - expect($('#log p').text().trim()).toEqual log + log = lines.join() + expect($('#log').text().trim()).toEqual log @listsRepos = (items) -> listsItems('repo', items) diff --git a/assets/scripts/spec/support/helpers.coffee b/assets/scripts/spec/support/helpers.coffee index fb41779e..c8007a71 100644 --- a/assets/scripts/spec/support/helpers.coffee +++ b/assets/scripts/spec/support/helpers.coffee @@ -8,4 +8,3 @@ @waitFor = waitsFor - diff --git a/assets/scripts/spec/support/mocks.coffee b/assets/scripts/spec/support/mocks.coffee index f17d941f..ea256dab 100644 --- a/assets/scripts/spec/support/mocks.coffee +++ b/assets/scripts/spec/support/mocks.coffee @@ -1,45 +1,45 @@ responseTime = 0 repos = [ - { 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, last_build_duration: 30, last_build_started_at: '2012-07-02T00:00:00Z', last_build_finished_at: '2012-07-02T00:00:30Z', description: 'Description of travis-core' }, - { id: 2, owner: 'travis-ci', name: 'travis-assets', slug: 'travis-ci/travis-assets', build_ids: [3], last_build_id: 3, last_build_number: 3, last_build_result: 1, last_build_duration: 30, last_build_started_at: '2012-07-02T00:01:00Z', last_build_finished_at: '2012-07-01T00:01:30Z', description: 'Description of travis-assets'}, - { id: 3, owner: 'travis-ci', name: 'travis-hub', slug: 'travis-ci/travis-hub', build_ids: [4], last_build_id: 4, last_build_number: 4, last_build_result: undefined, last_build_duration: undefined, last_build_started_at: '2012-07-02T00:02:00Z', last_build_finished_at: undefined, description: 'Description of travis-hub'}, + { 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, last_build_duration: 30, last_build_started_at: '2012-07-02T00:00:00Z', last_build_finished_at: '2012-07-02T00:00:30Z', description: 'Description of travis-core' }, + { id: '2', owner: 'travis-ci', name: 'travis-assets', slug: 'travis-ci/travis-assets', build_ids: [3], last_build_id: 3, last_build_number: 3, last_build_result: 1, last_build_duration: 30, last_build_started_at: '2012-07-02T00:01:00Z', last_build_finished_at: '2012-07-01T00:01:30Z', description: 'Description of travis-assets'}, + { id: '3', owner: 'travis-ci', name: 'travis-hub', slug: 'travis-ci/travis-hub', build_ids: [4], last_build_id: 4, last_build_number: 4, last_build_result: undefined, last_build_duration: undefined, last_build_started_at: '2012-07-02T00:02:00Z', last_build_finished_at: undefined, description: 'Description of travis-hub'}, ] builds = [ - { id: 1, repository_id: '1', commit_id: 1, job_ids: [1, 2, 3], number: 1, pull_request: false, config: { rvm: ['rbx', '1.9.3', 'jruby'] }, duration: 30, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:30Z', result: 0 }, - { id: 2, repository_id: '1', commit_id: 2, job_ids: [4], number: 2, pull_request: false, config: { rvm: ['rbx'] } }, - { id: 3, repository_id: '2', commit_id: 3, job_ids: [5], number: 3, pull_request: false, config: { rvm: ['rbx'] }, duration: 30, started_at: '2012-07-02T00:01:00Z', finished_at: '2012-07-01T00:01:30Z', result: 1 }, - { id: 4, repository_id: '3', commit_id: 4, job_ids: [6], number: 4, pull_request: false, config: { rvm: ['rbx'] }, started_at: '2012-07-02T00:02:00Z' }, + { id: '1', repository_id: '1', commit_id: 1, job_ids: [1, 2, 3], number: 1, pull_request: false, config: { rvm: ['rbx', '1.9.3', 'jruby'] }, duration: 30, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:30Z', state: 'passed' }, + { id: '2', repository_id: '1', commit_id: 2, job_ids: [4], number: 2, pull_request: false, config: { rvm: ['rbx'] } }, + { id: '3', repository_id: '2', commit_id: 3, job_ids: [5], number: 3, pull_request: false, config: { rvm: ['rbx'] }, duration: 30, started_at: '2012-07-02T00:01:00Z', finished_at: '2012-07-01T00:01:30Z', state: 'failed' }, + { id: '4', repository_id: '3', commit_id: 4, job_ids: [6], number: 4, pull_request: false, config: { rvm: ['rbx'] }, started_at: '2012-07-02T00:02:00Z' }, ] commits = [ - { id: 1, sha: '1234567', branch: 'master', message: 'commit message 1', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..1234567' }, - { id: 2, sha: '2345678', branch: 'feature', message: 'commit message 2', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..2345678' }, - { id: 3, sha: '3456789', branch: 'master', message: 'commit message 3', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..3456789' }, - { id: 4, sha: '4567890', branch: 'master', message: 'commit message 4', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..4567890' }, + { id: '1', sha: '1234567', branch: 'master', message: 'commit message 1', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..1234567' }, + { id: '2', sha: '2345678', branch: 'feature', message: 'commit message 2', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..2345678' }, + { id: '3', sha: '3456789', branch: 'master', message: 'commit message 3', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..3456789' }, + { id: '4', sha: '4567890', branch: 'master', message: 'commit message 4', author_name: 'author name', author_email: 'author@email.com', committer_name: 'committer name', committer_email: 'committer@email.com', compare_url: 'http://github.com/compare/0123456..4567890' }, ] jobs = [ - { id: 1, repository_id: 1, build_id: 1, commit_id: 1, log_id: 1, number: '1.1', config: { rvm: 'rbx' }, duration: 30, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:30Z', result: 0 } - { id: 2, repository_id: 1, build_id: 1, commit_id: 1, log_id: 2, number: '1.2', config: { rvm: '1.9.3' }, duration: 40, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:40Z', result: 1 } - { id: 3, repository_id: 1, build_id: 1, commit_id: 1, log_id: 3, number: '1.3', config: { rvm: 'jruby' }, allow_failure: true } - { id: 4, repository_id: 1, build_id: 2, commit_id: 2, log_id: 4, number: '2.1', config: { rvm: 'rbx' } } - { id: 5, repository_id: 2, build_id: 3, commit_id: 3, log_id: 5, number: '3.1', config: { rvm: 'rbx' }, duration: 30, started_at: '2012-07-02T00:01:00Z', finished_at: '2012-07-02T00:01:30Z', result: 1 } - { id: 6, repository_id: 3, build_id: 4, commit_id: 4, log_id: 6, number: '4.1', config: { rvm: 'rbx' }, started_at: '2012-07-02T00:02:00Z' } - { id: 7, repository_id: 1, build_id: 5, commit_id: 5, log_id: 7, number: '5.1', config: { rvm: 'rbx' }, state: 'created', queue: 'builds.common' } - { id: 8, repository_id: 1, build_id: 5, commit_id: 5, log_id: 8, number: '5.2', config: { rvm: 'rbx' }, state: 'created', queue: 'builds.common' } + { id: '1', repository_id: 1, repository_slug: 'travis-ci/travis-core', build_id: 1, commit_id: 1, log_id: 1, number: '1.1', config: { rvm: 'rbx' }, duration: 30, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:30Z', state: 'passed' } + { id: '2', repository_id: 1, repository_slug: 'travis-ci/travis-core', build_id: 1, commit_id: 1, log_id: 2, number: '1.2', config: { rvm: '1.9.3' }, duration: 40, started_at: '2012-07-02T00:00:00Z', finished_at: '2012-07-02T00:00:40Z', state: 'failed' } + { id: '3', repository_id: 1, repository_slug: 'travis-ci/travis-core', build_id: 1, commit_id: 1, log_id: 3, number: '1.3', config: { rvm: 'jruby' }, allow_failure: true } + { id: '4', repository_id: 1, repository_slug: 'travis-ci/travis-core', build_id: 2, commit_id: 2, log_id: 4, number: '2.1', config: { rvm: 'rbx' } } + { id: '5', repository_id: 2, repository_slug: 'travis-ci/travis-assets', build_id: 3, commit_id: 3, log_id: 5, number: '3.1', config: { rvm: 'rbx' }, duration: 30, started_at: '2012-07-02T00:01:00Z', finished_at: '2012-07-02T00:01:30Z', state: 'failed' } + { id: '6', repository_id: 3, repository_slug: 'travis-ci/travis-hub', build_id: 4, commit_id: 4, log_id: 6, number: '4.1', config: { rvm: 'rbx' }, started_at: '2012-07-02T00:02:00Z' } + { id: '7', repository_id: 1, repository_slug: 'travis-ci/travis-core', build_id: 5, commit_id: 5, log_id: 7, number: '5.1', config: { rvm: 'rbx' }, state: 'created', queue: 'builds.linux' } + { id: '8', repository_id: 1, repository_slug: 'travis-ci/travis-core', build_id: 5, commit_id: 5, log_id: 8, number: '5.2', config: { rvm: 'rbx' }, state: 'created', queue: 'builds.linux' } ] artifacts = [ - { id: 1, body: 'log 1' } - { id: 2, body: 'log 2' } - { id: 3, body: 'log 3' } - { id: 4, body: 'log 4' } - { id: 5, body: 'log 5' } - { id: 6, body: 'log 6' } - { id: 7, body: 'log 7' } - { id: 8, body: 'log 8' } + { id: '1', body: 'log 1' } + { id: '2', body: 'log 2' } + { id: '3', body: 'log 3' } + { id: '4', body: 'log 4' } + { id: '5', body: 'log 5' } + { id: '6', body: 'log 6' } + { id: '7', body: 'log 7' } + { id: '8', body: 'log 8' } ] branches = [ @@ -49,8 +49,8 @@ branches = [ ] workers = [ - { id: 1, name: 'ruby-1', host: 'worker.travis-ci.org', state: 'ready' } - { id: 2, name: 'ruby-2', host: 'worker.travis-ci.org', state: 'ready' } + { id: '1', name: 'ruby-1', host: 'worker.travis-ci.org', state: 'ready' } + { id: '2', name: 'ruby-2', host: 'worker.travis-ci.org', state: 'ready' } ] hooks = [ diff --git a/assets/scripts/spec/unit/artifact_spec.coffee b/assets/scripts/spec/unit/artifact_spec.coffee deleted file mode 100644 index 4458152e..00000000 --- a/assets/scripts/spec/unit/artifact_spec.coffee +++ /dev/null @@ -1,36 +0,0 @@ -store = null -record = null - -describe 'Travis.Artifact', -> - beforeEach -> - store = Travis.Store.create() - - afterEach -> - store.destroy() - - describe 'with part of the body loaded', -> - beforeEach => - store.load Travis.Artifact, 1, { id: 1, body: 'first\nsecond\n' } - record = store.find(Travis.Artifact, 1) - - it 'packs the existing part of the body to parts', -> - expect( record.get('parts').toArray() ).toEqual( ['first\nsecond\n'] ) - - it 'adds new chunks of log to parts', -> - record.append('third\n') - expect( record.get('parts').toArray() ).toEqual( ['first\nsecond\n', 'third\n'] ) - - it 'properly handles array observers', -> - called = 0 - observer = { - arrayDidChange: -> called += 1 - arrayWillChange: -> called += 1 - } - - record.get('parts').addArrayObserver observer, - willChange: 'arrayWillChange' - didChange: 'arrayDidChange' - - record.append('something') - - expect(called).toEqual 2 diff --git a/assets/scripts/spec/unit/build_spec.coffee b/assets/scripts/spec/unit/build_spec.coffee index 2a81e1d1..f7374224 100644 --- a/assets/scripts/spec/unit/build_spec.coffee +++ b/assets/scripts/spec/unit/build_spec.coffee @@ -10,7 +10,8 @@ describe 'Travis.Build', -> describe 'incomplete attributes', -> beforeEach -> - record = store.loadIncomplete Travis.Build, { id: 1, state: 'started' } + store.loadIncomplete Travis.Build, { id: 1, state: 'started' } + record = store.find Travis.Build, 1 it 'does not load record on duration, finishedAt and result if job is not in finished state', -> record.get('_duration') @@ -19,12 +20,13 @@ describe 'Travis.Build', -> waits 50 runs -> - expect( record.get('complete') ).toBeFalsy() + expect( record.get('incomplete') ).toBeTruthy() it 'loads the rest of the record if it\'s in finished state', -> - record = store.loadIncomplete Travis.Build, { id: 1, state: 'finished' } + store.loadIncomplete Travis.Build, { id: 1, state: 'passed' } + record = store.find Travis.Build, 1 record.get('finishedAt') waits 50 runs -> - expect( record.get('complete') ).toBeTruthy() + expect( record.get('incomplete') ).toBeFalsy() diff --git a/assets/scripts/spec/unit/chunk_buffer_spec.coffee b/assets/scripts/spec/unit/chunk_buffer_spec.coffee new file mode 100644 index 00000000..399dcbb3 --- /dev/null +++ b/assets/scripts/spec/unit/chunk_buffer_spec.coffee @@ -0,0 +1,93 @@ +createChunk = (number, content) -> + Em.Object.create(number: number, content: content) + +describe 'Travis.ChunkBuffer', -> + it 'waits for parts to be in order before revealing them', -> + buffer = Travis.ChunkBuffer.create(content: []) + + buffer.pushObject createChunk(3, "baz") + buffer.pushObject createChunk(2, "bar") + + expect(buffer.get('length')).toEqual(0) + + buffer.pushObject createChunk(1, "foo") + + expect(buffer.get('length')).toEqual(3) + + expect(buffer.toArray()).toEqual(['foo', 'bar', 'baz']) + + it 'ignores a part if it fails to be delivered within timeout', -> + buffer = Travis.ChunkBuffer.create(content: [], timeout: 20, checkTimeoutFrequency: 5) + + buffer.pushObject createChunk(3, "baz") + + expect(buffer.get('length')).toEqual(0) + + buffer.pushObject createChunk(1, "foo") + + expect(buffer.get('length')).toEqual(1) + + waits 40 + runs -> + expect(buffer.get('length')).toEqual(2) + expect(buffer.toArray()).toEqual(['foo', 'baz']) + + buffer.destroy() + + it 'works correctly when parts are passed as content', -> + content = [createChunk(2, 'bar')] + + buffer = Travis.ChunkBuffer.create(content: content) + + expect(buffer.get('length')).toEqual(0) + + buffer.pushObject createChunk(1, "foo") + + expect(buffer.get('length')).toEqual(2) + expect(buffer.toArray()).toEqual(['foo', 'bar']) + + it 'works correctly when parts duplicated', -> + buffer = Travis.ChunkBuffer.create(content: []) + + buffer.pushObject createChunk(1, "foo") + buffer.pushObject createChunk(2, "bar") + buffer.pushObject createChunk(3, "baz") + + buffer.pushObject createChunk(2, "bar") + buffer.pushObject createChunk(3, "baz") + buffer.pushObject createChunk(4, "qux") + + expect(buffer.get('length')).toEqual(4) + expect(buffer.toArray()).toEqual(['foo', 'bar', 'baz', 'qux']) + + it 'fires array observers properly', -> + changes = [] + buffer = Travis.ChunkBuffer.create(content: []) + + observer = Em.Object.extend( + init: -> + @_super.apply this, arguments + + @get('content').addArrayObserver this, + willChange: 'arrayWillChange', + didChange: 'arrayDidChange' + + arrayWillChange: (->) + arrayDidChange: (array, index, removedCount, addedCount) -> + changes.pushObject([index, addedCount]) + ).create(content: buffer) + + buffer.pushObject createChunk(2, "baz") + + expect(buffer.get('length')).toEqual(0) + expect(changes.length).toEqual(0) + + buffer.pushObject createChunk(1, "foo") + + expect(buffer.get('length')).toEqual(2) + expect(changes.length).toEqual(1) + expect(changes[0]).toEqual([0, 2]) + + it 'sets next to start if start is given at init', -> + buffer = Travis.ChunkBuffer.create(content: [], start: 5) + expect(buffer.get('next')).toEqual(5) diff --git a/assets/scripts/spec/unit/incomplete_spec.coffee b/assets/scripts/spec/unit/incomplete_spec.coffee index 676752c3..adf501c3 100644 --- a/assets/scripts/spec/unit/incomplete_spec.coffee +++ b/assets/scripts/spec/unit/incomplete_spec.coffee @@ -1,29 +1,60 @@ -Travis.Foo = Travis.Model.extend - name: DS.attr('string') - description: DS.attr('string') - lastName: DS.attr('string') - - bar: DS.belongsTo('Travis.Bar') - niceBar: DS.belongsTo('Travis.Bar') - veryNiceBar: DS.belongsTo('Travis.Bar', key: 'very_nice_bar_indeed_id') - -Travis.Bar = Travis.Model.extend() - record = null store = null +adapterClass = null -$.mockjax - url: '/foos/1' - responseTime: 10 - responseText: { foo: { id: 1, name: 'foo', description: 'bar' } } - -describe 'Travis.Model', -> +describe 'Travis.Model - incomplete', -> beforeEach -> - store = Travis.Store.create() + $.mockjax + url: '/foos/1' + responseTime: 1 + responseText: { foo: { id: 1, name: 'foo', description: 'bar' } } + + Travis.Foo = Travis.Model.extend + name: DS.attr('string') + description: DS.attr('string') + lastName: DS.attr('string') + + bar: DS.belongsTo('Travis.Bar') + niceBar: DS.belongsTo('Travis.Bar') + veryNiceBar: DS.belongsTo('Travis.Bar') + + Travis.Foo.toString = -> 'Travis.Foo' + + Travis.Bar = Travis.Model.extend + name: DS.attr('string') + foos: DS.hasMany('Travis.Foo') + + Travis.Bar.toString = -> 'Travis.Bar' + + adapterClass = Travis.RestAdapter.extend() + adapterClass.map 'Travis.Foo', + veryNiceBar: { key: 'very_nice_bar_indeed_id' } + niceBar: { key: 'nice_bar_id' } + + store = Travis.Store.create + adapter: adapterClass.create() afterEach -> + delete Travis.Foo + delete Travis.Bar store.destroy() + it 'allows to merge many times', -> + store.load(Travis.Bar, { id: '1', foo_ids: ['1', '2'] }, { id: '1' }) + store.load(Travis.Foo, { id: '1', bar_id: '1' }, { id: '1' }) + store.load(Travis.Foo, { id: '2', bar_id: '1' }, { id: '2' }) + + record = store.find(Travis.Bar, 1) + store.find(Travis.Foo, 1) + store.find(Travis.Foo, 2) + + record.get('foos') + store.loadIncomplete(Travis.Bar, id: 1, name: 'foo') + store.loadIncomplete(Travis.Bar, id: 1, name: 'bar') + + expect( record.get('foos.length') ).toEqual(2) + expect( record.get('name') ).toEqual('bar') + describe 'with incomplete record with loaded associations', -> beforeEach -> attrs = { @@ -32,43 +63,45 @@ describe 'Travis.Model', -> nice_bar_id: 3 very_nice_bar_indeed_id: 4 } - record = store.loadIncomplete(Travis.Foo, attrs) + store.loadIncomplete(Travis.Foo, attrs) + record = store.find Travis.Foo, 1 store.load(Travis.Bar, id: 2) store.load(Travis.Bar, id: 3) store.load(Travis.Bar, id: 4) it 'does not load record on association access', -> - expect( record.get('bar.id') ).toEqual 2 - expect( record.get('niceBar.id') ).toEqual 3 - expect( record.get('veryNiceBar.id') ).toEqual 4 + expect( record.get('bar.id') ).toEqual '2' + expect( record.get('niceBar.id') ).toEqual '3' + expect( record.get('veryNiceBar.id') ).toEqual '4' waits 50 runs -> - expect( record.get('complete') ).toBeFalsy() + expect( record.get('incomplete') ).toBeTruthy() describe 'with incomplete record without loaded associations', -> beforeEach -> attrs = { id: 1 } - record = store.loadIncomplete(Travis.Foo, attrs) + store.loadIncomplete(Travis.Foo, attrs) + record = store.find Travis.Foo, 1 it 'loads record based on regular association key', -> record.get('bar') waits 50 runs -> - expect( record.get('complete') ).toBeTruthy() + expect( record.get('incomplete') ).toBeFalsy() it 'loads record based on camel case association key', -> record.get('niceBar') waits 50 runs -> - expect( record.get('complete') ).toBeTruthy() + expect( record.get('incomplete') ).toBeFalsy() it 'loads record based on ssociation with explicit key', -> record.get('veryNiceBar') waits 50 runs -> - expect( record.get('complete') ).toBeTruthy() + expect( record.get('incomplete') ).toBeFalsy() describe 'with incomplete record', -> beforeEach -> @@ -77,7 +110,8 @@ describe 'Travis.Model', -> name: 'foo' last_name: 'foobar' } - record = store.loadIncomplete(Travis.Foo, attrs) + store.loadIncomplete(Travis.Foo, attrs) + record = store.find Travis.Foo, 1 it 'shows if attribute is loaded', -> expect( record.isAttributeLoaded('name') ).toBeTruthy() @@ -87,7 +121,7 @@ describe 'Travis.Model', -> expect( record.get('name') ).toEqual 'foo' waits 50 runs -> - expect( record.get('complete') ).toBeFalsy() + expect( record.get('incomplete') ).toBeTruthy() it 'loads missing data if getPath is used', -> other = Em.Object.create(record: record) @@ -104,7 +138,6 @@ describe 'Travis.Model', -> waits 50 runs -> expect( record.get('description') ).toEqual 'bar' - expect( record.get('complete') ).toBeTruthy() expect( record.get('isComplete') ).toBeTruthy() it 'does not set incomplete on the record twice', -> @@ -124,28 +157,29 @@ describe 'Travis.Model', -> expect( record.get('lastName') ).toEqual 'foobar' waits 50 runs -> - expect( record.get('complete') ).toBeFalsy() + expect( record.get('incomplete') ).toBeTruthy() it 'adds takes into account additional data loaded as incomplete', -> - record = store.loadIncomplete(Travis.Foo, { id: 1, description: 'baz' }) + store.loadIncomplete(Travis.Foo, { id: 1, description: 'baz' }) + record = store.find Travis.Foo, 1 expect( record.get('description') ).toEqual 'baz' waits 50 runs -> - expect( record.get('complete') ).toBeFalsy() + expect( record.get('incomplete') ).toBeTruthy() describe 'with complete record', -> beforeEach -> - id = 5 + id = '5' attrs = { id: id name: 'foo' } - store.load(Travis.Foo, id, attrs) - record = Travis.Foo.find(id) + store.load(Travis.Foo, attrs, { id: attrs.id }) + record = store.find(Travis.Foo, id) it 'is marked as completed', -> - expect( record.get('complete') ).toBeTruthy() + expect( record.get('incomplete') ).toBeFalsy() it 'allows to get regular attribute', -> expect( record.get('name') ).toEqual 'foo' diff --git a/assets/scripts/spec/unit/job_spec.coffee b/assets/scripts/spec/unit/job_spec.coffee index 2550058b..42efff80 100644 --- a/assets/scripts/spec/unit/job_spec.coffee +++ b/assets/scripts/spec/unit/job_spec.coffee @@ -10,7 +10,8 @@ describe 'Travis.Job', -> describe 'incomplete attributes', -> beforeEach -> - record = store.loadIncomplete Travis.Job, { id: 1, state: 'started' } + store.loadIncomplete Travis.Job, { id: 1, state: 'started' } + record = store.find Travis.Job, 1 it 'does not load record on duration, finishedAt and result if job is not in finished state', -> record.get('_duration') @@ -19,15 +20,16 @@ describe 'Travis.Job', -> waits 50 runs -> - expect( record.get('complete') ).toBeFalsy() + expect( record.get('incomplete') ).toBeTruthy() it 'loads the rest of the record if it\'s in finished state', -> - record = store.loadIncomplete Travis.Job, { id: 1, state: 'finished' } + store.loadIncomplete Travis.Job, { id: 1, state: 'passed' } + record = store.find Travis.Job, 1 record.get('finishedAt') waits 50 runs -> - expect( record.get('complete') ).toBeTruthy() + expect( record.get('incomplete') ).toBeFalsy() describe 'with different number of config keys in sibling jobs', -> diff --git a/assets/scripts/spec/unit/log_spec.coffee b/assets/scripts/spec/unit/log_spec.coffee deleted file mode 100644 index a8ff7d1d..00000000 --- a/assets/scripts/spec/unit/log_spec.coffee +++ /dev/null @@ -1,229 +0,0 @@ -log = null -target = null - -describe 'Travis.Log', -> - beforeEach -> - target = Em.Object.create - calls: [] - appendLog: (payloads) -> - lines = payloads.map (p) -> - line = p.content - delete p.content - line - - @get('calls').pushObject - options: payloads - lines: lines - - log = Travis.Log.create(target: target) - - it 'works with log passed as a string', -> - log.append '1\n2' - - expect( target.get('calls.firstObject.lines') ).toEqual ['1', '2'] - - - it 'splits lines', -> - log.append ['1\r\n2\n\n', '3'] - - expect( target.get('calls.length') ).toEqual 1 - expect( target.get('calls.firstObject.lines') ).toEqual ['1', '2', '', '3'] - - it 'escapes html characters', -> - log.append '<>' - - expect( target.get('calls.firstObject.lines') ).toEqual ['<>'] - - it 'normalizes ansi mess', -> - log.append ['foo\r\r', 'bar'] - - expect( target.get('calls.firstObject.lines') ).toEqual [ 'foo', 'bar' ] - - it 'calls target with folds separation', -> - fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/ - log.addFold fold - - fold = Em.Object.create name: 'qux', startPattern: /^\$ qux/, endPattern: /^\$/ - log.addFold fold - - log.append [ - '1\n', '2\n' - '$ foo --foo\n', '1\n' - '$ bar\n' - '$ baz\n' - '$ qux\n', '1\n', '2\n' - '$ end\n' - ] - - # expect( target.get('calls.length') ).toEqual 5 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['1', '2'] - expect( options[0]).toEqual [ { number: 1 }, { number: 2 } ] - - expect( lines[1] ).toEqual ['$ foo --foo', '1'] - expect( options[1]).toEqual [ - { number: 3, fold: 'foo' }, - { number: 4, fold: 'foo', foldContinuation: true, foldEnd: true }] - - expect( lines[2] ).toEqual ['$ bar', '$ baz'] - expect( options[2]).toEqual [{ number: 5 }, { number: 6 }] - - expect( lines[3] ).toEqual ['$ qux', '1', '2'] - expect( options[3]).toEqual [ - { number: 7, fold: 'qux' }, - { number: 8, fold: 'qux', foldContinuation: true }, - { number: 9, fold: 'qux', foldContinuation: true, foldEnd: true }] - - expect( lines[4] ).toEqual ['$ end'] - expect( options[4]).toEqual [{ number: 10 }] - - it 'works properly when log is started with fold', -> - fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/ - log.addFold fold - - log.append [ - '$ foo --foo\n', '1\n' - '$ bar\n' - ] - - expect( target.get('calls.length') ).toEqual 2 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['$ foo --foo', '1'] - expect( options[0]).toEqual [ - { number: 1, fold: 'foo' }, - { number: 2, fold: 'foo', foldContinuation: true, foldEnd: true }] - - expect( lines[1] ).toEqual ['$ bar'] - expect( options[1]).toEqual [{ number: 3 }] - - it 'works properly for 2 consecutive folds', -> - fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/ - log.addFold fold - - log.append [ - '$ foo --foo\n', '1\n' - '$ foo --bar\n', '2\n' - '$ bar\n' - ] - - expect( target.get('calls.length') ).toEqual 3 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['$ foo --foo', '1'] - expect( options[0]).toEqual [ - { number: 1, fold: 'foo' }, - { number: 2, fold: 'foo', foldContinuation: true, foldEnd: true }] - - expect( lines[1] ).toEqual ['$ foo --bar', '2'] - expect( options[1]).toEqual [ - { number: 3, fold: 'foo' }, - { number: 4, fold: 'foo', foldContinuation: true, foldEnd: true }] - - expect( lines[2] ).toEqual ['$ bar'] - expect( options[2]).toEqual [{ number: 5 }] - - it 'works fine with not finalized fold', -> - fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/ - log.addFold fold - - log.append [ - '$ foo --foo\n', '1\n' - ] - - expect( target.get('calls.length') ).toEqual 1 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['$ foo --foo', '1'] - expect( options[0]).toEqual [ - { fold: 'foo', number: 1 }, - { fold: 'foo', number: 2, foldContinuation: true }] - - it 'allows to continue fold', -> - fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/ - log.addFold fold - - log.append [ - '$ foo --foo\n', '1\n' - ] - - log.append '2\n' - - log.append [ - '3\n' - '$ bar\n' - ] - - expect( target.get('calls.length') ).toEqual 4 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['$ foo --foo', '1'] - expect( options[0]).toEqual [ - { fold: 'foo', number: 1 }, - { fold: 'foo', number: 2, foldContinuation: true }] - - expect( lines[1] ).toEqual ['2'] - expect( options[1]).toEqual [ - { fold: 'foo', number: 3, foldContinuation: true } - ] - - expect( lines[2] ).toEqual ['3'] - expect( options[2]).toEqual [ - { fold: 'foo', number: 4, foldContinuation: true, foldEnd: true } - ] - - expect( lines[3] ).toEqual ['$ bar'] - expect( options[3]).toEqual [{ number: 5 }] - - it 'notifies that the line should be appended', -> - log.append '$ foo\n.' - - log.append '...' - - log.append '..\n$ bar\n' - - expect( target.get('calls.length') ).toEqual 3 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['$ foo', '.'] - expect( options[0]).toEqual [{ number: 1 }, { number: 2 }] - - expect( lines[1] ).toEqual ['...'] - expect( options[1]).toEqual [{ append: true, number: 2 }] - - expect( lines[2] ).toEqual ['..', '$ bar'] - expect( options[2]).toEqual [{ append: true, number: 2 }, { number: 3 }] - - it 'notifies that the line should be replaced', -> - log.append '$ foo\n' - - log.append '\rDownloading 50%' - log.append '\rDownloading 100%\r\n' - - log.append '$ bar\n' - - expect( target.get('calls.length') ).toEqual 4 - lines = target.get('calls').map (call) -> call.lines - options = target.get('calls').map (call) -> call.options - - expect( lines[0] ).toEqual ['$ foo'] - expect( options[0]).toEqual [{ number: 1 }] - - expect( lines[1] ).toEqual ['', 'Downloading 50%'] - expect( options[1]).toEqual [{ number: 2 }, { number: 2, replace: true }] - - expect( lines[2] ).toEqual ['', 'Downloading 100%'] - expect( options[2]).toEqual [{ number: 2, append: true }, { number: 2, replace: true }] - - expect( lines[3] ).toEqual ['$ bar'] - expect( options[3]).toEqual [{ number: 3 }] - - it 'notifies that the line should be replaced even if carriage return is in the middle', -> - diff --git a/assets/scripts/spec/unit/merge_spec.coffee b/assets/scripts/spec/unit/merge_spec.coffee new file mode 100644 index 00000000..75007d1e --- /dev/null +++ b/assets/scripts/spec/unit/merge_spec.coffee @@ -0,0 +1,105 @@ +record = null +store = null + +describe 'Travis.Model - merge', -> + beforeEach -> + Travis.Foo = Travis.Model.extend + login: DS.attr('string') + firstName: DS.attr('string') + email: DS.attr('string') + + bar: DS.belongsTo('Travis.Bar') + + Travis.Bar = Travis.Model.extend + foos: DS.hasMany('Travis.Foo') + + store = Travis.Store.create() + + afterEach -> + delete Travis.Foo + delete Travis.Bar + store.destroy() + + it 'updates the attributes of materialized record', -> + data = { id: '1', firstName: 'Piotr', email: 'drogus@example.org' } + store.load(Travis.Foo, { id: '1' }, data) + record = store.find(Travis.Foo, '1') + + changes = 0 + + observer = -> + changes += 1 + record.addObserver 'firstName', observer + + Ember.run -> + store.merge(Travis.Foo, { id: '1', first_name: 'Peter', login: 'drogus' }) + + record.removeObserver 'firstName', observer + + expect(changes > 0).toBeTruthy() + expect(record.get('firstName')).toEqual('Peter') + expect(record.get('login')).toEqual('drogus') + expect(record.get('email')).toEqual('drogus@example.org') + + it 'updates belongsTo relationship of materialized record', -> + data = { id: '1', login: 'drogus', bar_id: '1' } + store.load(Travis.Foo, data, { id: '1' }) + store.load(Travis.Bar, { id: '1' }, { id: '1' }) + store.load(Travis.Bar, { id: '2' }, { id: '2' }) + record = store.find(Travis.Foo, '1') + + changed = false + + observer = -> + changed = true + record.addObserver 'bar', observer + + Ember.run -> + store.merge(Travis.Foo, { id: '1', bar_id: '2' }) + + record.removeObserver 'bar', observer + + bar = store.find(Travis.Bar, '2') + + expect(changed).toEqual(true) + expect(record.get('bar')).toEqual(bar) + + it 'updates hasMany relationship of materialized record', -> + data = { id: '1', foo_ids: [1] } + store.load(Travis.Bar, data, { id: '1' }) + store.load(Travis.Foo, { id: '1' }, { id: '1' }) + store.load(Travis.Foo, { id: '2' }, { id: '2' }) + + record = store.find(Travis.Bar, '1') + + changed = false + + observer = -> + changed = true + record.addObserver 'foos.length', observer + + Ember.run -> + store.merge(Travis.Bar, { id: '1', foo_ids: [1, 2] }) + + record.removeObserver 'foos.length', observer + + expect(changed).toEqual(true) + expect(record.get('foos.length')).toEqual(2) + expect(record.get('foos').mapProperty('id')).toEqual(['1', '2']) + + it 'loads given data if it\'s not in the store yet', -> + store.merge(Travis.Foo, { id: '1', login: 'drogus' }) + + record = store.find(Travis.Foo, 1) + + expect(record.get('login')).toEqual('drogus') + expect(record.get('email')).toEqual(null) + + it 'merges data if it\'s just loaded into store', -> + store.load(Travis.Foo, { id: '1', login: 'drogus', email: 'drogus@example.org' }, { id: '1' }) + store.merge(Travis.Foo, { id: '1', login: 'svenfuchs' }) + + record = store.find(Travis.Foo, 1) + + expect(record.get('login')).toEqual('svenfuchs') + expect(record.get('email')).toEqual('drogus@example.org') diff --git a/assets/scripts/spec/unit/pre_view_spec.coffee b/assets/scripts/spec/unit/pre_view_spec.coffee deleted file mode 100644 index c1a95ce5..00000000 --- a/assets/scripts/spec/unit/pre_view_spec.coffee +++ /dev/null @@ -1,161 +0,0 @@ -view = null -store = null -record = null - -describe 'Travis.PreView', -> - beforeEach -> - store = Travis.Store.create() - - afterEach -> - store.destroy() - view.remove() - view.destroy() - - it 'works fine with existing log, which is appended', -> - store.load Travis.Artifact, 1, { id: 1, body: '$ start\n' } - log = Travis.Artifact.find(1) - log.set('version', 1) - - Ember.run -> - view = Travis.PreView.create(log: null) - view.append() - - expect( view.$('#log').length ).toEqual 1 - - Ember.run -> - view.set 'log', log - log.set 'isLoaded', true - - waits 50 - runs -> - expect( view.$('#log p').length ).toEqual 1 - expect( view.$('#log p').text().trim() ).toEqual '1$ start' - - Ember.run -> - log.append('$ end') - - waits 50 - runs -> - expect( view.$('#log p').length ).toEqual 2 - expect( view.$('#log p').text().trim() ).toEqual '1$ start2$ end' - - it 'works fine with log already attahed to view', -> - store.load Travis.Artifact, 1, { id: 1, body: '$ start\n' } - log = Travis.Artifact.find(1) - - Ember.run -> - view = Travis.PreView.create() - view.set('log', log) - view.append() - - Ember.run -> - log.append('end') - - waits 50 - runs -> - expect( view.$('#log p').length ).toEqual 2 - expect( view.$('#log p').text().trim() ).toEqual '1$ start2end' - - it 'folds items', -> - store.load Travis.Artifact, 1, { id: 1, body: '$ start\n' } - log = Travis.Artifact.find(1) - - Ember.run -> - view = Travis.PreView.create() - view.set('log', log) - view.append() - - Ember.run -> - log.append '$ bundle install\n1\n2\n' - - Ember.run -> - log.append '3\n4\n$ something' - - waits 50 - runs -> - expect( view.$('#log > p').length ).toEqual 2 - expect( view.$('#log .fold.bundle').length ).toEqual 1 - expect( view.$('#log .fold.bundle > p').length ).toEqual 5 - - - it 'works properly with fragment document', -> - store.load Travis.Artifact, 1, { id: 1, body: '' } - log = Travis.Artifact.find(1) - - Ember.run -> - view = Travis.PreView.create() - view.set('log', log) - view.append() - - waits 50 - runs -> - payloads = [ - { number: 1, content: 'foo' } - { number: 1, content: 'bar', append: true } - ] - - # it should work even if we need to append to fragment in memory - view.appendLog(payloads) - - expect( view.$('#L1').parent().text().trim() ).toEqual '1foobar' - - # now, let's append more to this line, it's in DOM already - view.appendLog([ { number: 1, content: 'baz', append: true } ]) - - expect( view.$('#L1').parent().text().trim() ).toEqual '1foobarbaz' - - payloads = [ - { number: 1, content: 'foo', replace: true } - ] - # replace should work in DOM - view.appendLog(payloads) - expect( view.$('#L1').parent().text().trim() ).toEqual '1foo' - - payloads = [ - { number: 2, content: 'foo' } - { number: 2, content: 'bar', replace: true } - ] - # replace should work when element is in fragment - view.appendLog(payloads) - expect( view.$('#L2').parent().text().trim() ).toEqual '2bar' - - payloads = [ - { number: 3, content: '$ bundle install', fold: 'bundle' } - { number: 4, content: 'Installing rails', fold: 'bundle', foldContinuation: true } - ] - # folds should work properly with fragment - view.appendLog(payloads) - expect( view.$('.bundle #L3').parent().text().trim() ).toEqual '3$ bundle install' - expect( view.$('.bundle #L4').parent().text().trim() ).toEqual '4Installing rails' - expect( view.$('.bundle > p').length ).toEqual 2 - - payloads = [ - { number: 5, content: 'Installing travis', fold: 'bundle', foldContinuation: true } - ] - # folds should also work when already in DOM - view.appendLog(payloads) - expect( view.$('.bundle #L5').parent().text().trim() ).toEqual '5Installing travis' - expect( view.$('.bundle > p').length ).toEqual 3 - - # regular line append - view.appendLog([ { number: 6, content: 'next'} ]) - expect( view.$('#L6').parent().text().trim() ).toEqual '6next' - - # openFold when in fragment - payloads = [ - { number: 7, content: '$ install', fold: 'install' } - { number: 8, content: 'Installing foo', fold: 'install', foldContinuation: true } - { number: 9, content: 'error', openFold: true, fold: 'install', foldContinuation: true } - ] - # folds should work properly with fragment - view.appendLog(payloads) - expect( view.$('.install').hasClass('show-first-line') ).toEqual false - - # end fold when in fragment - payloads = [ - { number: 10, content: '$ install', fold: 'install2' } - { number: 11, content: 'Installing foo', fold: 'install2', foldEnd: true, foldContinuation: true } - ] - # folds should work properly with fragment - view.appendLog(payloads) - expect( view.$('.install2').hasClass('show-first-line') ).toEqual false diff --git a/assets/scripts/travis.coffee b/assets/scripts/travis.coffee index 912b79a8..e30412a0 100644 --- a/assets/scripts/travis.coffee +++ b/assets/scripts/travis.coffee @@ -1,6 +1,9 @@ require 'ext/jquery' require 'ext/ember/namespace' +window.ENV ||= {} +window.ENV.RAISE_ON_DEPRECATION = true + if window.history.state == undefined window.history.state = {} oldPushState = window.history.pushState @@ -28,10 +31,77 @@ Storage = Em.Object.extend clear: -> @set('storage', {}) +window.Travis = Em.Application.extend(Ember.Evented, + authStateBinding: 'auth.state' + signedIn: (-> @get('authState') == 'signed-in' ).property('authState') -@Travis = Em.Namespace.create Ember.Evented, + setup: -> + @store = Travis.Store.create( + adapter: Travis.RestAdapter.create() + ) + @store.loadMany(Travis.Sponsor, Travis.SPONSORS) + + @slider = new Travis.Slider() + @pusher = new Travis.Pusher(Travis.config.pusher_key) + @tailing = new Travis.Tailing() + + @set('auth', Travis.Auth.create(app: this, endpoint: Travis.config.api_endpoint)) + + reset: -> + @store.destroy() + @setup() + + @_super.apply(this, arguments); + + lookup: -> + @__container__.lookup.apply this, arguments + + storeAfterSignInPath: (path) -> + @get('auth').storeAfterSignInPath(path) + + autoSignIn: (path) -> + @get('auth').autoSignIn() + + signIn: -> + @get('auth').signIn() + + signOut: -> + @get('auth').signOut() + + receive: -> + @store.receive.apply(@store, arguments) + + toggleSidebar: -> + $('body').toggleClass('maximized') + # TODO gotta force redraws here :/ + element = $('') + $('#top .profile').append(element) + Em.run.later (-> element.remove()), 10 + element = $('') + $('#repo').append(element) + Em.run.later (-> element.remove()), 10 + + setLocale: (locale) -> + return unless locale + I18n.locale = locale + Travis.set('locale', locale) + + defaultLocale: 'en' + + ready: -> + location.href = location.href.replace('#!/', '') if location.hash.slice(0, 2) == '#!' + I18n.fallbacks = true + @setLocale 'locale', @get('defaultLocale') + + currentDate: -> + new Date() +).create() + +Travis.deferReadiness() + +$.extend Travis, run: -> - Travis.run() + Travis.advanceReadiness() # bc, remove once merged to master config: api_endpoint: $('meta[rel="travis.api_endpoint"]').attr('href') @@ -40,34 +110,13 @@ Storage = Em.Object.extend CONFIG_KEYS: ['rvm', 'gemfile', 'env', 'jdk', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala', 'compiler'] - ROUTES: - 'profile/:login/me': ['profile', 'user'] - 'profile/:login': ['profile', 'hooks'] - 'profile': ['profile', 'hooks'] - 'stats': ['stats', 'show'] - ':owner/:name/jobs/:id/:line': ['home', 'job'] - ':owner/:name/jobs/:id': ['home', 'job'] - ':owner/:name/builds/:id': ['home', 'build'] - ':owner/:name/builds': ['home', 'builds'] - ':owner/:name/pull_requests': ['home', 'pullRequests'] - ':owner/:name/branches': ['home', 'branches'] - ':owner/:name': ['home', 'current'] - '': ['home', 'index'] - '#': ['home', 'index'] - QUEUES: [ - { name: 'common', display: 'Common' } { name: 'linux', display: 'Linux' } { name: 'mac_osx', display: 'Mac and OSX' } ] INTERVALS: { sponsors: -1, times: -1, updateTimes: 1000 } - setLocale: (locale) -> - return unless locale - I18n.locale = locale - Travis.set('locale', locale) - storage: (-> storage = null try @@ -77,7 +126,7 @@ Storage = Em.Object.extend storage )() - default_locale: 'en' + sessionStorage: (-> storage = null try @@ -91,26 +140,25 @@ Storage = Em.Object.extend storage )() - run: (attrs) -> - location.href = location.href.replace('#!/', '') if location.hash.slice(0, 2) == '#!' - - I18n.fallbacks = true - Travis.setLocale 'locale', @default_locale - - Ember.run.next this, -> - app = Travis.App.create(attrs || {}) - # TODO: router expects the classes for controllers on main namespace, so - # if we want to keep app at Travis.app, we need to copy that, it would - # be ideal to send a patch to ember and get rid of this - $.each Travis, (key, value) -> - app[key] = value if value && value.isClass && key != 'constructor' - - @app = app - @store = app.store - $ => app.initialize() - setupGoogleAnalytics() if Travis.config.ga_code require 'ext/i18n' require 'travis/ajax' -require 'app' +require 'auth' +require 'controllers' +require 'helpers' +require 'models' +require 'pusher' +require 'routes' +require 'slider' +require 'store' +require 'tailing' +require 'templates' +require 'views' + +require 'config/locales' +require 'data/sponsors' + +require 'travis/instrumentation' + +Travis.setup() diff --git a/assets/scripts/vendor/ember-data.js b/assets/scripts/vendor/ember-data.js index 9a71e855..4b83ba82 100644 --- a/assets/scripts/vendor/ember-data.js +++ b/assets/scripts/vendor/ember-data.js @@ -1,15 +1,46 @@ +// Last commit: 0c516e4 (2013-03-08 15:59:48 +0100) + + (function() { window.DS = Ember.Namespace.create({ - CURRENT_API_REVISION: 4 + // this one goes past 11 + CURRENT_API_REVISION: 12 }); })(); +(function() { +var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred + Evented = Ember.Evented, // ember-runtime/mixins/evented + run = Ember.run, // ember-metal/run-loop + get = Ember.get; // ember-metal/accessors + +var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { + init: function() { + this._super.apply(this, arguments); + this.one('didLoad', function() { + run(this, 'resolve', this); + }); + + if (get(this, 'isLoaded')) { + this.trigger('didLoad'); + } + } +}); + +DS.LoadPromise = LoadPromise; + +})(); + + + (function() { var get = Ember.get, set = Ember.set; +var LoadPromise = DS.LoadPromise; // system/mixins/load_promise + /** A record array is an array that contains records of a certain type. The record array materializes records as needed when they are retrieved for the first @@ -18,8 +49,7 @@ var get = Ember.get, set = Ember.set; in response to queries. */ -DS.RecordArray = Ember.ArrayProxy.extend({ - +DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { /** The model type contained by this record array. @@ -33,17 +63,46 @@ DS.RecordArray = Ember.ArrayProxy.extend({ // necessary, by the store. content: null, + isLoaded: false, + isUpdating: false, + // The store that created this record array. store: null, objectAtContent: function(index) { var content = get(this, 'content'), - clientId = content.objectAt(index), + reference = content.objectAt(index), store = get(this, 'store'); - if (clientId !== undefined) { - return store.findByClientId(get(this, 'type'), clientId); + if (reference) { + return store.recordForReference(reference); } + }, + + materializedObjectAt: function(index) { + var reference = get(this, 'content').objectAt(index); + if (!reference) { return; } + + if (get(this, 'store').recordIsMaterialized(reference)) { + return this.objectAt(index); + } + }, + + update: function() { + if (get(this, 'isUpdating')) { return; } + + var store = get(this, 'store'), + type = get(this, 'type'); + + store.fetchAll(type, this); + }, + + addReference: function(reference) { + get(this, 'content').addObject(reference); + }, + + removeReference: function(reference) { + get(this, 'content').removeObject(reference); } }); @@ -56,6 +115,7 @@ var get = Ember.get; DS.FilteredRecordArray = DS.RecordArray.extend({ filterFunction: null, + isLoaded: true, replace: function() { var type = get(this, 'type').toString(); @@ -77,169 +137,25 @@ var get = Ember.get, set = Ember.set; DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ query: null, - isLoaded: false, replace: function() { var type = get(this, 'type').toString(); throw new Error("The result of a server query (on " + type + ") is immutable."); }, - load: function(array) { + load: function(references) { var store = get(this, 'store'), type = get(this, 'type'); - var clientIds = store.loadMany(type, array).clientIds; - this.beginPropertyChanges(); - set(this, 'content', Ember.A(clientIds)); + set(this, 'content', Ember.A(references)); set(this, 'isLoaded', true); this.endPropertyChanges(); - } -}); - -})(); - - - -(function() { -var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor; - -var Set = function() { - this.hash = {}; - this.list = []; -}; - -Set.prototype = { - add: function(item) { - var hash = this.hash, - guid = guidFor(item); - - if (hash.hasOwnProperty(guid)) { return; } - - hash[guid] = true; - this.list.push(item); - }, - - remove: function(item) { - var hash = this.hash, - guid = guidFor(item); - - if (!hash.hasOwnProperty(guid)) { return; } - - delete hash[guid]; - var list = this.list, - index = Ember.EnumerableUtils.indexOf(this, item); - - list.splice(index, 1); - }, - - isEmpty: function() { - return this.list.length === 0; - } -}; - -var LoadedState = Ember.State.extend({ - recordWasAdded: function(manager, record) { - var dirty = manager.dirty, observer; - dirty.add(record); - - observer = function() { - if (!get(record, 'isDirty')) { - record.removeObserver('isDirty', observer); - manager.send('childWasSaved', record); - } - }; - - record.addObserver('isDirty', observer); - }, - - recordWasRemoved: function(manager, record) { - var dirty = manager.dirty, observer; - dirty.add(record); - - observer = function() { - record.removeObserver('isDirty', observer); - if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); } - }; - - record.addObserver('isDirty', observer); - } -}); - -var states = { - 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) { - this._super(manager, record); - manager.goToState('dirty'); - }, - - update: function(manager, clientIds) { - var manyArray = manager.manyArray; - set(manyArray, 'content', clientIds); - } - }), - - dirty: LoadedState.create({ - isLoaded: true, - isDirty: true, - - childWasSaved: function(manager, child) { - var dirty = manager.dirty; - dirty.remove(child); - - if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); } - }, - - arrayBecameSaved: function(manager) { - manager.goToState('clean'); - } - }) -}; - -DS.ManyArrayStateManager = Ember.StateManager.extend({ - manyArray: null, - 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 = get(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'); - } + var self = this; + // TODO: does triggering didLoad event should be the last action of the runLoop? + Ember.run.once(function() { + self.trigger('didLoad'); + }); } }); @@ -250,124 +166,176 @@ DS.ManyArrayStateManager = Ember.StateManager.extend({ (function() { var get = Ember.get, set = Ember.set; +/** + A ManyArray is a RecordArray that represents the contents of a has-many + relationship. + + The ManyArray is instantiated lazily the first time the relationship is + requested. + + ### Inverses + + Often, the relationships in Ember Data applications will have + an inverse. For example, imagine the following models are + defined: + + App.Post = DS.Model.extend({ + comments: DS.hasMany('App.Comment') + }); + + App.Comment = DS.Model.extend({ + post: DS.belongsTo('App.Post') + }); + + If you created a new instance of `App.Post` and added + a `App.Comment` record to its `comments` has-many + relationship, you would expect the comment's `post` + property to be set to the post that contained + the has-many. + + We call the record to which a relationship belongs the + relationship's _owner_. +*/ DS.ManyArray = DS.RecordArray.extend({ init: function() { - set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this })); - - return this._super(); + this._super.apply(this, arguments); + this._changesToSync = Ember.OrderedSet.create(); }, - parentRecord: null, + /** + @private - isDirty: Ember.computed(function() { - return get(this, 'stateManager.currentState.isDirty'); - }).property('stateManager.currentState').cacheable(), + The record to which this relationship belongs. - isLoaded: Ember.computed(function() { - return get(this, 'stateManager.currentState.isLoaded'); - }).property('stateManager.currentState').cacheable(), + @property {DS.Model} + */ + owner: null, - send: function(event, context) { - this.get('stateManager').send(event, context); + // LOADING STATE + + isLoaded: false, + + loadingRecordsCount: function(count) { + this.loadingRecordsCount = count; + }, + + loadedRecord: function() { + this.loadingRecordsCount--; + if (this.loadingRecordsCount === 0) { + set(this, 'isLoaded', true); + this.trigger('didLoad'); + } }, fetch: function() { - var clientIds = get(this, 'content'), + var references = get(this, 'content'), store = get(this, 'store'), - type = get(this, 'type'); + type = get(this, 'type'), + owner = get(this, 'owner'); - store.fetchUnloadedClientIds(type, clientIds); + store.fetchUnloadedReferences(type, references, owner); }, // Overrides Ember.Array's replace method to implement replaceContent: function(index, removed, added) { - var parentRecord = get(this, 'parentRecord'); - var pendingParent = parentRecord && !get(parentRecord, 'id'); - var stateManager = get(this, 'stateManager'); - // Map the array of record objects into an array of client ids. added = added.map(function(record) { - Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === record.constructor)); - - // If the record to which this many array belongs does not yet - // have an id, notify the newly-added record that it must wait - // for the parent to receive an id before the child can be - // saved. - if (pendingParent) { - record.send('waitingOn', parentRecord); - } - - var oldParent = this.assignInverse(record, parentRecord); - - record.get('transaction') - .relationshipBecameDirty(record, oldParent, parentRecord); - - stateManager.send('recordWasAdded', record); - - return record.get('clientId'); + Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor)); + return get(record, '_reference'); }, this); - var store = this.store; - - var len = index+removed, record; - for (var i = index; i < len; i++) { - // TODO: null out inverse FK - record = this.objectAt(i); - 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 - // can tell the child it no longer needs to wait. - if (pendingParent) { - record.send('doneWaitingOn', parentRecord); - } - - stateManager.send('recordWasAdded', record); - } - this._super(index, removed, added); }, - assignInverse: function(record, parentRecord, remove) { - var associationMap = get(record.constructor, 'associations'), - possibleAssociations = associationMap.get(parentRecord.constructor), - possible, actual, oldParent; + arrangedContentDidChange: function() { + this.fetch(); + }, - if (!possibleAssociations) { return; } + arrayContentWillChange: function(index, removed, added) { + var owner = get(this, 'owner'), + name = get(this, 'name'); - for (var i = 0, l = possibleAssociations.length; i < l; i++) { - possible = possibleAssociations[i]; + if (!owner._suspendedRelationships) { + // This code is the first half of code that continues inside + // of arrayContentDidChange. It gets or creates a change from + // the child object, adds the current owner as the old + // parent if this is the first time the object was removed + // from a ManyArray, and sets `newParent` to null. + // + // Later, if the object is added to another ManyArray, + // the `arrayContentDidChange` will set `newParent` on + // the change. + for (var i=index; ihi |
Howdy Dave
+Howdy Mary
+Howdy Sara
+Sorry, nobody is available for this task.
+ {{/each}} + ``` + ### Specifying a View class for items + If you provide an `itemViewClass` option that references a view class + with its own `template` you can omit the block. + + The following template: + + ```handlebars + {{#view App.MyView }} + {{each view.items itemViewClass="App.AnItemView"}} + {{/view}} + ``` + + And application code + + ```javascript + App = Ember.Application.create({ + MyView: Ember.View.extend({ + items: [ + Ember.Object.create({name: 'Dave'}), + Ember.Object.create({name: 'Mary'}), + Ember.Object.create({name: 'Sara'}) + ] + }) + }); + + App.AnItemView = Ember.View.extend({ + template: Ember.Handlebars.compile("Greetings {{name}}") + }); + ``` + + Will result in the HTML structure below + + ```html +