diff --git a/NOTES.txt b/NOTES.txt new file mode 100644 index 00000000..1cfb310e --- /dev/null +++ b/NOTES.txt @@ -0,0 +1,43 @@ +# Wat + +* wat. http://jsfiddle.net/svenfuchs/YPeQp + +* Where the frack am i supposed to put view/display related helper logic + when using #each? (e.g. format published_at for each blog post on blog/) + +* Oh, uh, now there's `contextBinding`? What's that? How's that different from + `contentBinding`? + +# Handlebars + +* Can't {{bindAttr}} be just {{attr}}? Who cares it's "bound" in that context? + {{#each}} isn't {{#bindEach}} either. + +* Why is {{#collection contentBinding="foo"}} not just {{#collection foo}}? + Also, can {{#collection contentBinding="content"}} be just {{#collection}}? + +# Router + +* I think `Route#connectOutlets` should be something like `action`, `run`, + `call` or similar (*if* it's intended to be the main user facing method). + +* I didn't expect I'd have to pay attention to the timing of loading records + (even when they're present locally as fixtures) in `Router#serialize`. Is it + not possible to make this a bound property? Everything else that I use in + a handlebar template seems to be capable of being updated late except for + the url generation? + +* The `Route#serialize` method is called with various types objects as the + second argument, which confused the heck out of me. Depending on what I set + as a context in `connectOutlet` and then define as a context in the action + helper (does it need to be quoted or not? what's the namespace and current + scope there?) I might get a controller or model instance. When I use the + back/forward button I get a plain object? + +# Stuff + +* I'm sure joining get and getPath was considered, right? I keep forgetting + to change `get` to `getPath` when I move stuff around and now need to + look up a path instead of a single key. What's the reason for separate + methods? Performance? + diff --git a/app/assets/javascripts/app/app.js.coffee b/app/assets/javascripts/app/app.js.coffee index 2f4d7ab0..273ad264 100644 --- a/app/assets/javascripts/app/app.js.coffee +++ b/app/assets/javascripts/app/app.js.coffee @@ -1,3 +1,4 @@ +#= require_tree ./helpers #= require_tree ./models #= require_tree ./templates #= require ./controllers.js @@ -10,4 +11,12 @@ Travis.store = DS.Store.extend( revision: 4 adapter: Travis.FixtureAdapter.create() ).create() + +# apparently fixtures behave weird unless preloaded :/ should move to mockjax for testing +Travis.Build.find() +Travis.Repository.find() +Travis.Commit.find() +Travis.Job.find() +Travis.Artifact.find() + Travis.initialize() diff --git a/app/assets/javascripts/app/controllers.js.coffee b/app/assets/javascripts/app/controllers.js.coffee index 64742c72..175ac594 100644 --- a/app/assets/javascripts/app/controllers.js.coffee +++ b/app/assets/javascripts/app/controllers.js.coffee @@ -1,8 +1,14 @@ Travis.ApplicationController = Em.Controller.extend() Travis.RepositoriesController = Em.ArrayController.extend() -Travis.RepositoryController = Em.Controller.extend() +Travis.RepositoryController = Em.ObjectController.extend(Travis.Urls.Repository) Travis.TabsController = Em.Controller.extend() -Travis.CurrentController = Em.Controller.extend() Travis.HistoryController = Em.ArrayController.extend() -Travis.BuildController = Em.Controller.extend() +Travis.JobController = Em.ObjectController.extend() Travis.LoadingController = Em.Controller.extend() + +Travis.CurrentController = Travis.BuildController = Em.ObjectController.extend + classes: (-> + Travis.Helpers.colorForResult(@getPath('content.result')) + ).property('content.result') + + diff --git a/app/assets/javascripts/app/helpers/handlebars.js.coffee b/app/assets/javascripts/app/helpers/handlebars.js.coffee new file mode 100644 index 00000000..067d823d --- /dev/null +++ b/app/assets/javascripts/app/helpers/handlebars.js.coffee @@ -0,0 +1,38 @@ +safe = (string) -> + new Handlebars.SafeString(string) + +Handlebars.registerHelper 'whats_this', (id) -> + safe ' ' + +Handlebars.registerHelper 'tipsy', (text, tip) -> + safe '' + text + '' + +Handlebars.registerHelper 't', (key) -> + safe I18n.t(key) + +Ember.registerBoundHelper 'formatTime', (value, options) -> + safe Travis.Helpers.timeAgoInWords(value) || '-' + +Ember.registerBoundHelper 'formatDuration', (duration, options) -> + safe Travis.Helpers.timeInWords(duration) + +Ember.registerBoundHelper 'formatCommit', (commit, options) -> + branch = commit.get('branch') + branch = " #{branch}" if branch + safe (commit.get('sha') || '').substr(0, 7) + branch + +Ember.registerBoundHelper 'formatSha', (sha, options) -> + safe (sha || '').substr(0, 7) + +Ember.registerBoundHelper 'pathFrom', (url, options) -> + safe (url || '').split('/').pop() + +Ember.registerBoundHelper 'formatMessage', (message, options) -> + safe Travis.Helpers.formatMessage(message, options) + +Ember.registerBoundHelper 'formatConfig', (config, options) -> + safe Travis.Helpers.formatConfig(config) + +Ember.registerBoundHelper 'formatLog', (log, options) -> + Travis.Log.filter(log) if log + diff --git a/app/assets/javascripts/app/helpers/helpers.js.coffee b/app/assets/javascripts/app/helpers/helpers.js.coffee new file mode 100644 index 00000000..7056fed3 --- /dev/null +++ b/app/assets/javascripts/app/helpers/helpers.js.coffee @@ -0,0 +1,65 @@ +@Travis.Helpers = + colorForResult: (result) -> + (if result is 0 then 'green' else (if result is 1 then 'red' else null)) + + formatConfig: (config) -> + config = $.only config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'scala', 'jdk', 'python', 'perl' + values = $.map config, (value, key) -> + value = (if value && value.join then value.join(', ') else value) || '' + '%@: %@'.fmt $.camelize(key), value + if values.length == 0 then '-' else values.join(', ') + + formatMessage: (message, options) -> + message = message or '' + message = message.split(/\n/)[0] if options.short + @_emojize(@_escape(message)).replace /\n/g, '
' + + timeAgoInWords: (date) -> + $.timeago.distanceInWords date + + durationFrom: (started, finished) -> + started = started and @_toUtc(new Date(@_normalizeDateString(started))) + finished = if finished then @_toUtc(new Date(@_normalizeDateString(finished))) else @_nowUtc() + if started && finished then Math.round((finished - started) / 1000) else 0 + + timeInWords: (duration) -> + days = Math.floor(duration / 86400) + hours = Math.floor(duration % 86400 / 3600) + minutes = Math.floor(duration % 3600 / 60) + seconds = duration % 60 + + if days > 0 + 'more than 24 hrs' + else + result = [] + result.push hours + ' hr' if hours is 1 + result.push hours + ' hrs' if hours > 1 + result.push minutes + ' min' if minutes > 0 + result.push seconds + ' sec' if seconds > 0 + if result.length > 0 then result.join(' ') else '-' + + _normalizeDateString: (string) -> + if window.JHW + string = string.replace('T', ' ').replace(/-/g, '/') + string = string.replace('Z', '').replace(/\..*$/, '') + string + + _nowUtc: -> + @_toUtc new Date() + + _toUtc: (date) -> + Date.UTC date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds() + + _emojize: (text) -> + emojis = text.match(/:\S+?:/g) + if emojis isnt null + $.each emojis.uniq(), (ix, emoji) -> + strippedEmoji = emoji.substring(1, emoji.length - 1) + unless EmojiDictionary.indexOf(strippedEmoji) is -1 + image = '\''' + text = text.replace(new RegExp(emoji, 'g'), image) + text + + _escape: (text) -> + text.replace(/&/g, '&').replace(//g, '>' + diff --git a/app/assets/javascripts/app/helpers/urls.js.coffee b/app/assets/javascripts/app/helpers/urls.js.coffee new file mode 100644 index 00000000..2107cbfc --- /dev/null +++ b/app/assets/javascripts/app/helpers/urls.js.coffee @@ -0,0 +1,36 @@ +@Travis.Urls = + Repository: + urlGithub: (-> + 'http://github.com/%@'.fmt @get('slug') + ).property('slug'), + + urlGithubWatchers: (-> + 'http://github.com/%@/watchers'.fmt @get('slug') + ).property('slug'), + + urlGithubNetwork: (-> + 'http://github.com/%@/network'.fmt @get('slug') + ).property('slug'), + + urlGithubAdmin: (-> + 'http://github.com/%@/admin/hooks#travis_minibucket'.fmt @get('slug') + ).property('slug') + + statusImage: (-> + '%@.png'.fmt @get('slug') + ).property('slug') + + Commit: + urlAuthor: (-> + 'mailto:%@'.fmt @getPath('commit.author_email') + ).property('commit') + + urlCommitter: (-> + 'mailto:%@'.fmt @getPath('commit.committer_email') + ).property('commit') + + Build: + githubCommit: (-> + 'http://github.com/%@/commit/%@'.fmt @getPath('repository.slug'), @getPath('commit.sha') + ).property('repository.slug', 'commit.sha') + diff --git a/app/assets/javascripts/app/models/artifact.js.coffee b/app/assets/javascripts/app/models/artifact.js.coffee new file mode 100644 index 00000000..59275618 --- /dev/null +++ b/app/assets/javascripts/app/models/artifact.js.coffee @@ -0,0 +1,11 @@ +@Travis.Artifact = Travis.Model.extend + body: DS.attr('string') + +@Travis.Artifact.FIXTURES = [ + { id: 1, body: 'log 1' } + { id: 2, body: 'log 2' } + { id: 3, body: 'log 3' } + { id: 4, body: 'log 4' } + { id: 5, body: 'log 4' } +] + diff --git a/app/assets/javascripts/app/models/branch.js.coffee b/app/assets/javascripts/app/models/branch.js.coffee new file mode 100644 index 00000000..5b270c44 --- /dev/null +++ b/app/assets/javascripts/app/models/branch.js.coffee @@ -0,0 +1,24 @@ +@Travis.Branch = Travis.Model.extend Travis.Helpers, + repository_id: DS.attr('number') + number: DS.attr('number') + branch: DS.attr('string') + message: DS.attr('string') + result: DS.attr('number') + duration: DS.attr('number') + started_at: DS.attr('string') + finished_at: DS.attr('string') + + commit: DS.belongsTo('Travis.Commit') + + repository: (-> + Travis.Repository.find @get('repository_id') if @get('repository_id') + ).property('repository_id').cacheable() + + tick: -> + @notifyPropertyChange 'started_at' + @notifyPropertyChange 'finished_at' + +@Travis.Branch.reopenClass + byRepositoryId: (id) -> + @find repository_id: id + diff --git a/app/assets/javascripts/app/models/build.js.coffee b/app/assets/javascripts/app/models/build.js.coffee index 57a6c266..6e888b43 100644 --- a/app/assets/javascripts/app/models/build.js.coffee +++ b/app/assets/javascripts/app/models/build.js.coffee @@ -1,5 +1,4 @@ -@Travis.Build = Travis.Model.extend # Travis.Helpers, - repository_id: DS.attr('number') +@Travis.Build = Travis.Model.extend state: DS.attr('string') number: DS.attr('number') branch: DS.attr('string') @@ -28,8 +27,8 @@ ).property('data.job_ids.length') isFailureMatrix: (-> - @get('allowedFailureJobs').length > 0 - ).property('allowedFailureJobs') + @getPath('allowedFailureJobs.length') > 0 + ).property('allowedFailureJobs.length') # TODO why does the hasMany association not work? jobs: (-> @@ -37,13 +36,21 @@ ).property('data.job_ids.length') requiredJobs: (-> - @get('jobs').filter (item, index) -> item.get('allow_failure') isnt true + @get('jobs').filter (job) -> job.get('allow_failure') != true ).property('jobs') allowedFailureJobs: (-> - @get('jobs').filter (item, index) -> item.get 'allow_failure' + @get('jobs').filter (job) -> job.get 'allow_failure' ).property('jobs') + configKeys: (-> + config = @get('config') + return [] unless config + keys = $.keys($.only(config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala')) + headers = [I18n.t('build.job'), I18n.t('build.duration'), I18n.t('build.finished_at')] + $.map(headers.concat(keys), (key) -> return $.camelize(key)) + ).property('config') + tick: -> @notifyPropertyChange 'duration' @notifyPropertyChange 'finished_at' @@ -56,8 +63,8 @@ @find(url: '/repositories/' + id + '/builds.json?bare=true&after_number=' + build_number, repository_id: id, orderBy: 'number DESC') @Travis.Build.FIXTURES = [ - { id: 1, repository_id: 1, number: 1, event_type: 'push' }, - { id: 2, repository_id: 1, number: 2, event_type: 'push' }, - { id: 3, repository_id: 2, number: 3, event_type: 'push' }, - { id: 4, repository_id: 3, number: 4, event_type: 'push' } + { id: 1, repository_id: 1, commit_id: 1, job_ids: [1, 2], number: 1, event_type: 'push', config: { rvm: ['rbx', '1.9.3'] }, finished_at: '2012-06-20T00:21:20Z', duration: 35, result: 0 }, + { id: 2, repository_id: 1, commit_id: 2, job_ids: [1], number: 2, event_type: 'push' }, + { id: 3, repository_id: 2, commit_id: 3, job_ids: [2], number: 3, event_type: 'push' }, + { id: 4, repository_id: 3, commit_id: 4, job_ids: [3], number: 4, event_type: 'push' } ] diff --git a/app/assets/javascripts/app/models/commit.js.coffee b/app/assets/javascripts/app/models/commit.js.coffee new file mode 100644 index 00000000..98902d33 --- /dev/null +++ b/app/assets/javascripts/app/models/commit.js.coffee @@ -0,0 +1,19 @@ +@Travis.Commit = Travis.Model.extend + sha: DS.attr('string') + branch: DS.attr('string') + message: DS.attr('string') + compare_url: DS.attr('string') + author_name: DS.attr('string') + author_email: DS.attr('string') + committer_name: DS.attr('string') + committer_email: DS.attr('string') + + build: DS.belongsTo('Travis.Build') + +@Travis.Commit.FIXTURES = [ + { id: 1, sha: '123456', branch: 'master', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' } + { id: 2, sha: '234567', branch: 'feature', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' } + { id: 3, sha: '345678', branch: 'master', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' } + { id: 4, sha: '456789', branch: 'master', message: 'the commit message', compare_url: 'http://github.com/compare', author_name: 'Author', author_email: 'author@email.org', committer_name: 'Committer', committer_email: 'committer@email.org' } +] + diff --git a/app/assets/javascripts/app/models/job.js.coffee b/app/assets/javascripts/app/models/job.js.coffee new file mode 100644 index 00000000..4b74498a --- /dev/null +++ b/app/assets/javascripts/app/models/job.js.coffee @@ -0,0 +1,61 @@ +@Travis.Job = Travis.Model.extend + repository_id: DS.attr('number') + build_id: DS.attr('number') + log_id: DS.attr('number') + queue: DS.attr('string') + state: DS.attr('string') + number: DS.attr('string') + result: DS.attr('number') + duration: DS.attr('number') + started_at: DS.attr('string') + finished_at: DS.attr('string') + allow_failure: DS.attr('boolean') + + repository: DS.belongsTo('Travis.Repository') + commit: DS.belongsTo('Travis.Commit') + build: DS.belongsTo('Travis.Build') + log: DS.belongsTo('Travis.Artifact') + + config: (-> + @getPath 'data.config' + ).property('data.config') + + sponsor: (-> + @getPath('data.sponsor') + ).property('data.sponsor') + + configValues: (-> + config = @get('config') + return [] unless config + $.values($.only(config, 'rvm', 'gemfile', 'env', 'otp_release', 'php', 'node_js', 'scala', 'jdk', 'python', 'perl')) + ).property('config') + + appendLog: (log) -> + @set 'log', @get('log') + log + + subscribe: -> + Travis.app.subscribe 'job-' + @get('id') + + onStateChange: (-> + Travis.app.unsubscribe 'job-' + @get('id') if @get('state') == 'finished' + ).observes('state') + + tick: -> + @notifyPropertyChange 'duration' + @notifyPropertyChange 'finished_at' + +@Travis.Job.reopenClass + queued: (queue) -> + @all() + Travis.store.filter this, (job) -> job.get('queue') == 'builds.' + queue + findMany: (ids) -> + Travis.store.findMany this, ids + +@Travis.Job.FIXTURES = [ + { id: 1, repository_id: 1, build_id: 1, log_id: 1, number: '1.1', config: { rvm: 'rbx' }, finished_at: '2012-06-20T00:21:20Z', duration: 35, result: 0 } + { id: 2, repository_id: 1, build_id: 1, log_id: 2, number: '1.2', config: { rvm: '1.9.3' } } + { id: 3, repository_id: 1, build_id: 2, log_id: 3, number: '2.1' } + { id: 4, repository_id: 2, build_id: 3, log_id: 4, number: '3.1' } + { id: 5, repository_id: 3, build_id: 5, log_id: 5, number: '4.1' } +] + diff --git a/app/assets/javascripts/app/models/repository.js.coffee b/app/assets/javascripts/app/models/repository.js.coffee index ebea1c59..e9508f34 100644 --- a/app/assets/javascripts/app/models/repository.js.coffee +++ b/app/assets/javascripts/app/models/repository.js.coffee @@ -1,5 +1,4 @@ -@Travis.Repository = Travis.Model.extend # Travis.Helpers, - slug: DS.attr('string') +@Travis.Repository = Travis.Model.extend name: DS.attr('string') owner: DS.attr('string') description: DS.attr('string') @@ -9,6 +8,8 @@ last_build_started_at: DS.attr('string') last_build_finished_at: DS.attr('string') + lastBuild: DS.belongsTo('Travis.Build') + builds: (-> Travis.Build.byRepositoryId @get('id'), event_type: 'push' ).property() @@ -17,21 +18,22 @@ Travis.Build.byRepositoryId @get('id'), event_type: 'pull_request' ).property() - lastBuild: (-> - Travis.Build.find @get('last_build_id') - ).property('last_build_id') + slug: (-> + "#{@get('owner')}/#{@get('name')}" + ).property('owner', 'name'), last_build_duration: (-> duration = @getPath('data.last_build_duration') - duration = @durationFrom(@get('last_build_started_at'), @get('last_build_finished_at')) unless duration + duration = Travis.Helpers.durationFrom(@get('last_build_started_at'), @get('last_build_finished_at')) unless duration duration ).property('data.last_build_duration', 'last_build_started_at', 'last_build_finished_at') stats: (-> - return unless Travis.env is 'production' - url = 'https://api.github.com/json/repos/show/' + @get('slug') - @get('_stats') || $.get(url, (data) => @set('_stats', data)) && undefined - ).property('_stats') + @get('_stats') || $.get("https://api.github.com/repos/#{@get('slug')}", (data) => + @set('_stats', data) + @notifyPropertyChange 'stats' + ) && {} + ).property('slug') select: -> Travis.Repository.select(self.get('id')) @@ -59,9 +61,9 @@ repository.set 'selected', repository.get('id') is id @Travis.Repository.FIXTURES = [ - { id: 1, owner: 'travis-ci', name: 'travis-core', build_ids: [1, 2] }, - { id: 2, owner: 'travis-ci', name: 'travis-assets', build_ids: [3] }, - { id: 3, owner: 'travis-ci', name: 'travis-hub', build_ids: [4] }, + { id: 1, owner: 'travis-ci', name: 'travis-core', build_ids: [1, 2], last_build_id: 1, last_build_number: 1, last_build_result: 0 }, + { id: 2, owner: 'travis-ci', name: 'travis-assets', build_ids: [3] , last_build_id: 3, last_build_number: 3}, + { id: 3, owner: 'travis-ci', name: 'travis-hub', build_ids: [4] , last_build_id: 4, last_build_number: 4}, ] diff --git a/app/assets/javascripts/app/models/service_hook.js.coffee b/app/assets/javascripts/app/models/service_hook.js.coffee new file mode 100644 index 00000000..3c33dde1 --- /dev/null +++ b/app/assets/javascripts/app/models/service_hook.js.coffee @@ -0,0 +1,18 @@ +@Travis.ServiceHook = Travis.Model.extend + primaryKey: 'slug' + name: DS.attr('string') + owner_name: DS.attr('string') + active: DS.attr('boolean') + + slug: (-> + [@get('owner_name'), @get('name')].join('/') + ).property() + + toggle: -> + @set 'active', !@get('active') + Travis.app.store.commit() + +@Travis.ServiceHook.reopenClass + url: 'profile/service_hooks' + + diff --git a/app/assets/javascripts/app/models/worker.js.coffee b/app/assets/javascripts/app/models/worker.js.coffee new file mode 100644 index 00000000..43ff9ddc --- /dev/null +++ b/app/assets/javascripts/app/models/worker.js.coffee @@ -0,0 +1,36 @@ +@Travis.WorkerGroup = Ember.ArrayProxy.extend + init: -> + @set('content', []) + + host: (-> + @getPath 'firstObject.host' + ).property() + +@Travis.Worker = Travis.Model.extend + state: DS.attr('string') + name: DS.attr('string') + host: DS.attr('string') + last_seen_at: DS.attr('string') + + isTesting: (-> + @get('state') == 'working' && !!@getPath('payload.config') + ).property('state', 'config') + + number: (-> + @get('name').match(/\d+$/)[0] + ).property('name') + + display: (-> + name = @get('name').replace('travis-', '') + state = @get('state') + payload = @get('payload') + if state == 'working' && payload != undefined + repo = if payload.repository then $.truncate(payload.repository.slug, 18) else undefined + number = if payload.build and payload.build.number then ' #' + payload.build.number else '' + state = if repo then repo + number else state + name + ': ' + state + ).property('state') + + urlJob: (-> + '#!/%@/jobs/%@'.fmt @getPath('payload.repository.slug'), @getPath('payload.build.id') + ).property('payload', 'state') diff --git a/app/assets/javascripts/app/routes.js.coffee b/app/assets/javascripts/app/routes.js.coffee index 96a9a2db..a7e659c1 100644 --- a/app/assets/javascripts/app/routes.js.coffee +++ b/app/assets/javascripts/app/routes.js.coffee @@ -26,7 +26,6 @@ Travis.Router = Em.Router.extend onceLoaded builds, -> router.connectCurrent builds.get('firstObject') - viewCurrent: Ember.Route.transitionTo('current') history: Em.Route.extend route: '/:owner/:name/builds' serialize: (router, repository) -> @@ -39,18 +38,31 @@ Travis.Router = Em.Router.extend onceLoaded builds, -> router.connectHistory builds - viewHistory: Ember.Route.transitionTo('history') build: Em.Route.extend route: '/:owner/:name/builds/:id' serialize: (router, build) -> - router.serializeBuild build + router.serializeObject build connectOutlets: (router, build) -> - params = router.serializeBuild(build) + params = router.serializeObject(build) router.connectLayout params, (repository, build) -> router.connectBuild build + job: Em.Route.extend + route: '/:owner/:name/jobs/:id' + serialize: (router, job) -> + console.log job + router.serializeObject job + + connectOutlets: (router, job) -> + params = router.serializeObject(job) + router.connectLayout params, (repository, job) -> + router.connectJob job + + viewCurrent: Ember.Route.transitionTo('current') + viewHistory: Ember.Route.transitionTo('history') viewBuild: Ember.Route.transitionTo('build') + viewJob: Ember.Route.transitionTo('job') serializeRepository: (repository) -> if repository instanceof DS.Model @@ -58,14 +70,14 @@ Travis.Router = Em.Router.extend else repository or {} - serializeBuild: (build) -> - if build instanceof DS.Model - repository = build.get('repository') + serializeObject: (object) -> + if object instanceof DS.Model + repository = object.get('repository') params = @serializeRepository(repository) $.extend params, - id: build.get('id') + id: object.get('id') else - build or {} + object or {} connectLayout: (params, callback) -> repositories = Travis.Repository.find() @@ -130,3 +142,9 @@ Travis.Router = Em.Router.extend outletName: 'tab' name: 'build' context: build + + connectJob: (job) -> + @get('repositoryController').connectOutlet + outletName: 'tab' + name: 'job' + context: job diff --git a/app/assets/javascripts/app/templates/builds/list.hbs b/app/assets/javascripts/app/templates/builds/list.hbs index e4ac6515..09a6adc3 100644 --- a/app/assets/javascripts/app/templates/builds/list.hbs +++ b/app/assets/javascripts/app/templates/builds/list.hbs @@ -1,6 +1,27 @@ - + + + + + + + + + + + {{#collection tagName="tbody" contentBinding="content" itemViewClass="Travis.BuildsItemView" itemClassBinding="color"}} + {{#with view.content}} + + + + + + {{/with}} + {{/collection}} +
{{t builds.name}}{{t builds.commit}}{{t builds.message}}{{t builds.duration}}{{t builds.finished_at}}
{{number}}{{formatCommit commit}} {{commit.sha}}{{{formatMessage commit.message short="true"}}}{{formatDuration duration}}{{formatTime finished_at}}
+ +

+ +

diff --git a/app/assets/javascripts/app/templates/builds/show.hbs b/app/assets/javascripts/app/templates/builds/show.hbs index f3f8c024..7bd4ce4d 100644 --- a/app/assets/javascripts/app/templates/builds/show.hbs +++ b/app/assets/javascripts/app/templates/builds/show.hbs @@ -1,2 +1,46 @@ -build {{content.id}} +
+
+
+
{{t builds.name}}
+
{{number}}
+
{{t builds.finished_at}}
+
{{formatTime finished_at}}
+
{{t builds.duration}}
+
{{formatDuration duration}}
+
+
+
{{t builds.commit}}
+
{{formatCommit commit}}
+ {{#if commit.compare_url}} +
{{t builds.compare}}
+
{{pathFrom commit.compare_url}}
+ {{/if}} + {{#if commit.author_name}} +
{{t builds.author}}
+
{{commit.author_name}}
+ {{/if}} + {{#if commit.committer_name}} +
{{t builds.committer}}
+
{{commit.committer_name}}
+ {{/if}} +
+ +
{{t builds.message}}
+
{{{formatMessage commit.message}}}
+ + {{#if isMatrix}} + {{else}} +
{{t builds.config}}
+
{{formatConfig config}}
+ {{/if}} +
+ + {{#if isLoaded}} + {{#if isMatrix}} + {{view Travis.JobsView}} + {{else}} + {{view Travis.LogView}} + {{/if}} + {{/if}} +
diff --git a/app/assets/javascripts/app/templates/jobs/list.hbs b/app/assets/javascripts/app/templates/jobs/list.hbs new file mode 100644 index 00000000..b8f8e996 --- /dev/null +++ b/app/assets/javascripts/app/templates/jobs/list.hbs @@ -0,0 +1,70 @@ + + + + + {{#each configKeys}} + + {{/each}} + + + + {{#each requiredJobs}} + + + + + {{#each configValues}} + + {{/each}} + + {{/each}} + +
{{t jobs.build_matrix}}
{{this}}
{{number}}{{formatDuration duration}}{{formatTime finished_at}}{{this}}
+ +{{#if isFailureMatrix}} + + + + + {{#each configKeys}} + + {{/each}} + + + + {{#each allowedFailureJobs}} + + + + + {{#each configValues}} + + {{/each}} + + {{/each}} + +
+ {{t jobs.allowed_failures}}{{whats_this allow_failure_help}} +
{{this}}
{{number}}{{formatDuration duration}}{{formatTime finished_at}}{{this}}
+ +
+
{{t "jobs.allowed_failures"}}
+
+

+ Allowed Failures are items in your build matrix that are allowed to + fail without causing the entire build to be shown as failed. This lets you add + in experimental and preparatory builds to test against versions or + configurations that you are not ready to officially support. +

+

+ You can define allowed failures in the build matrix as follows: +

+
+    matrix:
+      allow_failures:
+        - rvm: ruby-head
+      
+
+
+{{/if}} + diff --git a/app/assets/javascripts/app/templates/jobs/log.hbs b/app/assets/javascripts/app/templates/jobs/log.hbs new file mode 100644 index 00000000..24a87c2d --- /dev/null +++ b/app/assets/javascripts/app/templates/jobs/log.hbs @@ -0,0 +1,11 @@ +{{! ugh ... }} +{{#with jobs.firstObject}} +
{{{formatLog log.body}}}
+ + {{#if sponsor.name}} + + {{/if}} +{{/with}} diff --git a/app/assets/javascripts/app/templates/jobs/show.hbs b/app/assets/javascripts/app/templates/jobs/show.hbs new file mode 100644 index 00000000..759b9672 --- /dev/null +++ b/app/assets/javascripts/app/templates/jobs/show.hbs @@ -0,0 +1,37 @@ +
+
+
+
Job
+
{{number}}
+
{{t jobs.finished_at}}
+
{{formatTime finished_at}}
+
{{t jobs.duration}}
+
{{formatDuration duration}}
+
+ +
+
{{t jobs.commit}}
+
{{formatCommit commit}}
+ {{#if commit.compare_url}} +
{{t jobs.compare}}
+
{{pathFrom commit.compare_url}}
+ {{/if}} + {{#if commit.author_name}} +
{{t jobs.author}}
+
{{commit.author_name}}
+ {{/if}} + {{#if commit.committer_name}} +
{{t jobs.committer}}
+
{{commit.committer_name}}
+ {{/if}} +
+ +
{{t jobs.message}}
+
{{formatMessage commit.message}}
+
{{t jobs.config}}
+
{{formatConfig config}}
+
+ + {{view Travis.LogView}} +
+ diff --git a/app/assets/javascripts/app/templates/repositories/list.hbs b/app/assets/javascripts/app/templates/repositories/list.hbs index 5ab23480..51cfaccb 100644 --- a/app/assets/javascripts/app/templates/repositories/list.hbs +++ b/app/assets/javascripts/app/templates/repositories/list.hbs @@ -1,6 +1,22 @@ - +{{#collection tagName="ul" id="repositories" contentBinding="content" itemViewClass="Travis.RepositoriesItemView" itemClassBinding="classes"}} + {{#with view.content}} +
+ {{slug}} + #{{last_build_number}} +

+ {{t repositories.duration}}: + {{formatDuration last_build_duration}}, + {{t repositories.finished_at}}: + {{formatTime last_build_finished_at}} +

+ {{#if description}} +

{{description}}

+ {{/if}} + +
+ {{/with}} +{{/collection}} +{{^collection contentBinding="repositories" id="list" class="loading"}} +

+{{/collection}} diff --git a/app/assets/javascripts/app/templates/repositories/show.hbs b/app/assets/javascripts/app/templates/repositories/show.hbs index 1fb4ce61..bf1c0f73 100644 --- a/app/assets/javascripts/app/templates/repositories/show.hbs +++ b/app/assets/javascripts/app/templates/repositories/show.hbs @@ -1,4 +1,14 @@ -

{{content.owner}}/{{content.name}}

+

+ {{slug}} +

+ +

{{description}}

+ + {{outlet tabs}} diff --git a/app/assets/javascripts/app/templates/repositories/tabs.hbs b/app/assets/javascripts/app/templates/repositories/tabs.hbs index 4a2b565b..acddf8bd 100644 --- a/app/assets/javascripts/app/templates/repositories/tabs.hbs +++ b/app/assets/javascripts/app/templates/repositories/tabs.hbs @@ -1,5 +1,10 @@ diff --git a/app/assets/javascripts/app/views.js.coffee b/app/assets/javascripts/app/views.js.coffee index b3b2b400..b355d220 100644 --- a/app/assets/javascripts/app/views.js.coffee +++ b/app/assets/javascripts/app/views.js.coffee @@ -1,9 +1,26 @@ Travis.ApplicationView = Em.View.extend templateName: 'application' Travis.RepositoriesView = Em.View.extend templateName: 'repositories/list' -Travis.RepositoryView = Em.View.extend templateName: 'repositories/show' -Travis.TabsView = Em.View.extend templateName: 'repositories/tabs' -Travis.CurrentView = Em.View.extend templateName: 'builds/show' -Travis.HistoryView = Em.View.extend templateName: 'builds/list' -Travis.BuildView = Em.View.extend templateName: 'builds/show' -Travis.LoadingView = Em.View.extend templateName: 'loading' + +Travis.RepositoriesItemView = Em.View.extend + classes: (-> + color = Travis.Helpers.colorForResult(@getPath('content.last_build_result')) + classes = ['repository', color] + classes.push 'selected' if @getPath('content.selected') + classes.join(' ') + ).property('content.last_build_result', 'content.selected') + +Travis.RepositoryView = Em.View.extend templateName: 'repositories/show' +Travis.TabsView = Em.View.extend templateName: 'repositories/tabs' +Travis.HistoryView = Em.View.extend templateName: 'builds/list' + +Travis.BuildsItemView = Em.View.extend + classes: (-> + Travis.Helpers.colorForResult(@getPath('content.result')) + ).property('content.result') + +Travis.CurrentView = Travis.BuildView = Em.View.extend templateName: 'builds/show' +Travis.LoadingView = Em.View.extend templateName: 'loading' +Travis.JobsView = Em.View.extend templateName: 'jobs/list' +Travis.JobView = Em.View.extend templateName: 'jobs/show' +Travis.LogView = Em.View.extend templateName: 'jobs/log' diff --git a/app/assets/javascripts/config/i18n.js b/app/assets/javascripts/config/i18n.js new file mode 100644 index 00000000..eca9d6ee --- /dev/null +++ b/app/assets/javascripts/config/i18n.js @@ -0,0 +1,3 @@ +var I18n = I18n || {}; +I18n.translations = {"ca":{"locales":{"en":"English","es":"Español","fr":"Français","ja":"日本語","nb":"Norsk Bokmål","nl":"Nederlands","pl":"Polski","pt-BR":"português brasileiro","ru":"Русский"}},"en":{"errors":{"messages":{"not_found":"not found","already_confirmed":"was already confirmed","not_locked":"was not locked"}},"devise":{"failure":{"unauthenticated":"You need to sign in or sign up before continuing.","unconfirmed":"You have to confirm your account before continuing.","locked":"Your account is locked.","invalid":"Invalid email or password.","invalid_token":"Invalid authentication token.","timeout":"Your session expired, please sign in again to continue.","inactive":"Your account was not activated yet."},"sessions":{"signed_in":"Signed in successfully.","signed_out":"Signed out successfully."},"passwords":{"send_instructions":"You will receive an email with instructions about how to reset your password in a few minutes.","updated":"Your password was changed successfully. You are now signed in."},"confirmations":{"send_instructions":"You will receive an email with instructions about how to confirm your account in a few minutes.","confirmed":"Your account was successfully confirmed. You are now signed in."},"registrations":{"signed_up":"You have signed up successfully. If enabled, a confirmation was sent to your e-mail.","updated":"You updated your account successfully.","destroyed":"Bye! Your account was successfully cancelled. We hope to see you again soon."},"unlocks":{"send_instructions":"You will receive an email with instructions about how to unlock your account in a few minutes.","unlocked":"Your account was successfully unlocked. You are now signed in."},"mailer":{"confirmation_instructions":{"subject":"Confirmation instructions"},"reset_password_instructions":{"subject":"Reset password instructions"},"unlock_instructions":{"subject":"Unlock Instructions"}}},"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} hour","other":"%{count} hours"},"minutes_exact":{"one":"%{count} minute","other":"%{count} minutes"},"seconds_exact":{"one":"%{count} second","other":"%{count} seconds"}}},"workers":"Workers","queue":"Queue","no_job":"There are no jobs","repositories":{"branch":"Branch","image_url":"Image URL","markdown":"Markdown","textile":"Textile","rdoc":"RDOC","commit":"Commit","message":"Message","started_at":"Started","duration":"Duration","finished_at":"Finished","tabs":{"current":"Current","build_history":"Build History","branches":"Branch Summary","pull_requests":"Pull Requests","build":"Build","job":"Job"}},"build":{"job":"Job","duration":"Duration","finished_at":"Finished"},"jobs":{"messages":{"sponsored_by":"This test suite was run on a worker box sponsored by"},"build_matrix":"Build Matrix","allowed_failures":"Allowed Failures","author":"Author","config":"Config","compare":"Compare","committer":"Committer","branch":"Branch","commit":"Commit","message":"Message","started_at":"Started","duration":"Duration","finished_at":"Finished"},"builds":{"name":"Build","messages":{"sponsored_by":"This test suite was run on a worker box sponsored by"},"build_matrix":"Build Matrix","allowed_failures":"Allowed Failures","author":"Author","config":"Config","compare":"Compare","committer":"Committer","branch":"Branch","commit":"Commit","message":"Message","started_at":"Started","duration":"Duration","finished_at":"Finished","show_more":"Show more"},"layouts":{"top":{"home":"Home","blog":"Blog","docs":"Docs","stats":"Stats","github_login":"Sign in with Github","profile":"Profile","sign_out":"Sign Out","admin":"Admin"},"application":{"fork_me":"Fork me on Github","recent":"Recent","search":"Search","sponsers":"Sponsors","sponsors_link":"See all of our amazing sponsors →","my_repositories":"My Repositories"},"about":{"alpha":"This stuff is alpha.","messages":{"alpha":"Please do not consider this a stable service. We're still far from that! More info here."},"join":"Join us and help!","mailing_list":"Mailing List","repository":"Repository","twitter":"Twitter"},"mobile":{"author":"Author","build":"Build","build_matrix":"Build Matrix","commit":"Commit","committer":"Committer","compare":"Compare","config":"Config","duration":"Duration","finished_at":"Finished at","job":"Job","log":"Log"}},"profiles":{"show":{"email":"Email","github":"Github","message":{"your_repos":" Flick the switches below to turn on the Travis service hook for your projects, then push to GitHub.
\n To test against multiple rubies, see","config":"how to configure custom build options"},"messages":{"notice":"To get started, please read our Getting Started guide.\n It will only take a couple of minutes."},"token":"Token","your_repos":"Your Repositories","update":"Update","update_locale":"Update","your_locale":"Your Locale"}},"statistics":{"index":{"count":"Count","repo_growth":"Repository Growth","total_projects":"Total Projects/Repositories","build_count":"Build Count","last_month":"last month","total_builds":"Total Builds"}},"locales":{"en":"English","es":"Español","fr":"Français","ja":"日本語","nb":"Norsk Bokmål","pl":"Polski","ru":"Русский","nl":"Nederlands","pt-BR":"português brasileiro"}},"es":{"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} hora","other":"%{count} horas"},"minutes_exact":{"one":"%{count} minuto","other":"%{count} minutos"},"seconds_exact":{"one":"%{count} segundo","other":"%{count} segundos"}}},"workers":"Procesos","queue":"Cola","no_job":"No hay trabajos","repositories":{"branch":"Rama","image_url":"Imagen URL","markdown":"Markdown","textile":"Textile","rdoc":"RDOC","commit":"Commit","message":"Mensaje","started_at":"Iniciado","duration":"Duración","finished_at":"Finalizado","tabs":{"current":"Actual","build_history":"Histórico","branches":"Ramas","build":"Builds","job":"Trabajo"}},"build":{"job":"Trabajo","duration":"Duración","finished_at":"Finalizado"},"jobs":{"messages":{"sponsored_by":"Esta serie de tests han sido ejecutados en una caja de Proceso patrocinada por"},"build_matrix":"Matriz de Builds","allowed_failures":"Fallos Permitidos","author":"Autor","config":"Configuración","compare":"Comparar","committer":"Committer","branch":"Rama","commit":"Commit","message":"Mensaje","started_at":"Iniciado","duration":"Duración","finished_at":"Finalizado","sponsored_by":"Patrocinado por"},"builds":{"name":"Build","messages":{"sponsored_by":"Esta serie de tests han sido ejecutados en una caja de Proceso patrocinada por"},"build_matrix":"Matriz de Builds","allowed_failures":"Fallos Permitidos","author":"Autor","config":"Configuración","compare":"Comparar","committer":"Committer","branch":"Rama","commit":"Commit","message":"Mensaje","started_at":"Iniciado","duration":"Duración","finished_at":"Finalizado"},"layouts":{"top":{"home":"Inicio","blog":"Blog","docs":"Documentación","stats":"Estadísticas","github_login":"Iniciar sesión con Github","profile":"Perfil","sign_out":"Desconectar","admin":"Admin"},"application":{"fork_me":"Hazme un Fork en Github","recent":"Reciente","search":"Buscar","sponsers":"Patrocinadores","sponsors_link":"Ver todos nuestros patrocinadores →","my_repositories":"Mis Repositorios"},"about":{"alpha":"Esto es alpha.","messages":{"alpha":"Por favor no considereis esto un servicio estable. Estamos estamos aún lejos de ello! Más información aquí."},"join":"Únetenos y ayudanos!","mailing_list":"Lista de Correos","repository":"Repositorio","twitter":"Twitter"}},"profiles":{"show":{"email":"Correo electrónico","github":"Github","message":{"your_repos":" Activa los interruptores para inicial el Travis service hook para tus proyectos, y haz un Push en GitHub.
\n Para probar varias versiones de ruby, mira","config":"como configurar tus propias opciones para el Build"},"messages":{"notice":"Para comenzar, por favor lee nuestra Guía de Inicio .\n Solo tomará unos pocos minutos."},"token":"Token","your_repos":"Tus repositorios","update":"Actualizar","update_locale":"Actualizar","your_locale":"Tu Idioma"}},"statistics":{"index":{"count":"Número","repo_growth":"Crecimiento de Repositorios","total_projects":"Total de Proyectos/Repositorios","build_count":"Número de Builds","last_month":"mes anterior","total_builds":"Total de Builds"}},"locales":{"en":"English","es":"Español","fr":"Français","ja":"日本語","nb":"Norsk Bokmål","pl":"Polski","ru":"Русский","nl":"Nederlands","pt-BR":"português brasileiro"}},"fr":{"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} heure","other":"%{count} heures"},"minutes_exact":{"one":"%{count} minute","other":"%{count} minutes"},"seconds_exact":{"one":"%{count} seconde","other":"%{count} secondes"}}},"workers":"Processus","queue":"File","no_job":"Pas de tâches","repositories":{"branch":"Branche","image_url":"Image","markdown":"Markdown","textile":"Textile","rdoc":"RDOC","commit":"Commit","message":"Message","started_at":"Commencé","duration":"Durée","finished_at":"Terminé","tabs":{"current":"Actuel","build_history":"Historique des tâches","branches":"Résumé des branches","build":"Construction","job":"Tâche"}},"build":{"job":"Tâche","duration":"Durée","finished_at":"Terminé"},"jobs":{"messages":{"sponsored_by":"Cette série de tests a été exécutée sur une machine sponsorisée par"},"build_matrix":"Matrice des versions","allowed_failures":"Échecs autorisés","author":"Auteur","config":"Config","compare":"Comparer","committer":"Committeur","branch":"Branche","commit":"Commit","message":"Message","started_at":"Commencé","duration":"Durée","finished_at":"Terminé","sponsored_by":"Cette série de tests a été exécutée sur une machine sponsorisée par"},"builds":{"name":"Version","messages":{"sponsored_by":"Cette série de tests a été exécutée sur une machine sponsorisée par"},"build_matrix":"Matrice des versions","allowed_failures":"Échecs autorisés","author":"Auteur","config":"Config","compare":"Comparer","committer":"Committeur","branch":"Branche","commit":"Commit","message":"Message","started_at":"Commencé","duration":"Durée","finished_at":"Terminé"},"layouts":{"top":{"home":"Accueil","blog":"Blog","docs":"Documentation","stats":"Statistiques","github_login":"Connection Github","profile":"Profil","sign_out":"Déconnection","admin":"Admin"},"application":{"fork_me":"Faites un Fork sur Github","recent":"Récent","search":"Chercher","sponsers":"Sponsors","sponsors_link":"Voir tous nos extraordinaire sponsors →","my_repositories":"Mes dépôts"},"about":{"alpha":"Ceci est en alpha.","messages":{"alpha":"S'il vous plaît ne considérez pas ce service comme étant stable. Nous sommes loin de ça! Plus d'infos ici."},"join":"Joignez-vous à nous et aidez-nous!","mailing_list":"Liste de distribution","repository":"Dépôt","twitter":"Twitter"},"mobile":{"author":"Auteur","build":"Version","build_matrix":"Matrice des versions","commit":"Commit","committer":"Committeur","compare":"Comparer","config":"Config","duration":"Durée","finished_at":"Terminé à","job":"Tâche","log":"Journal"}},"profiles":{"show":{"github":"Github","message":{"your_repos":"Utilisez les boutons ci-dessous pour activer Travis sur vos projets puis déployez sur GitHub.
\nPour tester sur plus de versions de ruby, voir","config":"comment configurer des options de version personnalisées"},"messages":{"notice":"Pour commencer, veuillez lire notre guide de démarrage.\n Cela ne vous prendra que quelques minutes."},"token":"Jeton","your_repos":"Vos dépôts","email":"Courriel","update":"Modifier","update_locale":"Modifier","your_locale":"Votre langue"}},"statistics":{"index":{"count":"Décompte","repo_growth":"Croissance de dépôt","total_projects":"Total des projets/dépôts","build_count":"Décompte des versions","last_month":"mois dernier","total_builds":"Total des versions"}},"admin":{"actions":{"create":"créer","created":"créé","delete":"supprimer","deleted":"supprimé","update":"mise à jour","updated":"mis à jour"},"credentials":{"log_out":"Déconnection"},"delete":{"confirmation":"Oui, je suis sure","flash_confirmation":"%{name} a été détruit avec succès"},"flash":{"error":"%{name} n'a pas pu être %{action}","noaction":"Aucune action n'a été entreprise","successful":"%{name} a réussi à %{action}"},"history":{"name":"Historique","no_activity":"Aucune activité","page_name":"Historique pour %{name}"},"list":{"add_new":"Ajouter un nouveau","delete_action":"Supprimer","delete_selected":"Supprimer la sélection","edit_action":"Modifier","search":"Rechercher","select":"Sélectionner le %{name} à modifier","select_action":"Sélectionner","show_all":"Montrer tout"},"new":{"basic_info":"Information de base","cancel":"Annuler","chosen":"%{name} choisi","chose_all":"Choisir tout","clear_all":"Déselectionner tout","many_chars":"caractères ou moins","one_char":"caractère.","optional":"Optionnel","required":"Requis","save":"Sauvegarder","save_and_add_another":"Sauvegarder et en ajouter un autre","save_and_edit":"Sauvegarder et modifier","select_choice":"Faites vos choix et cliquez"},"dashboard":{"add_new":"Ajouter un nouveau","last_used":"Dernière utilisation","model_name":"Nom du modèle","modify":"Modification","name":"Tableau de bord","pagename":"Administration du site","records":"Enregistrements","show":"Voir","ago":"plus tôt"}},"home":{"name":"accueil"},"repository":{"duration":"Durée"},"devise":{"confirmations":{"confirmed":"Votre compte a été crée avec succès. Vous être maintenant connecté.","send_instructions":"Vous allez recevoir un courriel avec les instructions de confirmation de votre compte dans quelques minutes."},"failure":{"inactive":"Votre compte n'a pas encore été activé.","invalid":"Adresse courriel ou mot de passe invalide.","invalid_token":"Jeton d'authentification invalide.","locked":"Votre compte est bloqué.","timeout":"Votre session est expirée, veuillez vous reconnecter pour continuer.","unauthenticated":"Vous devez vous connecter ou vous enregistrer afin de continuer","unconfirmed":"Vous devez confirmer votre compte avant de continuer."},"mailer":{"confirmation_instructions":{"subject":"Instructions de confirmations"},"reset_password_instructions":{"subject":"Instruction de remise à zéro du mot de passe"},"unlock_instructions":{"subject":"Instruction de débloquage"}},"passwords":{"send_instructions":"Vous recevrez un courriel avec les instructions de remise à zéro du mot de passe dans quelques minutes.","updated":"Votre mot de passe a été changé avec succès. Vous êtes maintenant connecté."},"registrations":{"destroyed":"Au revoir! Votre compte a été annulé avec succès. Nous espérons vous revoir bientôt.","signed_up":"Vous êtes enregistré avec succès. Si activé, une confirmation vous a été envoyé par courriel.","updated":"Votre compte a été mis a jour avec succès"},"sessions":{"signed_in":"Connecté avec succès","signed_out":"Déconnecté avec succès"},"unlocks":{"send_instructions":"Vous recevrez un courriel contenant les instructions pour débloquer votre compte dans quelques minutes.","unlocked":"Votre compte a été débloqué avec succès."}},"errors":{"messages":{"already_confirmed":"étais déja confirmé","not_found":"n'a pas été trouvé","not_locked":"n'étais pas bloqué"}},"locales":{"en":"English","es":"Español","ja":"日本語","ru":"Русский","fr":"Français","nb":"Norsk Bokmål","pl":"Polski","nl":"Nederlands","pt-BR":"português brasileiro"}},"ja":{"workers":"ワーカー","queue":"キュー","no_job":"ジョブはありません","repositories":{"branch":"ブランチ","image_url":"画像URL","markdown":".md","textile":".textile","rdoc":".rdoc","commit":"コミット","message":"メッセージ","started_at":"開始時刻","duration":"処理時間","finished_at":"終了時刻","tabs":{"current":"最新","build_history":"ビルド履歴","branches":"ブランチまとめ","build":"ビルド","job":"ジョブ"}},"build":{"job":"ジョブ","duration":"処理時間","finished_at":"終了時刻"},"jobs":{"messages":{"sponsored_by":"このテストは以下のスポンサーの協力で行いました。"},"build_matrix":"ビルドマトリクス","allowed_failures":"失敗許容範囲内","author":"制作者","config":"設定","compare":"比較","committer":"コミット者","branch":"ブランチ","commit":"コミット","message":"メッセージ","started_at":"開始時刻","duration":"処理時間","finished_at":"終了時刻"},"builds":{"name":"ビルド","messages":{"sponsored_by":"このテストは以下のスポンサーの協力で行いました。"},"build_matrix":"失敗許容範囲外","allowed_failures":"失敗許容範囲内","author":"制作者","config":"設定","compare":"比較","committer":"コミット者","branch":"ブランチ","commit":"コミット","message":"メッセージ","started_at":"開始時刻","duration":"処理時間","finished_at":"終了時刻"},"layouts":{"about":{"alpha":"まだアルファですよ!","join":"参加してみよう!","mailing_list":"メールリスト","messages":{"alpha":"Travis-ciは安定したサービスまで後一歩!詳しくはこちら"},"repository":"リポジトリ","twitter":"ツイッター"},"application":{"fork_me":"Githubでフォークしよう","my_repositories":"マイリポジトリ","recent":"最近","search":"検索","sponsers":"スポンサー","sponsors_link":"スポンサーをもっと見る →"},"top":{"blog":"ブログ","docs":"Travisとは?","github_login":"Githubでログイン","home":"ホーム","profile":"プロフィール","sign_out":"ログアウト","stats":"統計","admin":"管理"},"mobile":{"author":"制作者","build":"ビルド","build_matrix":"ビルドマトリクス","commit":"コミット","committer":"コミット者","compare":"比較","config":"設定","duration":"処理時間","finished_at":"終了時刻","job":"ジョブ","log":"ログ"}},"profiles":{"show":{"github":"Github","email":"メール","message":{"config":"詳細設定","your_repos":"以下のスイッチを設定し、Travis-ciを有効にします。Githubへプッシュしたらビルドは自動的に開始します。複数バーションや細かい設定はこちらへ:"},"messages":{"notice":"まずはTravisのはじめ方を参照してください。"},"token":"トークン","your_repos":"リポジトリ","update":"更新","update_locale":"更新","your_locale":"言語設定"}},"statistics":{"index":{"build_count":"ビルド数","count":"数","last_month":"先月","repo_growth":"リポジトリ","total_builds":"合計ビルド数","total_projects":"合計リポジトリ"}},"locales":{"en":"English","es":"Español","fr":"Français","ja":"日本語","nb":"Norsk Bokmål","pl":"Polski","ru":"Русский","nl":"Nederlands","pt-BR":"português brasileiro"}},"nb":{"admin":{"actions":{"create":"opprett","created":"opprettet","delete":"slett","deleted":"slettet","update":"oppdater","updated":"oppdatert"},"credentials":{"log_out":"Logg ut"},"dashboard":{"add_new":"Legg til ny","ago":"siden","last_used":"Sist brukt","model_name":"Modell","modify":"Rediger","name":"Dashbord","pagename":"Nettstedsadministrasjon","records":"Oppføringer","show":"Vis"},"delete":{"confirmation":"Ja, jeg er sikker","flash_confirmation":"%{name} ble slettet"},"flash":{"error":"%{name} kunne ikke bli %{action}","noaction":"Ingen handlinger ble utført","successful":"%{name} ble %{action}"},"history":{"name":"Logg","no_activity":"Ingen aktivitet","page_name":"Logg for %{name}"},"list":{"add_new":"Legg til ny","delete_action":"Slett","delete_selected":"Slett valgte","edit_action":"Rediger","search":"Søk","select":"Velg %{name} for å redigere","select_action":"Velg","show_all":"Vis alle "},"new":{"basic_info":"Basisinformasjon","cancel":"Avbryt","chosen":"Valgt %{name}","chose_all":"Velg alle","clear_all":"Fjern alle","many_chars":"eller færre tegn.","one_char":"tegn.","optional":"Valgfri","required":"Påkrevd","save":"Lagre","save_and_add_another":"Lagre og legg til ny","save_and_edit":"Lagre og rediger","select_choice":"Kryss av for dine valg og klikk"}},"build":{"duration":"Varighet","finished_at":"Fullført","job":"Jobb"},"builds":{"allowed_failures":"Tillatte feil","author":"Forfatter","branch":"Gren","build_matrix":"Jobbmatrise","commit":"Innsending","committer":"Innsender","compare":"Sammenlign","config":"Oppsett","duration":"Varighet","finished_at":"Fullført","message":"Beskrivelse","messages":{"sponsored_by":"Denne testen ble kjørt på en maskin sponset av"},"name":"Jobb","started_at":"Startet"},"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} time","other":"%{count} timer"},"minutes_exact":{"one":"%{count} minutt","other":"%{count} minutter"},"seconds_exact":{"one":"%{count} sekund","other":"%{count} sekunder"}}},"devise":{"confirmations":{"confirmed":"Din konto er aktivert og du er nå innlogget.","send_instructions":"Om noen få minutter så vil du få en e-post med informasjon om hvordan du bekrefter kontoen din."},"failure":{"inactive":"Kontoen din har ikke blitt aktivert enda.","invalid":"Ugyldig e-post eller passord.","invalid_token":"Ugyldig autentiseringskode.","locked":"Kontoen din er låst.","timeout":"Du ble logget ut siden på grunn av mangel på aktivitet, vennligst logg inn på nytt.","unauthenticated":"Du må logge inn eller registrere deg for å fortsette.","unconfirmed":"Du må bekrefte kontoen din før du kan fortsette."},"mailer":{"confirmation_instructions":{"subject":"Bekreftelsesinformasjon"},"reset_password_instructions":{"subject":"Instruksjoner for å få nytt passord"},"unlock_instructions":{"subject":"Opplåsningsinstruksjoner"}},"passwords":{"send_instructions":"Om noen få minutter så vil du få en epost med informasjon om hvordan du kan få et nytt passord.","updated":"Passordet ditt ble endret, og du er logget inn."},"registrations":{"destroyed":"Adjø! Kontoen din ble kansellert. Vi håper vi ser deg igjen snart.","signed_up":"Du er nå registrert.","updated":"Kontoen din ble oppdatert."},"sessions":{"signed_in":"Du er nå logget inn.","signed_out":"Du er nå logget ut."},"unlocks":{"send_instructions":"Om noen få minutter så kommer du til å få en e-post med informasjon om hvordan du kan låse opp kontoen din.","unlocked":"Kontoen din ble låst opp, og du er nå logget inn igjen."}},"errors":{"messages":{"already_confirmed":"har allerede blitt bekreftet","not_found":"ikke funnnet","not_locked":"var ikke låst"}},"home":{"name":"hjem"},"jobs":{"allowed_failures":"Tillatte feil","author":"Forfatter","branch":"Gren","build_matrix":"Jobbmatrise","commit":"Innsending","committer":"Innsender","compare":"Sammenlign","config":"Oppsett","duration":"Varighet","finished_at":"Fullført","message":"Beskrivelse","messages":{"sponsored_by":"Denne testserien ble kjørt på en maskin sponset av"},"started_at":"Startet"},"layouts":{"about":{"alpha":"Dette er alfa-greier.","join":"Bli med og hjelp oss!","mailing_list":"E-postliste","messages":{"alpha":"Dette er ikke en stabil tjeneste. Vi har fremdeles et stykke igjen! Mer informasjon finner du her."},"repository":"Kodelager","twitter":"Twitter."},"application":{"fork_me":"Se koden på Github","my_repositories":"Mine kodelagre","recent":"Nylig","search":"Søk","sponsers":"Sponsorer","sponsors_link":"Se alle de flotte sponsorene våre →"},"mobile":{"author":"Forfatter","build":"Jobb","build_matrix":"Jobbmatrise","commit":"Innsending","committer":"Innsender","compare":"Sammenlign","config":"Oppsett","duration":"Varighet","finished_at":"Fullført","job":"Jobb","log":"Logg"},"top":{"admin":"Administrator","blog":"Blogg","docs":"Dokumentasjon","github_login":"Logg inn med Github","home":"Hjem","profile":"Profil","sign_out":"Logg ut","stats":"Statistikk"}},"no_job":"Ingen jobber finnnes","profiles":{"show":{"email":"E-post","github":"Github","message":{"config":"hvordan sette opp egne jobbinnstillinger","your_repos":"Slå\u0010 på Travis for prosjektene dine ved å dra i bryterne under, og send koden til Github.
\nFor å teste mot flere ruby-versjoner, se dokumentasjonen for"},"messages":{"notice":"For å komme i gang, vennligst les kom-i-gang-veivisereren vår. Det tar bare et par minutter."},"token":"Kode","update":"Oppdater","update_locale":"Oppdater","your_locale":"Ditt språk","your_repos":"Dine kodelagre"}},"queue":"Kø","repositories":{"branch":"Gren","commit":"Innsender","duration":"Varighet","finished_at":"Fullført","image_url":"Bilde-URL","markdown":"Markdown","message":"Beskrivelse","rdoc":"RDOC","started_at":"Startet","tabs":{"branches":"Grensammendrag","build":"Jobb","build_history":"Jobblogg","current":"Siste","job":"Jobb"},"textile":"Textile"},"repository":{"duration":"Varighet"},"statistics":{"index":{"build_count":"Antall jobber","count":"Antall","last_month":"siste måned","repo_growth":"Vekst i kodelager","total_builds":"Totale jobber","total_projects":"Antall prosjekter/kodelagre"}},"workers":"Arbeidere","locales":{"en":"English","es":"Español","ja":"日本語","ru":"Русский","fr":"Français","nb":"Norsk Bokmål","pl":"Polski","nl":"Nederlands","pt-BR":"português brasileiro"}},"nl":{"admin":{"actions":{"create":"aanmaken","created":"aangemaakt","delete":"verwijderen","deleted":"verwijderd","update":"bijwerken","updated":"bijgewerkt"},"credentials":{"log_out":"Afmelden"},"dashboard":{"add_new":"Nieuwe toevoegen","ago":"geleden","last_used":"Laatst gebruikt","model_name":"Model naam","modify":"Wijzigen","pagename":"Site administratie","show":"Laten zien","records":"Gegevens"},"delete":{"confirmation":"Ja, ik ben zeker","flash_confirmation":"%{name} is vernietigd"},"flash":{"error":"%{name} kon niet worden %{action}","noaction":"Er zijn geen acties genomen","successful":"%{name} is %{action}"},"history":{"name":"Geschiedenis","no_activity":"Geen activiteit","page_name":"Geschiedenis van %{name}"},"list":{"add_new":"Nieuwe toevoegen","delete_action":"Verwijderen","delete_selected":"Verwijder geselecteerden","edit_action":"Bewerken","search":"Zoeken","select":"Selecteer %{name} om te bewerken","select_action":"Selecteer","show_all":"Laat allen zien"},"new":{"basic_info":"Basisinfo","cancel":"Annuleren","chosen":"%{name} gekozen","chose_all":"Kies allen","clear_all":"Deselecteer allen","many_chars":"tekens of minder.","one_char":"teken.","optional":"Optioneel","required":"Vereist","save":"Opslaan","save_and_add_another":"Opslaan en een nieuwe toevoegen","save_and_edit":"Opslaan en bewerken","select_choice":"Selecteer uw keuzes en klik"}},"build":{"duration":"Duur","finished_at":"Voltooid","job":"Taak"},"builds":{"allowed_failures":"Toegestane mislukkingen","author":"Auteur","branch":"Tak","build_matrix":"Bouw Matrix","compare":"Vergelijk","config":"Configuratie","duration":"Duur","finished_at":"Voltooid","message":"Bericht","messages":{"sponsored_by":"Deze tests zijn gedraaid op een machine gesponsord door"},"name":"Bouw","started_at":"Gestart","commit":"Commit","committer":"Committer"},"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} uur","other":"%{count} uren"},"minutes_exact":{"one":"%{count} minuut","other":"%{count} minuten"},"seconds_exact":{"one":"%{count} seconde","other":"%{count} seconden"}}},"devise":{"confirmations":{"confirmed":"Uw account is bevestigd. U wordt nu ingelogd.","send_instructions":"Binnen enkele minuten zal u een email ontvangen met instructies om uw account te bevestigen."},"failure":{"inactive":"Uw account is nog niet geactiveerd.","invalid":"Ongeldig email adres of wachtwoord.","invalid_token":"Ongeldig authenticatie token.","locked":"Uw account is vergrendeld.","timeout":"Uw sessie is verlopen, gelieve opnieuw in te loggen om verder te gaan.","unauthenticated":"U moet inloggen of u registeren voordat u verder gaat.","unconfirmed":"U moet uw account bevestigen voordat u verder gaat."},"mailer":{"confirmation_instructions":{"subject":"Bevestigings-instructies"},"reset_password_instructions":{"subject":"Wachtwoord herstel instructies"},"unlock_instructions":{"subject":"Ontgrendel-instructies"}},"passwords":{"send_instructions":"Binnen enkele minuten zal u een email krijgen met instructies om uw wachtwoord opnieuw in te stellen.","updated":"Uw wachtwoord is veranderd. U wordt nu ingelogd."},"registrations":{"destroyed":"Dag! Uw account is geannuleerd. We hopen u vlug terug te zien.","signed_up":"Uw registratie is voltooid. Als het ingeschakeld is wordt een bevestiging naar uw email adres verzonden.","updated":"Het bijwerken van uw account is gelukt."},"sessions":{"signed_in":"Inloggen gelukt.","signed_out":"Uitloggen gelukt."},"unlocks":{"send_instructions":"Binnen enkele minuten zal u een email krijgen met instructies om uw account te ontgrendelen.","unlocked":"Uw account is ontgrendeld. U wordt nu ingelogd."}},"errors":{"messages":{"already_confirmed":"was al bevestigd","not_found":"niet gevonden","not_locked":"was niet vergrendeld"}},"jobs":{"allowed_failures":"Toegestane mislukkingen","author":"Auteur","branch":"Tak","build_matrix":"Bouw matrix","compare":"Vergelijk","config":"Configuratie","duration":"Duur","finished_at":"Voltooid","message":"Bericht","messages":{"sponsored_by":"Deze testen zijn uitgevoerd op een machine gesponsord door"},"started_at":"Gestart","commit":"Commit","committer":"Committer"},"layouts":{"about":{"alpha":"Dit is in alfa-stadium.","join":"Doe met ons mee en help!","mailing_list":"Mailing lijst","messages":{"alpha":"Gelieve deze service niet te beschouwen als stabiel. Daar zijn we nog lang niet! Meer info hier."},"repository":"Repository","twitter":"Twitter"},"application":{"fork_me":"Maak een fork op Github","my_repositories":"Mijn repositories","recent":"Recent","search":"Zoeken","sponsers":"Sponsors","sponsors_link":"Bekijk al onze geweldige sponsors →"},"mobile":{"author":"Auteur","build":"Bouw","build_matrix":"Bouw matrix","compare":"Vergelijk","config":"Configuratie","duration":"Duur","finished_at":"Voltooid op","job":"Taak","commit":"Commit","committer":"Committer","log":"Logboek"},"top":{"admin":"Administratie","blog":"Blog","docs":"Documentatie","github_login":"Inloggen met Github","home":"Home","profile":"Profiel","sign_out":"Uitloggen","stats":"Statistieken"}},"locales":{"en":"English","es":"Español","fr":"Français","ja":"日本語","nb":"Norsk Bokmål","nl":"Nederlands","pl":"Polski","ru":"Русский","pt-BR":"português brasileiro"},"no_job":"Er zijn geen taken","profiles":{"show":{"email":"Email adres","github":"Github","message":{"config":"hoe eigen bouw-opties in te stellen","your_repos":"Zet de schakelaars hieronder aan om de Travis hook voor uw projecten te activeren en push daarna naar Github
\nOm te testen tegen meerdere rubies, zie"},"messages":{"notice":"Om te beginnen kunt u onze startersgids lezen.\\n Het zal maar enkele minuten van uw tijd vergen."},"update":"Bijwerken","update_locale":"Bijwerken","your_locale":"Uw taal","your_repos":"Uw repositories","token":"Token"}},"queue":"Wachtrij","repositories":{"branch":"Tak","duration":"Duur","finished_at":"Voltooid","image_url":"Afbeeldings URL","message":"Bericht","started_at":"Gestart","tabs":{"branches":"Tak samenvatting","build":"Bouw","build_history":"Bouw geschiedenis","current":"Huidig","job":"Taak"},"commit":"Commit","markdown":"Markdown","rdoc":"RDOC","textile":"Textile"},"repository":{"duration":"Duur"},"statistics":{"index":{"build_count":"Bouw aantal","count":"Aantal","last_month":"voorbije maand","repo_growth":"Repository groei","total_builds":"Bouw totaal","total_projects":"Projecten/Repository totaal"}},"workers":"Machines","home":{"name":"Hoofdpagina"}},"pl":{"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} godzina","other":"%{count} godziny"},"minutes_exact":{"one":"%{count} minuta","other":"%{count} minuty"},"seconds_exact":{"one":"%{count} sekunda","other":"%{count} sekundy"}}},"workers":"Workers","queue":"Kolejka","no_job":"Brak zadań","repositories":{"branch":"Gałąź","image_url":"URL obrazka","markdown":"Markdown","textile":"Textile","rdoc":"RDOC","commit":"Commit","message":"Opis","started_at":"Rozpoczęto","duration":"Czas trwania","finished_at":"Zakończono","tabs":{"current":"Aktualny","build_history":"Historia Buildów","branches":"Wszystkie Gałęzie","build":"Build","job":"Zadanie"}},"build":{"job":"Zadanie","duration":"Czas trwania","finished_at":"Zakończono"},"jobs":{"messages":{"sponsored_by":"Te testy zostały uruchomione na maszynie sponsorowanej przez"},"build_matrix":"Macierz Buildów","allowed_failures":"Dopuszczalne Niepowodzenia","author":"Autor","config":"Konfiguracja","compare":"Porównanie","committer":"Committer","branch":"Gałąź","commit":"Commit","message":"Opis","started_at":"Rozpoczęto","duration":"Czas trwania","finished_at":"Zakończono","sponsored_by":"Te testy zostały uruchomione na maszynie sponsorowanej przez"},"builds":{"name":"Build","messages":{"sponsored_by":"Te testy zostały uruchomione na maszynie sponsorowanej przez"},"build_matrix":"Macierz Buildów","allowed_failures":"Dopuszczalne Niepowodzenia","author":"Autor","config":"Konfiguracja","compare":"Porównanie","committer":"Komitujący","branch":"Gałąź","commit":"Commit","message":"Opis","started_at":"Rozpoczęto","duration":"Czas trwania","finished_at":"Zakończono"},"layouts":{"top":{"home":"Start","blog":"Blog","docs":"Dokumentacja","stats":"Statystki","github_login":"Zaloguj się przy pomocy Githuba","profile":"Profil","sign_out":"Wyloguj się"},"application":{"fork_me":"Fork me on Github","recent":"Ostatnie","search":"Wyniki","sponsers":"Sponsorzy","sponsors_link":"Zobacz naszych wszystkich wspaniałych sponsorów →","my_repositories":"Moje repozytoria"},"about":{"alpha":"To wciąż jest wersja alpha.","messages":{"alpha":"Proszę nie traktuj tego jako stabilnej usługi. Wciąż nam wiele do tego brakuje! Więcej informacji znajdziesz tutaj."},"join":"Pomóż i dołącz do nas!","mailing_list":"Lista mailingowa","repository":"Repozytorium","twitter":"Twitter"},"mobile":{"author":"Autor","build":"Build","build_matrix":"Macierz Buildów","commit":"Commit","committer":"Komitujący","compare":"Porównianie","config":"Konfiguracja","duration":"Czas trwania","finished_at":"Zakończono","job":"Zadanie","log":"Log"}},"profiles":{"show":{"email":"Email","github":"Github","message":{"your_repos":" Przesuń suwak poniżej, aby włączyć Travisa, dla twoich projektów, a następnie umieść swój kod na GitHubie.
\n Aby testować swój kod przy użyciu wielu wersji Rubiego, zobacz","config":"jak skonfigurować niestandardowe opcje builda"},"messages":{"notice":"Aby zacząć, przeczytaj nasz Przewodnik .\n Zajmie ci to tylko kilka minut."},"token":"Token","your_repos":"Twoje repozytoria"}},"statistics":{"index":{"count":"Ilość","repo_growth":"Przyrost repozytoriów","total_projects":"Łącznie projektów/repozytoriów","build_count":"Liczba buildów","last_month":"ostatni miesiąc","total_builds":"Łącznie Buildów"}},"date":{"abbr_day_names":["nie","pon","wto","śro","czw","pią","sob"],"abbr_month_names":["sty","lut","mar","kwi","maj","cze","lip","sie","wrz","paź","lis","gru"],"day_names":["niedziela","poniedziałek","wtorek","środa","czwartek","piątek","sobota"],"formats":{"default":"%d-%m-%Y","long":"%B %d, %Y","short":"%d %b"},"month_names":["styczeń","luty","marzec","kwiecień","maj","czerwiec","lipiec","sierpień","wrzesień","październik","listopad","grudzień"],"order":["day","month","year"]},"errors":{"format":"%{attribute} %{message}","messages":{"accepted":"musi zostać zaakceptowane","blank":"nie może być puste"}},"locales":{"en":"English","es":"Español","ja":"日本語","ru":"Русский","fr":"Français","nb":"Norsk Bokmål","pl":"Polski","nl":"Nederlands","pt-BR":"português brasileiro"}},"pt-BR":{"admin":{"actions":{"create":"criar","created":"criado","delete":"deletar","deleted":"deletado","update":"atualizar","updated":"atualizado"},"credentials":{"log_out":"Deslogar"},"dashboard":{"add_new":"Adicionar novo","ago":"atrás","last_used":"Última utilização","model_name":"Nome do modelo","modify":"Modificar","name":"Dashboard","pagename":"Administração do site","records":"Registros","show":"Mostrar"},"delete":{"confirmation":"Sim, tenho certeza","flash_confirmation":"%{name} foi destruído com sucesso"},"flash":{"error":"%{name} falhou ao %{action}","noaction":"Nenhuma ação foi tomada","successful":"%{name} foi %{action} com sucesso"},"history":{"name":"Histórico","no_activity":"Nenhuma Atividade","page_name":"Histórico para %{name}"},"list":{"add_new":"Adicionar novo","delete_action":"Deletar","delete_selected":"Deletar selecionados","edit_action":"Editar","search":"Buscar","select":"Selecionar %{name} para editar","select_action":"Selecionar","show_all":"Mostrar todos"},"new":{"basic_info":"Informações básicas","cancel":"Cancelar","chosen":"Escolhido %{name}","chose_all":"Escolher todos","clear_all":"Limpar todos","many_chars":"caracteres ou menos.","one_char":"caractere.","optional":"Opcional","required":"Requerido","save":"Salvar","save_and_add_another":"Salvar e adicionar outro","save_and_edit":"Salvar e alterar","select_choice":"Selecione e clique"}},"build":{"duration":"Duração","finished_at":"Concluído em","job":"Trabalho"},"builds":{"allowed_failures":"Falhas Permitidas","author":"Autor","branch":"Branch","build_matrix":"Matriz de Build","commit":"Commit","committer":"Committer","compare":"Comparar","config":"Config","duration":"Duração","finished_at":"Concluído em","message":"Mensagem","messages":{"sponsored_by":"Esta série de testes foi executada em uma caixa de processos patrocinada por"},"name":"Build","started_at":"Iniciou em"},"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} hora","other":"%{count} horas"},"minutes_exact":{"one":"%{count} minuto","other":"%{count} minutos"},"seconds_exact":{"one":"%{count} segundo","other":"%{count} segundos"}}},"devise":{"confirmations":{"confirmed":"Sua conta foi confirmada com sucesso. Você agora está logado.","send_instructions":"Você receberá um email com instruções de como confirmar sua conta em alguns minutos."},"failure":{"inactive":"Sua conta ainda não foi ativada.","invalid":"Email ou senha inválidos.","invalid_token":"Token de autenticação inválido.","locked":"Sua conta está trancada.","timeout":"Sua sessão expirou, por favor faça seu login novamente.","unauthenticated":"Você precisa fazer o login ou cadastrar-se antes de continuar.","unconfirmed":"Você precisa confirmar sua conta antes de continuar."},"mailer":{"confirmation_instructions":{"subject":"Instruções de confirmação"},"reset_password_instructions":{"subject":"Instruções de atualização de senha"},"unlock_instructions":{"subject":"Instruções de destrancamento"}},"passwords":{"send_instructions":"Você receberá um email com instruções de como atualizar sua senha em alguns minutos.","updated":"Sua senha foi alterada com sucesso. Você agora está logado."},"registrations":{"destroyed":"Tchau! Sua conta foi cancelada com sucesso. Esperamos vê-lo novamente em breve!","signed_up":"Você se cadastrou com sucesso. Se ativada, uma confirmação foi enviada para seu email.","updated":"Você atualizou sua conta com sucesso."},"sessions":{"signed_in":"Logado com sucesso.","signed_out":"Deslogado com sucesso."},"unlocks":{"send_instructions":"Você receberá um email com instruções de como destrancar sua conta em alguns minutos.","unlocked":"Sua conta foi destrancada com sucesso. Você agora está logado."}},"errors":{"messages":{"already_confirmed":"já foi confirmado","not_found":"não encontrado","not_locked":"não estava trancado"}},"home":{"name":"home"},"jobs":{"allowed_failures":"Falhas Permitidas","author":"Autor","branch":"Branch","build_matrix":"Matriz de Build","commit":"Commit","committer":"Committer","compare":"Comparar","config":"Config","duration":"Duração","finished_at":"Concluído em","message":"Mensagem","messages":{"sponsored_by":"Esta série de testes foi executada em uma caixa de processos patrocinada por"},"started_at":"Iniciou em"},"layouts":{"about":{"alpha":"Isto é um alpha.","join":"Junte-se à nós e ajude!","mailing_list":"Lista de email","messages":{"alpha":"Por favor, não considere isto um serviço estável. Estamos muito longe disso! Mais informações aqui."},"repository":"Repositório","twitter":"Twitter"},"application":{"fork_me":"Faça fork no Github","my_repositories":"Meus Repositórios","recent":"Recentes","search":"Buscar","sponsers":"Patrocinadores","sponsors_link":"Conheça todos os nossos patrocinadores →"},"mobile":{"author":"Autor","build":"Build","build_matrix":"Matriz de Build","commit":"Commit","committer":"Committer","compare":"Comparar","config":"Config","duration":"Duração","finished_at":"Concluído em","job":"Trabalho","log":"Log"},"top":{"admin":"Admin","blog":"Blog","docs":"Documentação","github_login":"Logue com o Github","home":"Home","profile":"Perfil","sign_out":"Sair","stats":"Estatísticas"}},"locales":{"en":"English","es":"Español","fr":"Français","ja":"日本語","nb":"Norsk Bokmål","nl":"Nederlands","pl":"Polski","ru":"Русский","pt-BR":"português brasileiro"},"no_job":"Não há trabalhos","profiles":{"show":{"email":"Email","github":"Github","message":{"config":"como configurar opções de build","your_repos":"Use os botões abaixo para ligar ou desligar o hook de serviço do Travis para seus projetos, e então, faça um push para o Github.
Para testar com múltiplas versões do Ruby, leia"},"messages":{"notice":"Para começar, leia nosso Guia de início. Só leva alguns minutinhos."},"token":"Token","update":"Atualizar","update_locale":"Atualizar","your_locale":"Sua língua","your_repos":"Seus Repositórios"}},"queue":"Fila","repositories":{"branch":"Branch","commit":"Commit","duration":"Duração","finished_at":"Concluído em","image_url":"URL da imagem","markdown":"Markdown","message":"Mensagem","rdoc":"RDOC","started_at":"Iniciou em","tabs":{"branches":"Sumário do Branch","build":"Build","build_history":"Histórico de Build","current":"Atual","job":"Trabalho"},"textile":"Textile"},"repository":{"duration":"Duração"},"statistics":{"index":{"build_count":"Número de Builds","count":"Número","last_month":"último mês","repo_growth":"Crescimento de Repositório","total_builds":"Total de Builds","total_projects":"Total de Projetos/Repositórios"}},"workers":"Processos"},"ru":{"admin":{"actions":{"create":"создать","created":"создано","delete":"удалить","deleted":"удалено","update":"обновить","updated":"обновлено"},"credentials":{"log_out":"Выход"},"dashboard":{"add_new":"Добавить","ago":"назад","last_used":"Использовалось в последний раз","model_name":"Имя модели","modify":"Изменить","name":"Панель управления","pagename":"Управление сайтом","records":"Записи","show":"Показать"},"delete":{"confirmation":"Да, я уверен","flash_confirmation":"%{name} успешно удалено"},"history":{"name":"История","no_activity":"Нет активности","page_name":"История %{name}"},"list":{"add_new":"Добавить","delete_action":"Удалить","delete_selected":"Удалить выбранные","edit_action":"Редактировать","search":"Поиск","select":"Для редактирования выберите %{name}","select_action":"Выбрать","show_all":"Показать все"},"new":{"basic_info":"Основная информация","cancel":"Отмена","chosen":"Выбрано %{name}","chose_all":"Выбрать все","clear_all":"Очистить все","one_char":"символ.","optional":"Необязательно","required":"Обязательно","save":"Сохранить","save_and_add_another":"Сохранить и добавить другое","save_and_edit":"Сохранить и продолжить редактирование","select_choice":"Выберите и кликните","many_chars":"символов или меньше."},"flash":{"error":"%{name} не удалось %{action}","noaction":"Никаких действий не произведено","successful":"%{name} было успешно %{action}"}},"build":{"duration":"Длительность","finished_at":"Завершен","job":"Задача"},"builds":{"allowed_failures":"Допустимые неудачи","author":"Автор","branch":"Ветка","build_matrix":"Матрица","commit":"Коммит","committer":"Коммитер","compare":"Дифф","config":"Конфигурация","duration":"Длительность","finished_at":"Завершен","message":"Комментарий","messages":{"sponsored_by":"Эта серия тестов была запущена на машине, спонсируемой"},"name":"Билд","started_at":"Начало"},"datetime":{"distance_in_words":{"hours_exact":{"one":"%{count} час","few":"%{count} часа","many":"%{count} часов","other":"%{count} часа"},"minutes_exact":{"one":"%{count} минута","few":"%{count} минуты","many":"%{count} минут","other":"%{count} минуты"},"seconds_exact":{"one":"%{count} секунда","few":"%{count} секунды","many":"%{count} секунд","other":"%{count} секунды"}}},"devise":{"confirmations":{"confirmed":"Ваш аккаунт успешно подтвержден. Приветствуем!","send_instructions":"В течении нескольких минут вы получите электронное письмо с инструкциями для прохождения процедуры подтверждения аккаунта."},"failure":{"inactive":"Ваш аккаунт еще не активирован.","invalid":"Ошибка в адресе почты или пароле.","invalid_token":"Неправильный токен аутентификации.","locked":"Ваш аккаунт заблокирован.","timeout":"Сессия окончена. Для продолжения работы войдите снова.","unauthenticated":"Вам нужно войти или зарегистрироваться.","unconfirmed":"Вы должны сначала подтвердить свой аккаунт."},"mailer":{"confirmation_instructions":{"subject":"Инструкции для подтверждению аккаунта"},"reset_password_instructions":{"subject":"Инструкции для сброса пароля"},"unlock_instructions":{"subject":"Инструкции для разблокирования аккаунта"}},"passwords":{"send_instructions":"В течении нескольких минут вы получите электронное письмо с инструкциями для сброса пароля.","updated":"Ваш пароль успешно изменен. Приветствуем!"},"registrations":{"destroyed":"Ваш аккаунт был успешно удален. Живите долго и процветайте!","signed_up":"Вы успешно прошли регистрацию. Инструкции для подтверждения аккаунта отправлены на ваш электронный адрес.","updated":"Аккаунт успешно обновлен."},"sessions":{"signed_in":"Приветствуем!","signed_out":"Удачи!"},"unlocks":{"send_instructions":"В течении нескольких минут вы получите электронное письмо с инструкциям для разблокировния аккаунта.","unlocked":"Ваш аккаунт успешно разблокирован. Приветствуем!"}},"errors":{"messages":{"already_confirmed":"уже подтвержден","not_found":"не найден","not_locked":"не заблокирован"}},"home":{"name":"Главная"},"jobs":{"allowed_failures":"Допустимые неудачи","author":"Автор","branch":"Ветка","build_matrix":"Матрица","commit":"Коммит","committer":"Коммитер","compare":"Сравнение","config":"Конфигурация","duration":"Длительность","finished_at":"Завершен","message":"Комментарий","messages":{"sponsored_by":"Эта серия тестов была запущена на машине спонсируемой"},"started_at":"Начало"},"layouts":{"about":{"alpha":"Это альфа-версия","join":"Присоединяйтесь к нам и помогайте!","mailing_list":"Лист рассылки","messages":{"alpha":"Пожалуйста, не считайте данный сервис стабильным. Мы еще очень далеки от стабильности! Подробности"},"repository":"Репозиторий","twitter":"Twitter"},"application":{"fork_me":"Fork me on Github","my_repositories":"Мои репозитории","recent":"Недавние","search":"Поиск","sponsers":"Спонсоры","sponsors_link":"Список всех наших замечательных спонсоров →"},"mobile":{"author":"Автор","build":"Сборка","build_matrix":"Матрица сборок","commit":"Коммит","committer":"Коммитер","compare":"Сравнение","config":"Конфигурация","duration":"Длительность","finished_at":"Завершен","job":"Задача","log":"Журнал"},"top":{"admin":"Управление","blog":"Блог","docs":"Документация","github_login":"Войти через Github","home":"Главная","profile":"Профиль","sign_out":"Выход","stats":"Статистика"}},"no_job":"Очередь пуста","profiles":{"show":{"email":"Электронная почта","github":"Github","message":{"config":"как настроить специальные опции билда","your_repos":"Используйте переключатели, чтобы включить Travis service hook для вашего проекта, а потом отправьте код на GitHub.
\nДля тестирования на нескольких версиях Ruby смотрите"},"messages":{"notice":"Перед началом, пожалуйста, прочтите Руководство для быстрого старта. Это займет всего несколько минут."},"token":"Токен","update":"Обновить","update_locale":"Обновить","your_locale":"Ваш язык","your_repos":"Ваши репозитории"}},"queue":"Очередь","repositories":{"branch":"Ветка","commit":"Коммит","duration":"Длительность","finished_at":"Завершен","image_url":"URL изображения","markdown":"Markdown","message":"Комментарий","rdoc":"RDOC","started_at":"Начало","tabs":{"branches":"Статус веток","build":"Билд","build_history":"История","current":"Текущий","job":"Задача"},"textile":"Textile"},"repository":{"duration":"Длительность"},"statistics":{"index":{"build_count":"Количество билдов","count":"Количество","last_month":"прошлый месяц","repo_growth":"Рост числа репозиториев","total_builds":"Всего билдов","total_projects":"Всего проектов/репозиториев"}},"workers":"Машины","locales":{"en":"English","es":"Español","ja":"日本語","ru":"Русский","fr":"Français","nb":"Norsk Bokmål","pl":"Polski","nl":"Nederlands","pt-BR":"português brasileiro"}}}; + diff --git a/app/assets/javascripts/lib/emoij.js.coffee b/app/assets/javascripts/lib/emoij.js.coffee new file mode 100644 index 00000000..a5440513 --- /dev/null +++ b/app/assets/javascripts/lib/emoij.js.coffee @@ -0,0 +1,43 @@ +@EmojiDictionary = [ + '-1', '0', '1', '109', '2', '3', '4', '5', '6', '7', '8', '8ball', '9', 'a', 'ab', 'airplane', 'alien', 'ambulance', 'angel', 'anger', 'angry', 'apple', + 'aquarius', 'aries', 'arrow_backward', 'arrow_down', 'arrow_forward', 'arrow_left', 'arrow_lower_left', 'arrow_lower_right', 'arrow_right', + 'arrow_up', 'arrow_upper_left', 'arrow_upper_right', 'art', 'astonished', 'atm', 'b', 'baby', 'baby_chick', 'baby_symbol', 'balloon', 'bamboo', 'bank', + 'barber', 'baseball', 'basketball', 'bath', 'bear', 'beer', 'beers', 'beginner', 'bell', 'bento', 'bike', 'bikini', 'bird', 'birthday', + 'black_square', 'blue_car', 'blue_heart', 'blush', 'boar', 'boat', 'bomb', 'book', 'boot', 'bouquet', 'bow', 'bowtie', 'boy', 'bread', 'briefcase', + 'broken_heart', 'bug', 'bulb', 'bullettrain_front', 'bullettrain_side', 'bus', 'busstop', 'cactus', 'cake', 'calling', 'camel', 'camera', 'cancer', + 'capricorn', 'car', 'cat', 'cd', 'chart', 'checkered_flag', 'cherry_blossom', 'chicken', 'christmas_tree', 'church', 'cinema', 'city_sunrise', + 'city_sunset', 'clap', 'clapper', 'clock1', 'clock10', 'clock11', 'clock12', 'clock2', 'clock3', 'clock4', 'clock5', 'clock6', 'clock7', 'clock8', + 'clock9', 'closed_umbrella', 'cloud', 'clubs', 'cn', 'cocktail', 'coffee', 'cold_sweat', 'computer', 'confounded', 'congratulations', 'construction', + 'construction_worker', 'convenience_store', 'cool', 'cop', 'copyright', 'couple', 'couple_with_heart', 'couplekiss', 'cow', 'crossed_flags', 'crown', + 'cry', 'cupid', 'currency_exchange', 'curry', 'cyclone', 'dancer', 'dancers', 'dango', 'dart', 'dash', 'de', 'department_store', 'diamonds', + 'disappointed', 'dog', 'dolls', 'dolphin', 'dress', 'dvd', 'ear', 'ear_of_rice', 'egg', 'eggplant', 'egplant', 'eight_pointed_black_star', + 'eight_spoked_asterisk', 'elephant', 'email', 'es', 'european_castle', 'exclamation', 'eyes', 'factory', 'fallen_leaf', 'fast_forward', 'fax', + 'fearful', 'feelsgood', 'feet', 'ferris_wheel', 'finnadie', 'fire', 'fire_engine', 'fireworks', 'fish', 'fist', 'flags', 'flushed', 'football', + 'fork_and_knife', 'fountain', 'four_leaf_clover', 'fr', 'fries', 'frog', 'fuelpump', 'gb', 'gem', 'gemini', 'ghost', 'gift', 'gift_heart', 'girl', + 'goberserk', 'godmode', 'golf', 'green_heart', 'grey_exclamation', 'grey_question', 'grin', 'guardsman', 'guitar', 'gun', 'haircut', + 'hamburger', 'hammer', 'hamster', 'hand', 'handbag', 'hankey', 'hash', 'headphones', 'heart', 'heart_decoration', 'heart_eyes', 'heartbeat', + 'heartpulse', 'hearts', 'hibiscus', 'high_heel', 'horse', 'hospital', 'hotel', 'hotsprings', 'house', 'hurtrealbad', 'icecream', 'id', + 'ideograph_advantage', 'imp', 'information_desk_person', 'iphone', 'it', 'jack_o_lantern', 'japanese_castle', 'joy', 'jp', 'key', 'kimono', 'kiss', + 'kissing_face', 'kissing_heart', 'koala', 'koko', 'kr', 'leaves', 'leo', 'libra', 'lips', 'lipstick', 'lock', 'loop', 'loudspeaker', 'love_hotel', + 'mag', 'mahjong', 'mailbox', 'man', 'man_with_gua_pi_mao', 'man_with_turban', 'maple_leaf', 'mask', 'massage', 'mega', 'memo', 'mens', 'metal', 'metro', + 'microphone', 'minidisc', 'mobile_phone_off', 'moneybag', 'monkey', 'monkey_face', 'moon', 'mortar_board', 'mount_fuji', 'mouse', 'movie_camera', + 'muscle', 'musical_note', 'nail_care', 'necktie', 'new', 'no_good', 'no_smoking', 'nose', 'notes', 'o', 'o2', 'ocean', 'octocat', 'octopus', + 'oden', 'office', 'ok', 'ok_hand', 'ok_woman', 'older_man', 'older_woman', 'open_hands', 'ophiuchus', 'palm_tree', 'parking', 'part_alternation_mark', + 'pencil', 'penguin', 'pensive', 'persevere', 'person_with_blond_hair', 'phone', 'pig', 'pill', 'pisces', 'plus1', 'point_down', 'point_left', + 'point_right', 'point_up', 'point_up_2', 'police_car', 'poop', 'post_office', 'postbox', 'pray', 'princess', 'punch', 'purple_heart', 'question', 'rabbit', + 'racehorse', 'radio', 'rage', 'rage1', 'rage2', 'rage3', 'rage4', 'rainbow', 'raised_hands', 'ramen', 'red_car', 'red_circle', 'registered', 'relaxed', + 'relieved', 'restroom', 'rewind', 'ribbon', 'rice', 'rice_ball', 'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster', 'rose', + 'ru', 'runner', 'sa', 'sagittarius', 'sailboat', 'sake', 'sandal', 'santa', 'satellite', 'satisfied', 'saxophone', 'school', 'school_satchel', + 'scissors', 'scorpius', 'scream', 'seat', 'secret', 'shaved_ice', 'sheep', 'shell', 'ship', 'shipit', 'shirt', 'shit', 'shoe', 'signal_strength', + 'six_pointed_star', 'ski', 'skull', 'sleepy', 'slot_machine', 'smile', 'smiley', 'smirk', 'smoking', 'snake', 'snowman', 'sob', 'soccer', + 'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles', 'speaker', 'speedboat', 'squirrel', 'star', 'star2', 'stars', 'station', + 'statue_of_liberty', 'stew', 'strawberry', 'sunflower', 'sunny', 'sunrise', 'sunrise_over_mountains', 'surfer', 'sushi', 'suspect', 'sweat', + 'sweat_drops', 'swimmer', 'syringe', 'tada', 'tangerine', 'taurus', 'taxi', 'tea', 'telephone', 'tennis', 'tent', 'thumbsdown', 'thumbsup', 'ticket', + 'tiger', 'tm', 'toilet', 'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'traffic_light', 'train', 'trident', 'trophy', 'tropical_fish', 'truck', + 'trumpet', 'tshirt', 'tulip', 'tv', 'u5272', 'u55b6', 'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7a7a', 'umbrella', 'unamused', + 'underage', 'unlock', 'up', 'us', 'v', 'vhs', 'vibration_mode', 'virgo', 'vs', 'walking', 'warning', 'watermelon', 'wave', 'wc', 'wedding', 'whale', + 'wheelchair', 'white_square', 'wind_chime', 'wink', 'wink2', 'wolf', 'woman', 'womans_hat', 'womens', 'x', 'yellow_heart', 'zap', 'zzz' +] + + + diff --git a/app/assets/javascripts/lib/ext/ember/bound_helper.js b/app/assets/javascripts/lib/ext/ember/bound_helper.js new file mode 100644 index 00000000..3e46827b --- /dev/null +++ b/app/assets/javascripts/lib/ext/ember/bound_helper.js @@ -0,0 +1,68 @@ +// https://gist.github.com/2018185 +// For reference: https://github.com/wagenet/ember.js/blob/ac66dcb8a1cbe91d736074441f853e0da474ee6e/packages/ember-handlebars/lib/views/bound_property_view.js +var BoundHelperView = Ember.View.extend(Ember._Metamorph, { + + context: null, + options: null, + property: null, + // paths of the property that are also observed + propertyPaths: [], + + value: Ember.K, + + valueForRender: function() { + var value = this.value(Ember.getPath(this.context, this.property), this.options); + if (this.options.escaped) { value = Handlebars.Utils.escapeExpression(value); } + return value; + }, + + render: function(buffer) { + buffer.push(this.valueForRender()); + }, + + valueDidChange: function() { + if (this.morph.isRemoved()) { return; } + this.morph.html(this.valueForRender()); + }, + + didInsertElement: function() { + this.valueDidChange(); + }, + + init: function() { + this._super(); + Ember.addObserver(this.context, this.property, this, 'valueDidChange'); + this.get('propertyPaths').forEach(function(propName) { + Ember.addObserver(this.context, this.property + '.' + propName, this, 'valueDidChange'); + }, this); + }, + + destroy: function() { + Ember.removeObserver(this.context, this.property, this, 'valueDidChange'); + this.get('propertyPaths').forEach(function(propName) { + this.context.removeObserver(this.property + '.' + propName, this, 'valueDidChange'); + }, this); + this._super(); + } + +}); + +Ember.registerBoundHelper = function(name, func) { + var propertyPaths = Array.prototype.slice.call(arguments, 2); + Ember.Handlebars.registerHelper(name, function(property, options) { + var data = options.data, + view = data.view, + ctx = this; + + var bindView = view.createChildView(BoundHelperView, { + property: property, + propertyPaths: propertyPaths, + context: ctx, + options: options.hash, + value: func + }); + + view.appendChild(bindView); + }); +}; + diff --git a/app/assets/javascripts/lib/ext/jquery.js.coffee b/app/assets/javascripts/lib/ext/jquery.js.coffee new file mode 100644 index 00000000..2f4f0678 --- /dev/null +++ b/app/assets/javascripts/lib/ext/jquery.js.coffee @@ -0,0 +1,143 @@ +$.fn.extend + outerHtml: -> + $(this).wrap('
').parent().html() + + outerElement: -> + $($(this).outerHtml()).empty() + + flash: -> + Utils.flash this + + unflash: -> + Utils.unflash this + + filterLog: -> + @deansi() + @foldLog() + + deansi: -> + @html Utils.deansi(@html()) + + foldLog: -> + @html Utils.foldLog(@html()) + + unfoldLog: -> + @html Utils.unfoldLog(@html()) + + updateTimes: -> + Utils.updateTimes this + + activateTab: (tab) -> + Utils.activateTab this, tab + + timeInWords: -> + $(this).each -> + $(this).text Utils.timeInWords(parseInt($(this).attr('title'))) + + updateGithubStats: (repository) -> + Utils.updateGithubStats repository, $(this) + +$.extend + keys: (obj) -> + keys = [] + $.each obj, (key) -> + keys.push key + + keys + + values: (obj) -> + values = [] + $.each obj, (key, value) -> + values.push value + + values + + camelize: (string, uppercase) -> + string = $.capitalize(string) if uppercase or typeof uppercase is 'undefined' + string.replace /_(.)?/g, (match, chr) -> + (if chr then chr.toUpperCase() else '') + + capitalize: (string) -> + string[0].toUpperCase() + string.substring(1) + + compact: (array) -> + $.grep array, (value) -> + !!value + + all: (array, callback) -> + args = Array::slice.apply(arguments) + callback = args.pop() + array = args.pop() or this + i = 0 + + while i < array.length + return false if callback(array[i]) + i++ + true + + detect: (array, callback) -> + args = Array::slice.apply(arguments) + callback = args.pop() + array = args.pop() or this + i = 0 + + while i < array.length + return array[i] if callback(array[i]) + i++ + + select: (array, callback) -> + args = Array::slice.apply(arguments) + callback = args.pop() + array = args.pop() or this + result = [] + i = 0 + + while i < array.length + result.push array[i] if callback(array[i]) + i++ + result + + slice: (object, key) -> + keys = Array::slice.apply(arguments) + object = (if (typeof keys[0] is 'object') then keys.shift() else this) + result = {} + for key of object + result[key] = object[key] if keys.indexOf(key) > -1 + result + + only: (object) -> + keys = Array::slice.apply(arguments) + object = (if (typeof keys[0] is 'object') then keys.shift() else this) + result = {} + for key of object + result[key] = object[key] unless keys.indexOf(key) is -1 + result + + except: (object) -> + keys = Array::slice.apply(arguments) + object = (if (typeof keys[0] is 'object') then keys.shift() else this) + result = {} + for key of object + result[key] = object[key] if keys.indexOf(key) is -1 + result + + map: (elems, callback, arg) -> + value = undefined + key = undefined + ret = [] + i = 0 + length = elems.length + isArray = elems instanceof jQuery || length != undefined && typeof length == 'number' && (length > 0 && elems[0] && elems[length - 1]) || length == 0 || jQuery.isArray(elems) + if isArray + while i < length + value = callback(elems[i], i, arg) + ret[ret.length] = value if value? + i++ + else + for key of elems + value = callback(elems[key], key, arg) + ret[ret.length] = value if value? + ret.concat.apply [], ret + + truncate: (string, length) -> + if string.length > length then string.trim().substring(0, length) + '...' else string diff --git a/app/assets/javascripts/lib/log.js.coffee b/app/assets/javascripts/lib/log.js.coffee new file mode 100644 index 00000000..a38ddfd5 --- /dev/null +++ b/app/assets/javascripts/lib/log.js.coffee @@ -0,0 +1,63 @@ +@Travis.Log = + FOLDS: + schema: /(\$ (?:bundle exec )?rake( db:create)? db:schema:load[\s\S]*?-- assume_migrated_upto_version[\s\S]*?<\/p>\n.*<\/p>)/g + migrate: /(\$ (?:bundle exec )?rake( db:create)? db:migrate[\s\S]*== +\w+: migrated \(.*\) =+)/g + bundle: /(\$ bundle install.*<\/p>\n((Updating|Using|Installing|Fetching|remote:|Receiving|Resolving).*?<\/p>\n|<\/p>\n)*)/g + exec: /([\/\w]*.rvm\/rubies\/[\S]*?\/(ruby|rbx|jruby) .*?<\/p>)/g + + filter: (log) -> + log = @escapeHtml(log) + log = @deansi(log) + log = log.replace(/\r/g, '') + log = @numberLines(log) + log = @fold(log) + log = log.replace(/\n/g, '') + log + + stripPaths: (log) -> + log.replace /\/home\/vagrant\/builds(\/[^\/\n]+){2}\//g, '' + + escapeHtml: (log) -> + Handlebars.Utils.escapeExpression log + + escapeRuby: (log) -> + log.replace /#<(\w+.*?)>/, '#<$1>' + + numberLines: (log) -> + result = '' + $.each log.trim().split('\n'), (ix, line) -> + number = ix + 1 + path = Travis.Log.location().substr(1).replace(/\/L\d+/, '') + '/L' + number + result += '

%@%@

\n'.fmt(path, path, number, number, line) + + result.trim() + + deansi: (log) -> + log = log.replace(/\r\r/g, '\r').replace(/\033\[K\r/g, '\r').replace(/^.*\r(?!$)/g, '').replace(/\[2K/g, '').replace(/\033\(B/g, '') + ansi = ansiparse(log) + text = '' + ansi.forEach (part) -> + classes = [] + part.foreground and classes.push(part.foreground) + part.background and classes.push('bg-' + part.background) + part.bold and classes.push('bold') + part.italic and classes.push('italic') + text += (if classes.length then ('' + part.text + '') else part.text) + + text.replace /\033/g, '' + + fold: (log) -> + log = @unfold(log) + $.each Travis.Log.FOLDS, (name, pattern) -> + log = log.replace(pattern, -> + '
' + arguments[1].trim() + '
' + ) + + log + + unfold: (log) -> + log.replace /
([\s\S]*?)<\/div>/g, '$1\n' + + location: -> + window.location.hash + diff --git a/app/assets/javascripts/lib/ticker.js.coffee b/app/assets/javascripts/lib/ticker.js.coffee new file mode 100644 index 00000000..fb889503 --- /dev/null +++ b/app/assets/javascripts/lib/ticker.js.coffee @@ -0,0 +1,18 @@ +@Travis.Ticker = Ember.Object.extend + init: -> + @_super() + @schedule() + tick: -> + context = @get('context') + @get('targets').forEach (target) => + target = context.get(target) + return unless target + if target.forEach + target.forEach (target) -> target.tick() + else + target.tick() + @schedule() + schedule: -> + Ember.run.later((=> @tick()), Travis.app.TICK_INTERVAL) + + diff --git a/app/assets/javascripts/travis.js b/app/assets/javascripts/travis.js index b9ba2a2b..b1c2c995 100644 --- a/app/assets/javascripts/travis.js +++ b/app/assets/javascripts/travis.js @@ -1,4 +1,8 @@ //= require_self +//= require ./vendor/ansiparse.js +//= require ./vendor/i18n.js +//= require ./vendor/jquery.timeago.js +//= require_tree ./config //= require_tree ./lib //= require app/app.js diff --git a/app/assets/javascripts/vendor/ansiparse.js b/app/assets/javascripts/vendor/ansiparse.js new file mode 100644 index 00000000..4adac134 --- /dev/null +++ b/app/assets/javascripts/vendor/ansiparse.js @@ -0,0 +1,161 @@ +ansiparse = function (str) { + // + // I'm terrible at writing parsers. + // + var matchingControl = null, + matchingData = null, + matchingText = '', + ansiState = [], + result = [], + state = {}; + + // + // General workflow for this thing is: + // \033\[33mText + // | | | + // | | matchingText + // | matchingData + // matchingControl + // + // In further steps we hope it's all going to be fine. It usually is. + // + + for (var i = 0; i < str.length; i++) { + if (matchingControl != null) { + if (matchingControl == '\033' && str[i] == '\[') { + // + // We've matched full control code. Lets start matching formating data. + // + + // + // "emit" matched text with correct state + // + if (matchingText) { + state.text = matchingText; + result.push(state); + state = {}; + matchingText = ""; + } + + matchingControl = null; + matchingData = ''; + } + else { + // + // We failed to match anything - most likely a bad control code. We + // go back to matching regular strings. + // + matchingText += matchingControl + str[i]; + matchingControl = null; + } + continue; + } + else if (matchingData != null) { + if (str[i] == ';') { + // + // `;` separates many formatting codes, for example: `\033[33;43m` + // means that both `33` and `43` should be applied. + // + // TODO: this can be simplified by modifying state here. + // + ansiState.push(matchingData); + matchingData = ''; + } + else if (str[i] == 'm') { + // + // `m` finished whole formatting code. We can proceed to matching + // formatted text. + // + ansiState.push(matchingData); + matchingData = null; + matchingText = ''; + + // + // Convert matched formatting data into user-friendly state object. + // + // TODO: DRY. + // + ansiState.forEach(function (ansiCode) { + if (ansiparse.foregroundColors[ansiCode]) { + state.foreground = ansiparse.foregroundColors[ansiCode]; + } + else if (ansiparse.backgroundColors[ansiCode]) { + state.background = ansiparse.backgroundColors[ansiCode]; + } + else if (ansiCode == 39) { + delete state.foreground; + } + else if (ansiCode == 49) { + delete state.background; + } + else if (ansiparse.styles[ansiCode]) { + state[ansiparse.styles[ansiCode]] = true; + } + else if (ansiCode == 22) { + state.bold = false; + } + else if (ansiCode == 23) { + state.italic = false; + } + else if (ansiCode == 24) { + state.underline = false; + } + }); + ansiState = []; + } + else { + matchingData += str[i]; + } + continue; + } + + if (str[i] == '\033') { + matchingControl = str[i]; + + } + else { + matchingText += str[i]; + } + } + + if (matchingText) { + state.text = matchingText + (matchingControl ? matchingControl : ''); + result.push(state); + } + return result; +} + +ansiparse.foregroundColors = { + '30': 'black', + '31': 'red', + '32': 'green', + '33': 'yellow', + '34': 'blue', + '35': 'magenta', + '36': 'cyan', + '37': 'white', + '90': 'grey' +}; + +ansiparse.backgroundColors = { + '40': 'black', + '41': 'red', + '42': 'green', + '43': 'yellow', + '44': 'blue', + '45': 'magenta', + '46': 'cyan', + '47': 'white' +}; + +ansiparse.styles = { + '1': 'bold', + '3': 'italic', + '4': 'underline' +}; + +if (typeof module == "object" && typeof window == "undefined") { + module.exports = ansiparse; +} + + diff --git a/app/assets/javascripts/vendor/ember.js b/app/assets/javascripts/vendor/ember.js index 8fa8f52a..f299f534 100644 --- a/app/assets/javascripts/vendor/ember.js +++ b/app/assets/javascripts/vendor/ember.js @@ -955,7 +955,7 @@ Ember.isArray = function(obj) { Ember.makeArray(); => [] Ember.makeArray(null); => [] Ember.makeArray(undefined); => [] - Ember.makeArray('lindsay'); => ['lindsay'] + Ember.makeArray('lindsay'); => ['lindsay'] Ember.makeArray([1,2,42]); => [1,2,42] var controller = Ember.ArrayProxy.create({ content: [] }); @@ -3586,7 +3586,7 @@ Ember.RunLoop = RunLoop; call. Ember.run(function(){ - // code to be execute within a RunLoop + // code to be execute within a RunLoop }); @name run @@ -3624,7 +3624,7 @@ var run = Ember.run; an lower-level way to use a RunLoop instead of using Ember.run(). Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); @@ -3640,7 +3640,7 @@ Ember.run.begin = function() { instead of using Ember.run(). Ember.run.begin(); - // code to be execute within a RunLoop + // code to be execute within a RunLoop Ember.run.end(); @returns {void} @@ -5388,7 +5388,7 @@ Ember.inspect = function(obj) { /** Compares two objects, returning true if they are logically equal. This is a deeper comparison than a simple triple equal. For sets it will compare the - internal objects. For any other object that implements `isEqual()` it will + internal objects. For any other object that implements `isEqual()` it will respect that method. Ember.isEqual('hello', 'hello'); => true @@ -5570,7 +5570,7 @@ Ember.String = { > beta > gamma - @param {String} str + @param {String} str The string to split @returns {String} split string @@ -5579,7 +5579,7 @@ Ember.String = { /** Converts a camelized string into all lower case separated by underscores. - + 'innerHTML'.decamelize() => 'inner_html' 'action_name'.decamelize() => 'action_name' 'css-class-name'.decamelize() => 'css-class-name' @@ -5596,7 +5596,7 @@ Ember.String = { /** Replaces underscores or spaces with dashes. - + 'innerHTML'.dasherize() => 'inner-html' 'action_name'.dasherize() => 'action-name' 'css-class-name'.dasherize() => 'css-class-name' @@ -5763,7 +5763,7 @@ if (Ember.EXTEND_PROTOTYPES) { /** The `property` extension of Javascript's Function prototype is available - when Ember.EXTEND_PROTOTYPES is true, which is the default. + when Ember.EXTEND_PROTOTYPES is true, which is the default. Computed properties allow you to treat a function like a property: @@ -5818,7 +5818,7 @@ if (Ember.EXTEND_PROTOTYPES) { /** The `observes` extension of Javascript's Function prototype is available - when Ember.EXTEND_PROTOTYPES is true, which is the default. + when Ember.EXTEND_PROTOTYPES is true, which is the default. You can observe property changes simply by adding the `observes` call to the end of your method declarations in classes that you write. @@ -5829,7 +5829,7 @@ if (Ember.EXTEND_PROTOTYPES) { // Executes whenever the "value" property changes }.observes('value') }); - + @see Ember.Observable */ Function.prototype.observes = function() { @@ -5839,7 +5839,7 @@ if (Ember.EXTEND_PROTOTYPES) { /** The `observesBefore` extension of Javascript's Function prototype is - available when Ember.EXTEND_PROTOTYPES is true, which is the default. + available when Ember.EXTEND_PROTOTYPES is true, which is the default. You can get notified when a property changes is about to happen by by adding the `observesBefore` call to the end of your method @@ -5850,7 +5850,7 @@ if (Ember.EXTEND_PROTOTYPES) { // Executes whenever the "value" property is about to change }.observesBefore('value') }); - + @see Ember.Observable */ Function.prototype.observesBefore = function() { @@ -6447,9 +6447,9 @@ Ember.Enumerable = Ember.Mixin.create( /** @lends Ember.Enumerable */ { /** Returns a copy of the array with all null elements removed. - + var arr = ["a", null, "c", null]; - arr.compact(); => ["a", "c"] + arr.compact(); => ["a", "c"] @returns {Array} the array without null elements. */ @@ -7449,7 +7449,7 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, colors.clear(); => [] colors.length(); => 0 - @returns {Ember.Array} An empty Array. + @returns {Ember.Array} An empty Array. */ clear: function () { var len = get(this, 'length'); @@ -7643,15 +7643,15 @@ var get = Ember.get, set = Ember.set; @class ## Overview - + This mixin provides properties and property observing functionality, core features of the Ember object model. - + Properties and observers allow one object to observe changes to a property on another object. This is one of the fundamental ways that models, controllers and views communicate with each other in an Ember application. - + Any object that has this mixin applied can be used in observer operations. That includes Ember.Object and most objects you will interact with as you write your Ember application. @@ -7659,16 +7659,16 @@ var get = Ember.get, set = Ember.set; Note that you will not generally apply this mixin to classes yourself, but you will use the features provided by this module frequently, so it is important to understand how to use it. - + ## Using get() and set() - + Because of Ember's support for bindings and observers, you will always access properties using the get method, and set properties using the set method. This allows the observing objects to be notified and computed properties to be handled properly. - + More documentation about `get` and `set` are below. - + ## Observing Property Changes You typically observe property changes simply by adding the `observes` @@ -7680,7 +7680,7 @@ var get = Ember.get, set = Ember.set; // Executes whenever the "value" property changes }.observes('value') }); - + Although this is the most common way to add an observer, this capability is actually built into the Ember.Object class on top of two methods defined in this mixin: `addObserver` and `removeObserver`. You can use @@ -7693,12 +7693,12 @@ var get = Ember.get, set = Ember.set; This will call the `targetAction` method on the `targetObject` to be called whenever the value of the `propertyKey` changes. - - Note that if `propertyKey` is a computed property, the observer will be - called when any of the property dependencies are changed, even if the + + Note that if `propertyKey` is a computed property, the observer will be + called when any of the property dependencies are changed, even if the resulting value of the computed property is unchanged. This is necessary because computed properties are not computed until `get` is called. - + @extends Ember.Mixin */ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { @@ -7712,7 +7712,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This method is usually similar to using object[keyName] or object.keyName, however it supports both computed properties and the unknownProperty handler. - + Because `get` unifies the syntax for accessing all these kinds of properties, it can make many refactorings easier, such as replacing a simple property with a computed property, or vice versa. @@ -7908,11 +7908,11 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { Ember.propertyDidChange(this, keyName); return this; }, - + /** Convenience method to call `propertyWillChange` and `propertyDidChange` in succession. - + @param {String} keyName The property key to be notified about. @returns {Ember.Observable} */ @@ -8004,7 +8004,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This method will be called when a client attempts to get the value of a property that has not been defined in one of the typical ways. Override this method to create "virtual" properties. - + @param {String} key The name of the unknown property that was requested. @returns {Object} The property value or undefined. Default is undefined. */ @@ -8016,7 +8016,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { This method will be called when a client attempts to set the value of a property that has not been defined in one of the typical ways. Override this method to create "virtual" properties. - + @param {String} key The name of the unknown property to be set. @param {Object} value The value the unknown property is to be set to. */ @@ -8027,7 +8027,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** This is like `get`, but allows you to pass in a dot-separated property path. - + person.getPath('address.zip'); // return the zip person.getPath('children.firstObject.age'); // return the first kid's age @@ -8043,7 +8043,7 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** This is like `set`, but allows you to specify the property you want to set as a dot-separated property path. - + person.setPath('address.zip', 10011); // set the zip to 10011 person.setPath('children.firstObject.age', 6); // set the first kid's age to 6 @@ -8061,9 +8061,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** Retrieves the value of a property, or a default value in the case that the property returns undefined. - + person.getWithDefault('lastName', 'Doe'); - + @param {String} keyName The name of the property to retrieve @param {Object} defaultValue The value to return if the property value is undefined @returns {Object} The property value or the defaultValue. @@ -8074,10 +8074,10 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** Set the value of a property to the current value plus some amount. - + person.incrementProperty('age'); team.incrementProperty('score', 2); - + @param {String} keyName The name of the property to increment @param {Object} increment The amount to increment by. Defaults to 1 @returns {Object} The new property value @@ -8087,13 +8087,13 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { set(this, keyName, (get(this, keyName) || 0)+increment); return get(this, keyName); }, - + /** Set the value of a property to the current value minus some amount. - + player.decrementProperty('lives'); orc.decrementProperty('health', 5); - + @param {String} keyName The name of the property to decrement @param {Object} increment The amount to decrement by. Defaults to 1 @returns {Object} The new property value @@ -8107,9 +8107,9 @@ Ember.Observable = Ember.Mixin.create(/** @scope Ember.Observable.prototype */ { /** Set the value of a boolean property to the opposite of it's current value. - + starship.toggleProperty('warpDriveEnaged'); - + @param {String} keyName The name of the property to toggle @returns {Object} The new property value */ @@ -11496,7 +11496,7 @@ var invokeForState = { `Ember.View` is the class in Ember responsible for encapsulating templates of HTML content, combining templates with data to render as sections of a page's DOM, and registering and responding to user-initiated events. - + ## HTML Tag The default HTML tag name used for a view's DOM representation is `div`. This can be customized by setting the `tagName` property. The following view class: @@ -11522,7 +11522,7 @@ var invokeForState = {
`class` attribute values can also be set by providing a `classNameBindings` property - set to an array of properties names for the view. The return value of these properties + set to an array of properties names for the view. The return value of these properties will be added as part of the value for the view's `class` attribute. These properties can be computed properties: @@ -11551,7 +11551,7 @@ var invokeForState = {
- When using boolean class name bindings you can supply a string value other than the + When using boolean class name bindings you can supply a string value other than the property name for use as the `class` HTML attribute by appending the preferred value after a ":" character when defining the binding: @@ -11592,11 +11592,11 @@ var invokeForState = {
- Updates to the the value of a class name binding will result in automatic update + Updates to the the value of a class name binding will result in automatic update of the HTML `class` attribute in the view's rendered HTML representation. If the value becomes `false` or `undefined` the class name will be removed. - Both `classNames` and `classNameBindings` are concatenated properties. + Both `classNames` and `classNameBindings` are concatenated properties. See `Ember.Object` documentation for more information about concatenated properties. ## HTML Attributes @@ -11642,7 +11642,7 @@ var invokeForState = { }.property() }) - Updates to the the property of an attribute binding will result in automatic update + Updates to the the property of an attribute binding will result in automatic update of the HTML attribute in the view's rendered HTML representation. `attributeBindings` is a concatenated property. See `Ember.Object` documentation @@ -11733,7 +11733,7 @@ var invokeForState = { primary templates, layouts can be any function that accepts an optional context parameter and returns a string of HTML that will be inserted inside view's tag. Views whose HTML element is self closing (e.g. ``) cannot have a layout and this property will be ignored. - + Most typically in Ember a layout will be a compiled Ember.Handlebars template. A view's layout can be set directly with the `layout` property or reference an @@ -11758,7 +11758,7 @@ var invokeForState = { See `Handlebars.helpers.yield` for more information. ## Responding to Browser Events - Views can respond to user-initiated events in one of three ways: method implementation, + Views can respond to user-initiated events in one of three ways: method implementation, through an event manager, and through `{{action}}` helper use in their template or layout. ### Method Implementation @@ -11775,8 +11775,8 @@ var invokeForState = { ### Event Managers Views can define an object as their `eventManager` property. This object can then implement methods that match the desired event names. Matching events that occur - on the view's rendered HTML or the rendered HTML of any of its DOM descendants - will trigger this method. A `jQuery.Event` object will be passed as the first + on the view's rendered HTML or the rendered HTML of any of its DOM descendants + will trigger this method. A `jQuery.Event` object will be passed as the first argument to the method and an `Ember.View` object as the second. The `Ember.View` will be the view whose rendered HTML was interacted with. This may be the view with the `eventManager` property or one of its descendent views. @@ -11810,7 +11810,7 @@ var invokeForState = { Similarly a view's event manager will take precedence for events of any views rendered as a descendent. A method name that matches an event name will not be called - if the view instance was rendered inside the HTML representation of a view that has + if the view instance was rendered inside the HTML representation of a view that has an `eventManager` property defined that handles events of the name. Events not handled by the event manager will still trigger method calls on the descendent. @@ -11832,7 +11832,7 @@ var invokeForState = { // eventManager doesn't handle click events }, mouseEnter: function(event){ - // will never be called if rendered inside + // will never be called if rendered inside // an OuterView. } }) @@ -11853,7 +11853,7 @@ var invokeForState = { Form events: 'submit', 'change', 'focusIn', 'focusOut', 'input' HTML5 drag and drop events: 'dragStart', 'drag', 'dragEnter', 'dragLeave', 'drop', 'dragEnd' - + ## Handlebars `{{view}}` Helper Other `Ember.View` instances can be included as part of a view's template by using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for additional information. @@ -14230,7 +14230,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; @class `Ember.CollectionView` is an `Ember.View` descendent responsible for managing a - collection (an array or array-like object) by maintaing a child view object and + collection (an array or array-like object) by maintaing a child view object and associated DOM representation for each item in the array and ensuring that child views and their associated rendered HTML are updated when items in the array are added, removed, or replaced. @@ -14274,7 +14274,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; ## Automatic matching of parent/child tagNames - Setting the `tagName` property of a `CollectionView` to any of + Setting the `tagName` property of a `CollectionView` to any of "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result in the item views receiving an appropriately matched `tagName` property. @@ -15118,15 +15118,15 @@ var arrayForEach = Ember.ArrayPolyfills.forEach; robotManager.getPath('currentState.name') // 'rampaging' Transition actions can also be created using the `transitionTo` method of the Ember.State class. The - following example StateManagers are equivalent: - + following example StateManagers are equivalent: + aManager = Ember.StateManager.create({ stateOne: Ember.State.create({ changeToStateTwo: Ember.State.transitionTo('stateTwo') }), stateTwo: Ember.State.create({}) }) - + bManager = Ember.StateManager.create({ stateOne: Ember.State.create({ changeToStateTwo: function(manager, context){ @@ -15207,7 +15207,7 @@ Ember.StateManager = Ember.State.extend( @default true */ errorOnUnhandledEvent: true, - + send: function(event, context) { Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState')); if (arguments.length === 1) { context = {}; } @@ -17894,7 +17894,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ Will result in HTML structure: - @@ -17916,7 +17916,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ }) aView.appendTo('body') - + Will result in HTML structure:
@@ -17990,7 +17990,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ Will result in the following HTML:
-
+
hi
@@ -18150,7 +18150,7 @@ var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt;

Howdy Mary

Howdy Sara

- + @name Handlebars.helpers.collection @param {String} path @param {Hash} options @@ -18479,7 +18479,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont
- Clicking "click me" will trigger the `anActionName` method of the `aView` object with a + Clicking "click me" will trigger the `anActionName` method of the `aView` object with a `jQuery.Event` object as its argument. The `jQuery.Event` object will be extended to include a `view` property that is set to the original view interacted with (in this case the `aView` object). @@ -18504,9 +18504,9 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont - Clicking "click me" in the rendered HTML of the above template will trigger the - `anActionName` method of the object at `MyApplication.someObject`. The first argument - to this method will be a `jQuery.Event` extended to include a `view` property that is + Clicking "click me" in the rendered HTML of the above template will trigger the + `anActionName` method of the object at `MyApplication.someObject`. The first argument + to this method will be a `jQuery.Event` extended to include a `view` property that is set to the original view interacted with. A path relative to the template's `Ember.View` instance can also be used as a target: @@ -18517,7 +18517,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont - Clicking "click me" in the rendered HTML of the above template will trigger the + Clicking "click me" in the rendered HTML of the above template will trigger the `anActionName` method of the view's parent view. The `{{action}}` helper is `Ember.StateManager` aware. If the target of @@ -18561,7 +18561,7 @@ ActionHelper.registerAction = function(actionName, eventName, target, view, cont See `Ember.EventDispatcher` for a list of acceptable DOM event names. Because `{{action}}` depends on Ember's event dispatch system it will only function if - an `Ember.EventDispatcher` instance is available. An `Ember.EventDispatcher` instance + an `Ember.EventDispatcher` instance is available. An `Ember.EventDispatcher` instance will be created when a new `Ember.Application` is created. Having an instance of `Ember.Application` will satisfy this requirement. @@ -18598,6 +18598,9 @@ EmberHandlebars.registerHelper('action', function(actionName, options) { target = target || view; + // console.log(hash.context) + // debugger + context = hash.context ? getPath(this, hash.context, options) : options.contexts[0]; var output = [], url; @@ -18759,7 +18762,7 @@ var set = Ember.set, get = Ember.get; /** @class - Creates an HTML input of type 'checkbox' with HTML related properties + Creates an HTML input of type 'checkbox' with HTML related properties applied directly to the input. {{view Ember.Checkbox classNames="applicaton-specific-checkbox"}} @@ -18778,7 +18781,7 @@ var set = Ember.set, get = Ember.get; through the Ember object or by interacting with its rendered element representation via the mouse, keyboard, or touch. Updating the value of the checkbox via jQuery will result in the checked value of the object and its element losing synchronization. - + ## Layout and LayoutName properties Because HTML `input` elements are self closing `layout` and `layoutName` properties will not be applied. See `Ember.View`'s layout section for more information. @@ -18890,7 +18893,7 @@ var get = Ember.get, set = Ember.set; ## Layout and LayoutName properties Because HTML `input` elements are self closing `layout` and `layoutName` properties will not be applied. See `Ember.View`'s layout section for more information. - + @extends Ember.TextSupport */ Ember.TextField = Ember.View.extend(Ember.TextSupport, @@ -19067,7 +19070,7 @@ var get = Ember.get, set = Ember.set; ## Layout and LayoutName properties - Because HTML `textarea` elements do not contain inner HTML the `layout` and `layoutName` + Because HTML `textarea` elements do not contain inner HTML the `layout` and `layoutName` properties will not be applied. See `Ember.View`'s layout section for more information. @extends Ember.TextSupport diff --git a/app/assets/javascripts/vendor/i18n.js b/app/assets/javascripts/vendor/i18n.js new file mode 100644 index 00000000..d3c591ad --- /dev/null +++ b/app/assets/javascripts/vendor/i18n.js @@ -0,0 +1,532 @@ +// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(searchElement /*, fromIndex */) { + "use strict"; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + + if (len === 0) { + return -1; + } + + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n !== 0 && n !== (Infinity) && n !== -(Infinity)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + + if (n >= len) { + return -1; + } + + var k = n >= 0 + ? n + : Math.max(len - Math.abs(n), 0); + + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + + return -1; + }; +} + +// Instantiate the object +var I18n = I18n || {}; + +// Set default locale to english +I18n.defaultLocale = "en"; + +// Set default handling of translation fallbacks to false +I18n.fallbacks = false; + +// Set default separator +I18n.defaultSeparator = "."; + +// Set current locale to null +I18n.locale = null; + +// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. +I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; + +I18n.fallbackRules = { +}; + +I18n.pluralizationRules = { + en: function (n) { + return n == 0 ? ["zero", "none", "other"] : n == 1 ? "one" : "other"; + } +}; + +I18n.getFallbacks = function(locale) { + if (locale === I18n.defaultLocale) { + return []; + } else if (!I18n.fallbackRules[locale]) { + var rules = [] + , components = locale.split("-"); + + for (var l = 1; l < components.length; l++) { + rules.push(components.slice(0, l).join("-")); + } + + rules.push(I18n.defaultLocale); + + I18n.fallbackRules[locale] = rules; + } + + return I18n.fallbackRules[locale]; +} + +I18n.isValidNode = function(obj, node, undefined) { + return obj[node] !== null && obj[node] !== undefined; +}; + +I18n.lookup = function(scope, options) { + var options = options || {} + , lookupInitialScope = scope + , translations = this.prepareOptions(I18n.translations) + , locale = options.locale || I18n.currentLocale() + , messages = translations[locale] || {} + , options = this.prepareOptions(options) + , currentScope + ; + + if (typeof(scope) == "object") { + scope = scope.join(this.defaultSeparator); + } + + if (options.scope) { + scope = options.scope.toString() + this.defaultSeparator + scope; + } + + scope = scope.split(this.defaultSeparator); + + while (messages && scope.length > 0) { + currentScope = scope.shift(); + messages = messages[currentScope]; + } + + if (!messages) { + if (I18n.fallbacks) { + var fallbacks = this.getFallbacks(locale); + for (var fallback = 0; fallback < fallbacks.length; fallbacks++) { + messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options)); + if (messages) { + break; + } + } + } + + if (!messages && this.isValidNode(options, "defaultValue")) { + messages = options.defaultValue; + } + } + + return messages; +}; + +// Merge serveral hash options, checking if value is set before +// overwriting any value. The precedence is from left to right. +// +// I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"}); +// #=> {name: "John Doe", role: "user"} +// +I18n.prepareOptions = function() { + var options = {} + , opts + , count = arguments.length + ; + + for (var i = 0; i < count; i++) { + opts = arguments[i]; + + if (!opts) { + continue; + } + + for (var key in opts) { + if (!this.isValidNode(options, key)) { + options[key] = opts[key]; + } + } + } + + return options; +}; + +I18n.interpolate = function(message, options) { + options = this.prepareOptions(options); + var matches = message.match(this.PLACEHOLDER) + , placeholder + , value + , name + ; + + if (!matches) { + return message; + } + + for (var i = 0; placeholder = matches[i]; i++) { + name = placeholder.replace(this.PLACEHOLDER, "$1"); + + value = options[name]; + + if (!this.isValidNode(options, name)) { + value = "[missing " + placeholder + " value]"; + } + + regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}")); + message = message.replace(regex, value); + } + + return message; +}; + +I18n.translate = function(scope, options) { + options = this.prepareOptions(options); + var translation = this.lookup(scope, options); + + try { + if (typeof(translation) == "object") { + if (typeof(options.count) == "number") { + return this.pluralize(options.count, scope, options); + } else { + return translation; + } + } else { + return this.interpolate(translation, options); + } + } catch(err) { + return this.missingTranslation(scope); + } +}; + +I18n.localize = function(scope, value) { + switch (scope) { + case "currency": + return this.toCurrency(value); + case "number": + scope = this.lookup("number.format"); + return this.toNumber(value, scope); + case "percentage": + return this.toPercentage(value); + default: + if (scope.match(/^(date|time)/)) { + return this.toTime(scope, value); + } else { + return value.toString(); + } + } +}; + +I18n.parseDate = function(date) { + var matches, convertedDate; + + // we have a date, so just return it. + if (typeof(date) == "object") { + return date; + }; + + // it matches the following formats: + // yyyy-mm-dd + // yyyy-mm-dd[ T]hh:mm::ss + // yyyy-mm-dd[ T]hh:mm::ss + // yyyy-mm-dd[ T]hh:mm::ssZ + // yyyy-mm-dd[ T]hh:mm::ss+0000 + // + matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/); + + if (matches) { + for (var i = 1; i <= 6; i++) { + matches[i] = parseInt(matches[i], 10) || 0; + } + + // month starts on 0 + matches[2] -= 1; + + if (matches[7]) { + convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6])); + } else { + convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]); + } + } else if (typeof(date) == "number") { + // UNIX timestamp + convertedDate = new Date(); + convertedDate.setTime(date); + } else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) { + // a valid javascript format with timezone info + convertedDate = new Date(); + convertedDate.setTime(Date.parse(date)) + } else { + // an arbitrary javascript string + convertedDate = new Date(); + convertedDate.setTime(Date.parse(date)); + } + + return convertedDate; +}; + +I18n.toTime = function(scope, d) { + var date = this.parseDate(d) + , format = this.lookup(scope) + ; + + if (date.toString().match(/invalid/i)) { + return date.toString(); + } + + if (!format) { + return date.toString(); + } + + return this.strftime(date, format); +}; + +I18n.strftime = function(date, format) { + var options = this.lookup("date"); + + if (!options) { + return date.toString(); + } + + options.meridian = options.meridian || ["AM", "PM"]; + + var weekDay = date.getDay() + , day = date.getDate() + , year = date.getFullYear() + , month = date.getMonth() + 1 + , hour = date.getHours() + , hour12 = hour + , meridian = hour > 11 ? 1 : 0 + , secs = date.getSeconds() + , mins = date.getMinutes() + , offset = date.getTimezoneOffset() + , absOffsetHours = Math.floor(Math.abs(offset / 60)) + , absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60) + , timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes) + ; + + if (hour12 > 12) { + hour12 = hour12 - 12; + } else if (hour12 === 0) { + hour12 = 12; + } + + var padding = function(n) { + var s = "0" + n.toString(); + return s.substr(s.length - 2); + }; + + var f = format; + f = f.replace("%a", options.abbr_day_names[weekDay]); + f = f.replace("%A", options.day_names[weekDay]); + f = f.replace("%b", options.abbr_month_names[month]); + f = f.replace("%B", options.month_names[month]); + f = f.replace("%d", padding(day)); + f = f.replace("%e", day); + f = f.replace("%-d", day); + f = f.replace("%H", padding(hour)); + f = f.replace("%-H", hour); + f = f.replace("%I", padding(hour12)); + f = f.replace("%-I", hour12); + f = f.replace("%m", padding(month)); + f = f.replace("%-m", month); + f = f.replace("%M", padding(mins)); + f = f.replace("%-M", mins); + f = f.replace("%p", options.meridian[meridian]); + f = f.replace("%S", padding(secs)); + f = f.replace("%-S", secs); + f = f.replace("%w", weekDay); + f = f.replace("%y", padding(year)); + f = f.replace("%-y", padding(year).replace(/^0+/, "")); + f = f.replace("%Y", year); + f = f.replace("%z", timezoneoffset); + + return f; +}; + +I18n.toNumber = function(number, options) { + options = this.prepareOptions( + options, + this.lookup("number.format"), + {precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false} + ); + + var negative = number < 0 + , string = Math.abs(number).toFixed(options.precision).toString() + , parts = string.split(".") + , precision + , buffer = [] + , formattedNumber + ; + + number = parts[0]; + precision = parts[1]; + + while (number.length > 0) { + buffer.unshift(number.substr(Math.max(0, number.length - 3), 3)); + number = number.substr(0, number.length -3); + } + + formattedNumber = buffer.join(options.delimiter); + + if (options.precision > 0) { + formattedNumber += options.separator + parts[1]; + } + + if (negative) { + formattedNumber = "-" + formattedNumber; + } + + if (options.strip_insignificant_zeros) { + var regex = { + separator: new RegExp(options.separator.replace(/\./, "\\.") + "$") + , zeros: /0+$/ + }; + + formattedNumber = formattedNumber + .replace(regex.zeros, "") + .replace(regex.separator, "") + ; + } + + return formattedNumber; +}; + +I18n.toCurrency = function(number, options) { + options = this.prepareOptions( + options, + this.lookup("number.currency.format"), + this.lookup("number.format"), + {unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."} + ); + + number = this.toNumber(number, options); + number = options.format + .replace("%u", options.unit) + .replace("%n", number) + ; + + return number; +}; + +I18n.toHumanSize = function(number, options) { + var kb = 1024 + , size = number + , iterations = 0 + , unit + , precision + ; + + while (size >= kb && iterations < 4) { + size = size / kb; + iterations += 1; + } + + if (iterations === 0) { + unit = this.t("number.human.storage_units.units.byte", {count: size}); + precision = 0; + } else { + unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]); + precision = (size - Math.floor(size) === 0) ? 0 : 1; + } + + options = this.prepareOptions( + options, + {precision: precision, format: "%n%u", delimiter: ""} + ); + + number = this.toNumber(size, options); + number = options.format + .replace("%u", unit) + .replace("%n", number) + ; + + return number; +}; + +I18n.toPercentage = function(number, options) { + options = this.prepareOptions( + options, + this.lookup("number.percentage.format"), + this.lookup("number.format"), + {precision: 3, separator: ".", delimiter: ""} + ); + + number = this.toNumber(number, options); + return number + "%"; +}; + +I18n.pluralizer = function(locale) { + pluralizer = this.pluralizationRules[locale]; + if (pluralizer !== undefined) return pluralizer; + return this.pluralizationRules["en"]; +}; + +I18n.findAndTranslateValidNode = function(keys, translation) { + for (i = 0; i < keys.length; i++) { + key = keys[i]; + if (this.isValidNode(translation, key)) return translation[key]; + } + return null; +}; + +I18n.pluralize = function(count, scope, options) { + var translation; + + try { + translation = this.lookup(scope, options); + } catch (error) {} + + if (!translation) { + return this.missingTranslation(scope); + } + + var message; + options = this.prepareOptions(options); + options.count = count.toString(); + + pluralizer = this.pluralizer(this.currentLocale()); + key = pluralizer(Math.abs(count)); + keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key]; + + message = this.findAndTranslateValidNode(keys, translation); + if (message == null) message = this.missingTranslation(scope, keys[0]); + + return this.interpolate(message, options); +}; + +I18n.missingTranslation = function() { + var message = '[missing "' + this.currentLocale() + , count = arguments.length + ; + + for (var i = 0; i < count; i++) { + message += "." + arguments[i]; + } + + message += '" translation]'; + + return message; +}; + +I18n.currentLocale = function() { + return (I18n.locale || I18n.defaultLocale); +}; + +// shortcuts +I18n.t = I18n.translate; +I18n.l = I18n.localize; +I18n.p = I18n.pluralize; + diff --git a/app/assets/javascripts/vendor/jquery.timeago.js b/app/assets/javascripts/vendor/jquery.timeago.js new file mode 100644 index 00000000..9b7c93a5 --- /dev/null +++ b/app/assets/javascripts/vendor/jquery.timeago.js @@ -0,0 +1,142 @@ +/* + * timeago: a jQuery plugin, version: 0.9.2 (2010-09-14) + * @requires jQuery v1.2.3 or later + * + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. '4 minutes ago' or 'about 1 day ago'). + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Licensed under the MIT: + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright (c) 2008-2010, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org) + */ +(function($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) return inWords(timestamp); + else if (typeof timestamp == 'string') return inWords($.timeago.parse(timestamp)); + else return inWords($.timeago.datetime(timestamp)); + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 3000, + allowFuture: true, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'ago', + suffixFromNow: 'from now', + seconds: 'less than a minute', + minute: 'about a minute', + minutes: '%d minutes', + hour: 'about an hour', + hours: 'about %d hours', + day: 'a day', + days: '%d days', + month: 'about a month', + months: '%d months', + year: 'about a year', + years: '%d years', + numbers: [] + } + }, + distanceInWords: function(date) { + if(!date) { + return; + } + if(typeof date == 'string') { + date = $.timeago.parse(date); + } + return $.timeago.inWords($.timeago.distance(date)); + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + distanceMillis = Math.abs(distanceMillis); + } + + var seconds = distanceMillis / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 48 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.floor(days)) || + days < 60 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.floor(days / 30)) || + years < 2 && substitute($l.year, 1) || + substitute($l.years, Math.floor(years)); + + return $.trim([prefix, words, suffix].join(' ')); + }, + distance: function(date) { + return (this.now() - date.getTime()); + }, + now: function() { + return new Date().getTime(); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d\d\d+/,''); // remove milliseconds + s = s.replace(/-/,'/').replace(/-/,'/'); + s = s.replace(/T/,' ').replace(/Z/,' UTC'); + s = s.replace(/([\+-]\d\d)\:?(\d\d)/,' $1$2'); // -04:00 -> -0400 + return new Date(s); + } + }); + + $.fn.timeago = function() { + this.each(function() { + var data = prepareData(this); + if (!isNaN(data.datetime)) { + $(this).text(inWords(data.datetime)); + } + }); + return this; + }; + + function prepareData(element) { + element = $(element); + if (!element.data('timeago') || (element.data('timeago').title != element.attr('title'))) { + element.data('timeago', { datetime: $t.parse(element.attr('title')), title: element.attr('title') }); + } + return element.data('timeago'); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return $t.distance(date); + } + + // fix for IE6 suckage + document.createElement('abbr'); + document.createElement('time'); +})(jQuery); + + + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 8255cee0..276fefc0 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -33,7 +33,7 @@ body { } #main { - width: 400px; + width: 800px; padding-left: 1em; } @@ -59,3 +59,40 @@ li { clear: both; padding-top: 20px; } + +.github-stats { + float: right; + width: 100px; +} +.github-stats li { + float: left; + margin-left: 20px; +} + +.summary .left, +.summary .right { + float: left; + width: 350px; +} + +dt { + clear: both; + float: left; + width: 60px; +} +dd { + float: left; +} + +.green { + border-top: 5px solid lightgreen; +} +.red { + border-top: 5px solid red; +} + +#jobs, +.log { + clear: both; + padding-top: 20px; +}