├── .rspec ├── spec ├── dummy │ ├── log │ │ └── .gitkeep │ ├── app │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── delayed_jobs │ │ │ │ ├── dummy_job.rb │ │ │ │ └── other_dummy_job.rb │ │ │ └── scheduled_jobs │ │ │ │ ├── logging_job.rb │ │ │ │ ├── dummy_job.rb │ │ │ │ ├── dummy_scheduled_job.rb │ │ │ │ └── dummy_heroku_job.rb │ │ ├── views │ │ │ ├── home │ │ │ │ └── index.html.haml │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── controllers │ │ │ ├── home_controller.rb │ │ │ └── application_controller.rb │ │ └── assets │ │ │ ├── stylesheets │ │ │ └── application.css │ │ │ └── javascripts │ │ │ └── application.js │ ├── lib │ │ └── assets │ │ │ └── .gitkeep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── db │ │ ├── test.sqlite3 │ │ └── development.sqlite3 │ ├── config │ │ ├── initializers │ │ │ ├── jobbr.rb │ │ │ ├── mime_types.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ ├── secret_token.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── routes.rb │ │ ├── environment.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── boot.rb │ │ ├── schedule.rb │ │ ├── database.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ ├── application.rb │ │ └── mongoid.yml │ ├── config.ru │ ├── Rakefile │ ├── bin │ │ └── rails │ └── README.rdoc ├── support │ └── generator_destination_root.rb ├── generators │ ├── scheduled_job_config_generator_spec.rb │ ├── delayed_job_generator_spec.rb │ ├── scheduled_job_generator_spec.rb │ └── initializer_generator_spec.rb ├── tasks │ ├── jobbr_heroku_tasks_spec.rb │ └── jobbr_tasks_spec.rb ├── controllers │ └── delayed_jobs_controller_spec.rb ├── models │ ├── delayed_spec.rb │ ├── job_spec.rb │ └── scheduled_spec.rb ├── spec_helper.rb └── features │ └── job_list_feature_spec.rb ├── app ├── assets │ ├── images │ │ └── jobbr │ │ │ └── .gitkeep │ ├── javascripts │ │ └── jobbr │ │ │ └── application.js.coffee │ └── stylesheets │ │ └── jobbr │ │ └── application.css.scss ├── controllers │ └── jobbr │ │ ├── runs_controller.rb │ │ ├── application_controller.rb │ │ ├── jobs_controller.rb │ │ └── delayed_jobs_controller.rb ├── views │ ├── jobbr │ │ ├── runs │ │ │ ├── _logs.html.haml │ │ │ └── show.html.haml │ │ └── jobs │ │ │ ├── index.html.haml │ │ │ ├── _job_list.html.haml │ │ │ └── show.html.haml │ └── layouts │ │ └── jobbr │ │ └── application.html.haml ├── models │ └── jobbr │ │ ├── log_message.rb │ │ ├── delayed.rb │ │ ├── scheduled.rb │ │ ├── run.rb │ │ └── job.rb └── helpers │ └── jobbr │ └── application_helper.rb ├── lib ├── jobbr.rb ├── jobbr │ ├── version.rb │ ├── engine.rb │ ├── whenever.rb │ ├── ohm_pagination.rb │ ├── logger.rb │ └── ohm.rb ├── generators │ └── jobbr │ │ ├── initializer │ │ ├── templates │ │ │ └── jobbr.rb │ │ ├── initializer_generator.rb │ │ └── USAGE │ │ ├── delayed_job │ │ ├── templates │ │ │ └── delayed_job.erb │ │ ├── USAGE │ │ └── delayed_job_generator.rb │ │ ├── scheduled_job_config │ │ ├── USAGE │ │ ├── scheduled_job_config_generator.rb │ │ └── templates │ │ │ └── schedule.rb │ │ └── scheduled_job │ │ ├── templates │ │ └── scheduled_job.erb │ │ ├── USAGE │ │ └── scheduled_job_generator.rb └── tasks │ ├── jobbr_tasks.rake │ └── jobbr_heroku_tasks.rake ├── .travis.yml ├── config ├── routes.rb └── locales │ └── jobbr.en.yml ├── .gitignore ├── script └── rails ├── bin └── jobbr ├── Gemfile ├── Rakefile ├── MIT-LICENSE ├── jobbr.gemspec ├── README.rdoc └── Gemfile.lock /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/dummy/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/jobbr/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/home/index.html.haml: -------------------------------------------------------------------------------- 1 | = delayed_job_polling_path -------------------------------------------------------------------------------- /lib/jobbr.rb: -------------------------------------------------------------------------------- 1 | require "jobbr/engine" 2 | 3 | module Jobbr 4 | end 5 | -------------------------------------------------------------------------------- /lib/jobbr/version.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | VERSION = '2.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.1 4 | services: 5 | - redis-server 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | 3 | end -------------------------------------------------------------------------------- /spec/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/jobbr/master/spec/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/db/development.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/jobbr/master/spec/dummy/db/development.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/jobbr.rb: -------------------------------------------------------------------------------- 1 | require 'ohm' 2 | require 'ohm/contrib' 3 | require 'jobbr/logger' 4 | require 'jobbr/ohm' 5 | require 'jobbr/ohm_pagination' 6 | -------------------------------------------------------------------------------- /lib/generators/jobbr/initializer/templates/jobbr.rb: -------------------------------------------------------------------------------- 1 | require 'ohm' 2 | require 'ohm/contrib' 3 | require 'jobbr/logger' 4 | require 'jobbr/ohm' 5 | require 'jobbr/ohm_pagination' 6 | 7 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | resource :home 4 | 5 | root to: 'home#index' 6 | 7 | mount Jobbr::Engine => "/jobbr" 8 | 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Jobbr::Engine.routes.draw do 2 | 3 | root to: 'jobs#index' 4 | 5 | resources :jobs do 6 | resources :runs 7 | end 8 | 9 | resources :delayed_jobs 10 | 11 | end 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .rbenv-version 3 | .bundle/ 4 | log/*.log 5 | pkg/ 6 | spec/dummy/db/*.sqlite3 7 | spec/dummy/log/*.log 8 | spec/dummy/tmp/ 9 | spec/dummy/.sass-cache 10 | *.gem 11 | coverage 12 | .idea 13 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/support/generator_destination_root.rb: -------------------------------------------------------------------------------- 1 | # mixin setting destination root needed by generator_spec 2 | module GeneratorDestinationRoot 3 | 4 | def initialize(*args) 5 | super(*args) 6 | self.destination_root = SPEC_TMP_ROOT 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/jobbr/runs_controller.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class RunsController < Jobbr::ApplicationController 4 | 5 | def show 6 | @job = Job.by_name(params[:job_id]) 7 | @run = @job.runs[params[:id]] 8 | end 9 | 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/app/models/delayed_jobs/dummy_job.rb: -------------------------------------------------------------------------------- 1 | module DelayedJobs 2 | 3 | class DummyJob < Jobbr::Job 4 | 5 | include Jobbr::Delayed 6 | 7 | def perform(run, params) 8 | run.logger.debug 'job is running' 9 | end 10 | 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/generators/scheduled_job_config_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jobbr::ScheduledJobConfigGenerator, type: :generator do 4 | 5 | it 'creates job model' do 6 | run_generator 7 | assert_file 'config/schedule.rb' 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/jobbr/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | class ApplicationController < ActionController::Base 3 | 4 | before_filter :set_locale 5 | 6 | protected 7 | 8 | def set_locale 9 | I18n.locale = :en 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/jobbr/delayed_job/templates/delayed_job.erb: -------------------------------------------------------------------------------- 1 | module DelayedJobs 2 | 3 | class <%=name.camelize%>Job < Jobbr::Job 4 | 5 | include Jobbr::Delayed 6 | 7 | def perform(run, params) 8 | # put your job code here 9 | end 10 | 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/jobbr/initializer/initializer_generator.rb: -------------------------------------------------------------------------------- 1 | class Jobbr::InitializerGenerator < Rails::Generators::Base 2 | source_root File.expand_path('../templates', __FILE__) 3 | 4 | def copy_config_file 5 | copy_file "jobbr.rb", "config/initializers/jobbr.rb" 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/jobbr/scheduled_job_config/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generate Whenever schedule.rb config file 3 | 4 | Example: 5 | rails generate scheduled_job_config 6 | 7 | This will create: 8 | Config: config/schedule.rb 9 | Config: config/initializers/jobbr.rb 10 | -------------------------------------------------------------------------------- /spec/dummy/app/models/scheduled_jobs/logging_job.rb: -------------------------------------------------------------------------------- 1 | module ScheduledJobs 2 | 3 | class LoggingJob < Jobbr::Job 4 | 5 | include Jobbr::Scheduled 6 | 7 | def perform(run) 8 | run.logger.debug 'foo' 9 | run.logger.error 'bar' 10 | end 11 | 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /app/views/jobbr/runs/_logs.html.haml: -------------------------------------------------------------------------------- 1 | %h5= title 2 | .well.logs{class: size} 3 | - run.ordered_messages.each do |msg| 4 | %span.date{class: msg.kind}= "[#{msg.created_at.localtime.strftime('%H:%M:%S')}]" 5 | %span.kind{class: msg.kind}= "[#{msg.kind}]" 6 | %span.message= raw(msg.message) 7 | %br 8 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/generators/delayed_job_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jobbr::DelayedJobGenerator, type: :generator do 4 | 5 | arguments %w(foo) 6 | 7 | it 'creates job model' do 8 | run_generator 9 | assert_file 'app/models/delayed_jobs/foo_job.rb' 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /app/models/jobbr/log_message.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class LogMessage < ::Ohm::Model 4 | 5 | include ::Ohm::Timestamps 6 | include ::Ohm::DataTypes 7 | 8 | attribute :kind, Type::Symbol 9 | attribute :message 10 | 11 | reference :run, 'Jobbr::Run' 12 | 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/models/delayed_jobs/other_dummy_job.rb: -------------------------------------------------------------------------------- 1 | module DelayedJobs 2 | 3 | class OtherDummyJob < Jobbr::Job 4 | 5 | include Jobbr::Delayed 6 | 7 | queue :critical 8 | 9 | def perform(run, params) 10 | run.logger.debug 'job is running' 11 | end 12 | 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/generators/scheduled_job_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jobbr::ScheduledJobGenerator, type: :generator do 4 | 5 | arguments %w(foo) 6 | 7 | it 'creates job model' do 8 | run_generator 9 | assert_file 'app/models/scheduled_jobs/foo_job.rb' 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/jobbr/delayed_job/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates a new delayed job in DelayedJobs namespace. 3 | 4 | Example: 5 | rails generate delayed_job MySample 6 | 7 | This will create: 8 | Model: app/models/delayed_jobs/my_sample_job.rb 9 | Config: config/initializers/jobbr.rb 10 | -------------------------------------------------------------------------------- /spec/generators/initializer_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jobbr::InitializerGenerator, type: :generator do 4 | 5 | before do 6 | run_generator 7 | end 8 | 9 | it 'creates jobbr initializer file' do 10 | assert_file 'config/initializers/jobbr.rb' 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/jobbr/initializer/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates a jobbr initializer to require various dependencies. 3 | Will also create a Whenever configuration file. 4 | 5 | Example: 6 | rails generate scheduled_job MySample 7 | 8 | This will create: 9 | Config: config/initializers/jobbr.rb 10 | -------------------------------------------------------------------------------- /lib/tasks/jobbr_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :jobbr do 2 | 3 | desc 'Mark all running job as failed.' 4 | task :sweep_running_jobs => :environment do 5 | 6 | Jobbr::Run.find(status: :running).union(status: :waiting).each do |run| 7 | run.status = :failed 8 | run.save 9 | end 10 | 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/jobbr/engine', __FILE__) 6 | 7 | require 'rails/all' 8 | require 'rails/engine/commands' 9 | -------------------------------------------------------------------------------- /spec/dummy/app/models/scheduled_jobs/dummy_job.rb: -------------------------------------------------------------------------------- 1 | module ScheduledJobs 2 | 3 | class DummyJob < Jobbr::Job 4 | 5 | include Jobbr::Scheduled 6 | 7 | # description 'Describe your job here' 8 | 9 | # every 1.day, at: '5am' 10 | 11 | def perform 12 | # put your job code here 13 | end 14 | 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/jobbr/engine.rb: -------------------------------------------------------------------------------- 1 | require 'jobbr/logger' 2 | 3 | module Jobbr 4 | class Engine < ::Rails::Engine 5 | 6 | isolate_namespace Jobbr 7 | 8 | initializer 'jobbr.action_controller' do |app| 9 | ActiveSupport.on_load :action_controller do 10 | helper Jobbr::ApplicationHelper 11 | end 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/jobbr/jobs/index.html.haml: -------------------------------------------------------------------------------- 1 | %ol.breadcrumb 2 | %li.active= t('.title') 3 | 4 | - unless @scheduled_jobs.empty? 5 | = render 'job_list', title: t('.scheduled_jobs'), jobs: @scheduled_jobs, css_class: 'scheduled-jobs' 6 | 7 | - unless @delayed_jobs.empty? 8 | = render 'job_list', title: t('.delayed_jobs'), jobs: @delayed_jobs, css_class: 'delayed-jobs' 9 | -------------------------------------------------------------------------------- /spec/dummy/app/models/scheduled_jobs/dummy_scheduled_job.rb: -------------------------------------------------------------------------------- 1 | module ScheduledJobs 2 | 3 | class DummyScheduledJob < Jobbr::Job 4 | 5 | include Jobbr::Scheduled 6 | 7 | description 'A dummy Job' 8 | 9 | every 1.day, at: '5.30 am' 10 | 11 | def perform(run) 12 | run.logger.debug 'Dummy!!!!' 13 | end 14 | 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/jobbr/scheduled_job/templates/scheduled_job.erb: -------------------------------------------------------------------------------- 1 | module ScheduledJobs 2 | 3 | class <%=name.camelize%>Job < Jobbr::Job 4 | 5 | include Jobbr::Scheduled 6 | 7 | # description 'Describe your job here' 8 | 9 | # every 1.day, at: '5am' 10 | 11 | def perform 12 | # put your job code here 13 | end 14 | 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/jobbr/scheduled_job_config/scheduled_job_config_generator.rb: -------------------------------------------------------------------------------- 1 | class Jobbr::ScheduledJobConfigGenerator < Rails::Generators::Base 2 | source_root File.expand_path('../templates', __FILE__) 3 | 4 | def copy_config_file 5 | copy_file "schedule.rb", "config/schedule.rb" 6 | generate 'jobbr:initializer' unless Rails.env.test? 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/models/scheduled_jobs/dummy_heroku_job.rb: -------------------------------------------------------------------------------- 1 | module ScheduledJobs 2 | 3 | class DummyHerokuJob < Jobbr::Job 4 | 5 | include Jobbr::Scheduled 6 | 7 | description 'Describe your job here' 8 | 9 | heroku_run :daily, priority: 0 10 | 11 | def perform(run) 12 | run.logger.debug 'heroku :)' 13 | end 14 | 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/config/schedule.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = ARGV.first || ENV['RAILS_ENV'] || 'development' 2 | require File.expand_path(File.dirname(__FILE__) + '/../config/environment') 3 | require 'jobbr/whenever' 4 | 5 | set :output, "#{path}/log/cron.log" 6 | job_type :jobbr, 'cd :path && RAILS_ENV=:environment bundle exec jobbr :task :output' 7 | 8 | Jobbr::Whenever.schedule_jobs(self) 9 | -------------------------------------------------------------------------------- /lib/generators/jobbr/scheduled_job_config/templates/schedule.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = ARGV.first || ENV['RAILS_ENV'] || 'development' 2 | require File.expand_path(File.dirname(__FILE__) + '/../config/environment') 3 | require 'jobbr/whenever' 4 | 5 | set :output, "#{path}/log/cron.log" 6 | job_type :jobbr, 'cd :path && RAILS_ENV=:environment bundle exec jobbr :task :output' 7 | 8 | Jobbr::Whenever.schedule_jobs(self) 9 | -------------------------------------------------------------------------------- /lib/generators/jobbr/scheduled_job/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates a new scheduled job in ScheduledJobs namespace. 3 | Will also create a Whenever configuration file. 4 | 5 | Example: 6 | rails generate scheduled_job MySample 7 | 8 | This will create: 9 | Model: app/models/scheduled_jobs/my_sample_job.rb 10 | Config: config/schedule.rb 11 | Config: config/initializers/jobbr.rb 12 | -------------------------------------------------------------------------------- /app/models/jobbr/delayed.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module Jobbr 4 | 5 | module Delayed 6 | 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | 11 | include Sidekiq::Extensions::ActiveRecord 12 | 13 | end 14 | 15 | module ClassMethods 16 | 17 | def queue(queue = nil) 18 | @queue = queue.to_sym if queue 19 | @queue 20 | end 21 | 22 | end 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/generators/jobbr/delayed_job/delayed_job_generator.rb: -------------------------------------------------------------------------------- 1 | class Jobbr::DelayedJobGenerator < Rails::Generators::NamedBase 2 | 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def create_delayed_job 6 | empty_directory "app/models/delayed_jobs" 7 | template "delayed_job.erb", "app/models/delayed_jobs/#{file_name}_job.rb", name: file_name 8 | generate 'jobbr:initializer' unless Rails.env.test? 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/tasks/jobbr_heroku_tasks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rake' 3 | 4 | describe 'jobbr_heroku_tasks' do 5 | 6 | before do 7 | Rake.application.rake_require 'tasks/jobbr_heroku_tasks' 8 | Rake::Task.define_task(:environment) 9 | end 10 | 11 | it 'runs daily jobs' do 12 | expect { 13 | Rake.application.invoke_task 'jobbr:heroku:daily' 14 | }.to change { Jobbr::Run.all.count }.from(0).to(1) 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /lib/generators/jobbr/scheduled_job/scheduled_job_generator.rb: -------------------------------------------------------------------------------- 1 | class Jobbr::ScheduledJobGenerator < Rails::Generators::NamedBase 2 | 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def create_scheduled_job 6 | empty_directory "app/models/scheduled_jobs" 7 | template "scheduled_job.erb", "app/models/scheduled_jobs/#{file_name}_job.rb", name: file_name 8 | generate 'jobbr:scheduled_job_config' unless Rails.env.test? 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /app/views/jobbr/runs/show.html.haml: -------------------------------------------------------------------------------- 1 | %ol.breadcrumb 2 | %li 3 | = link_to t('jobbr.jobs.index.title'), jobs_path 4 | %li 5 | = link_to @job.name.humanize, job_path(@job) 6 | %li.active 7 | - if @run.started_at 8 | = l @run.started_at 9 | 10 | = status_icon(@run.status) 11 | 12 | - if @run.run_time 13 | .well 14 | = t('.run_time', run_time: ChronicDuration.output(@run.run_time.round(2), format: :long)) 15 | 16 | = render 'logs', run: @run, title: t('.logs'), size: 'large' 17 | -------------------------------------------------------------------------------- /lib/jobbr/whenever.rb: -------------------------------------------------------------------------------- 1 | require 'jobbr/ohm' 2 | 3 | module Jobbr 4 | 5 | module Whenever 6 | 7 | extend self 8 | 9 | # Generates crontab for each scheduled Job using Whenever DSL. 10 | def schedule_jobs(job_list) 11 | Jobbr::Ohm.models(Jobbr::Scheduled).each do |job| 12 | if job.every 13 | job_list.every job.every[0], job.every[1] do 14 | job_list.jobbr job.task_name 15 | end 16 | end 17 | end 18 | end 19 | 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/jobbr/jobs_controller.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class JobsController < Jobbr::ApplicationController 4 | 5 | def index 6 | @scheduled_jobs = Jobbr::Job.scheduled 7 | @delayed_jobs = Jobbr::Job.delayed 8 | end 9 | 10 | def show 11 | if @job = Job.by_name(params[:id]) 12 | @runs = Jobbr::OhmPagination.new(@job.runs).sort_by(:started_at).order('ALPHA DESC').per(10).page(params[:page]) 13 | @last_run = @job.last_run 14 | end 15 | end 16 | 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/jobbr/delayed_jobs_controller.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class DelayedJobsController < Jobbr::ApplicationController 4 | 5 | def create 6 | params.merge!(current_user_id: current_user.id.to_s) rescue nil 7 | @run = Job.run_by_name(params[:job_name], params) 8 | render json: { id: @run.id.to_s } 9 | end 10 | 11 | def show 12 | job_run = Run[params[:id]] 13 | render json: { status: job_run.status, result: job_run.result, progress: job_run.progress } 14 | end 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '282ec86f2f30ae3fce5193f0eafbd7bdd78f83d05b42fe6f2305df1d7b6565407a439d9bb415edbde10e213aa39b8c715ee0c278165302ddbd89aa8b6b7fbc79' 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /bin/jobbr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | 5 | opt_parser = OptionParser.new do |opts| 6 | opts.banner = 'Usage: jobbr [job_name]' 7 | 8 | opts.on('-v', '--version') do 9 | puts "Jobbr v#{Jobbr::VERSION}" 10 | exit 11 | end 12 | 13 | opts.on('-h', '--help', 'Show this message') do 14 | puts opts 15 | exit 16 | end 17 | 18 | end 19 | 20 | opt_parser.parse! 21 | 22 | if ARGV.length == 1 23 | require File.expand_path('config/environment') 24 | unless ARGV[0].start_with?('scheduled_jobs') 25 | ARGV[0] = "scheduled_jobs/#{ARGV[0]}" 26 | end 27 | Jobbr::Job.run_by_name(ARGV[0]) 28 | else 29 | puts opt_parser 30 | end 31 | 32 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/tasks/jobbr_heroku_tasks.rake: -------------------------------------------------------------------------------- 1 | require 'jobbr/ohm.rb' 2 | 3 | namespace :jobbr do 4 | 5 | namespace :heroku do 6 | 7 | desc 'Run all minutely Heroku jobs' 8 | task :minutely => :environment do 9 | run_heroku_scheduled_classes(:minutely) 10 | end 11 | 12 | desc 'Run all hourly Heroku jobs' 13 | task :hourly => :environment do 14 | run_heroku_scheduled_classes(:hourly) 15 | end 16 | 17 | desc 'Run all daily Heroku jobs' 18 | task :daily => :environment do 19 | run_heroku_scheduled_classes(:daily) 20 | end 21 | 22 | def run_heroku_scheduled_classes(frequency) 23 | Jobbr::Ohm.models(Jobbr::Scheduled).select{|c| c.heroku_frequency == frequency }.sort{|a,b| b.heroku_priority <=> a.heroku_priority}.each(&:run) 24 | end 25 | 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '>= 4.0.0' 6 | 7 | # UI 8 | gem 'jquery-rails' 9 | gem 'haml' 10 | gem 'chronic_duration' 11 | gem 'sass-rails', '>= 4.0.2' 12 | gem 'coffee-rails' 13 | gem 'therubyracer' 14 | gem 'less-rails' 15 | gem 'bootstrap-sass' 16 | gem 'font-awesome-rails' 17 | gem 'turbolinks' 18 | gem 'kaminari' 19 | 20 | # Backend 21 | gem 'redis' 22 | gem 'ohm', '>= 2.0.1' 23 | gem 'ohm-contrib' 24 | gem 'sidekiq' 25 | gem 'whenever' 26 | gem 'require_all' 27 | 28 | group :development do 29 | gem 'unicorn' 30 | end 31 | 32 | group :test do 33 | gem 'rspec-rails', '~> 2.99' 34 | gem 'mocha' 35 | gem 'generator_spec' 36 | gem 'codeclimate-test-reporter', require: 'nil' 37 | gem 'capybara' 38 | gem 'poltergeist' 39 | gem 'launchy' 40 | gem 'timecop' 41 | end 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 9 | load APP_RAKEFILE 10 | 11 | # === Gems install tasks === 12 | Bundler::GemHelper.install_tasks 13 | 14 | desc 'Run Rspec tests' 15 | task :default do 16 | ["rspec spec"].each do |cmd| 17 | puts "Starting to run #{cmd}..." 18 | system("export DISPLAY=:99.0 && bundle exec #{cmd}") 19 | raise "#{cmd} failed!" unless $?.exitstatus == 0 20 | end 21 | end 22 | 23 | task :push_gem do 24 | puts "Building gem (version: #{Jobbr::VERSION})" 25 | system "gem build jobbr.gemspec" 26 | puts 'Pushing to rubygems.org' 27 | system "gem push jobbr-#{Jobbr::VERSION}.gem" 28 | end -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /app/models/jobbr/scheduled.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module Jobbr 4 | 5 | module Scheduled 6 | 7 | extend ActiveSupport::Concern 8 | 9 | module ClassMethods 10 | 11 | def every(every = nil, options = {}) 12 | @every = [every, options] if every 13 | @every 14 | end 15 | 16 | # heroku frequency can be :minutely, :hourly or :daily 17 | def heroku_run(frequency, options = {}) 18 | @heroku_frequency = frequency 19 | @heroku_priority = options[:priority] || 0 20 | end 21 | 22 | def heroku_frequency 23 | @heroku_frequency 24 | end 25 | 26 | def heroku_priority 27 | @heroku_priority 28 | end 29 | 30 | def task_name 31 | name.demodulize.underscore 32 | end 33 | 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/controllers/delayed_jobs_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jobbr 4 | 5 | describe DelayedJobsController do 6 | 7 | routes { Jobbr::Engine.routes } 8 | 9 | it 'creates a new job by its name' do 10 | expect { 11 | post :create, {job_name: 'delayed_jobs/dummy_job'} 12 | }.to change{ Job.count }.by(1) 13 | 14 | job = DelayedJobs::DummyJob.instance 15 | job.runs.count.should == 1 16 | 17 | JSON.parse(response.body)['id'].should == job.runs.first.id 18 | end 19 | 20 | it 'returns a job run status' do 21 | DelayedJobs::DummyJob.run({}) 22 | 23 | run = Run.all.first 24 | get :show, {id: run.id} 25 | 26 | json = JSON.parse(response.body) 27 | json['status'].should == 'success' 28 | json['progress'].should == 100 29 | json['result'].should be_nil 30 | end 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/views/layouts/jobbr/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | !!! 3 | %html{ :xmlns => 'http://www.w3.org/1999/xhtml' } 4 | %head 5 | %title JobbR 6 | 7 | = csrf_meta_tags 8 | 9 | :javascript 10 | function getURLParameter(name) { 11 | return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null 12 | } 13 | 14 | = stylesheet_link_tag 'jobbr/application' 15 | = javascript_include_tag 'jobbr/application' 16 | 17 | %body 18 | .navbar.navbar-default 19 | .navbar-header 20 | %a.navbar-brand= t('.title') 21 | .collapse.navbar-collapse 22 | .nav.navbar-nav.navbar-right 23 | %a.btn.btn-default#auto-refresh{type: 'button', 'data-toggle' => 'button', href: '#'} 24 | = fa_icon 'refresh' 25 | = t('.auto_refresh') 26 | #main.container 27 | = yield 28 | -------------------------------------------------------------------------------- /app/views/jobbr/jobs/_job_list.html.haml: -------------------------------------------------------------------------------- 1 | %h5= title 2 | 3 | %table.table.table-striped.table-hover{class: css_class} 4 | %thead 5 | %tr 6 | %th= t('.status') 7 | %th= t('.job_name') 8 | %th= t('.last_run') 9 | %th= t('.average_run_time') 10 | %th 11 | 12 | %tbody 13 | - jobs.each do |job| 14 | %tr 15 | %td 16 | = status_icon(job.last_run.status) 17 | %td= job.name 18 | - if job.last_run && job.last_run.started_at 19 | %td= l job.last_run.started_at.localtime 20 | %td= ChronicDuration.output(job.average_run_time) 21 | %td 22 | .btn-toolbar 23 | .btn-group 24 | = link_to job_path(job), class: 'btn all-runs', title: t('.see_all_runs') do 25 | = fa_icon 'list' 26 | = link_to job_run_path(job, job.last_run), class: 'btn last-run', title: t('.see_last_run') do 27 | = fa_icon 'download' 28 | -------------------------------------------------------------------------------- /spec/models/delayed_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jobbr 4 | 5 | describe Delayed do 6 | 7 | it 'creates a new job by its name' do 8 | expect { 9 | Job.run_by_name('delayed_jobs/dummy_job', {}, false) 10 | }.to change{ Job.count }.by(1) 11 | 12 | job = DelayedJobs::DummyJob.instance 13 | job.runs.count.should == 1 14 | job.runs.first.messages.count.should == 2 15 | end 16 | 17 | it 'does not create duplicated name jobs' do 18 | expect { 19 | Job.run_by_name('delayed_jobs/dummy_job', {}, false) 20 | Job.run_by_name('delayed_jobs/dummy_job', {}, false) 21 | Job.run_by_name('delayed_jobs/dummy_job', {}, false) 22 | }.to change{ Job.all.count }.by(1) 23 | 24 | expect { 25 | Job.run_by_name('delayed_jobs/other_dummy_job', {}, false) 26 | Job.run_by_name('delayed_jobs/other_dummy_job', {}, false) 27 | }.to change{ Job.all.count }.by(1) 28 | end 29 | 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/jobbr/ohm_pagination.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class OhmPagination 4 | 5 | attr_accessor :array, :current_page, :limit_value, :total_items, :sort_by, :order 6 | 7 | def initialize(array) 8 | @array = array 9 | @total_items = array.count 10 | end 11 | 12 | def page(page) 13 | @current_page = [page.to_i, 1].max 14 | self 15 | end 16 | 17 | def sort_by(sort_by) 18 | @sort_by = sort_by 19 | self 20 | end 21 | 22 | def order(order) 23 | @order = order 24 | self 25 | end 26 | 27 | def per(limit_value) 28 | @limit_value = limit_value.to_i 29 | self 30 | end 31 | 32 | def total_pages 33 | @total_pages ||= (total_items.to_f / limit_value).ceil 34 | end 35 | 36 | def each 37 | return unless block_given? 38 | limit_start = (current_page - 1) * limit_value 39 | array.sort_by(@sort_by, order: @order, limit: [limit_start, limit_value]).each do |item| 40 | yield(item) 41 | end 42 | end 43 | 44 | end 45 | 46 | end -------------------------------------------------------------------------------- /app/models/jobbr/run.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class Run < ::Ohm::Model 4 | 5 | include ::Ohm::Timestamps 6 | include ::Ohm::DataTypes 7 | include ::Ohm::Callbacks 8 | 9 | attribute :status, Type::Symbol 10 | attribute :started_at, Type::Time 11 | attribute :finished_at, Type::Time 12 | attribute :progress, Type::Integer 13 | attribute :result 14 | 15 | reference :job, 'Jobbr::Job' 16 | collection :messages, 'Jobbr::LogMessage' 17 | 18 | index :status 19 | 20 | def run_time 21 | @run_time ||= if finished_at && started_at 22 | finished_at - started_at 23 | else 24 | nil 25 | end 26 | end 27 | 28 | def to_param 29 | id 30 | end 31 | 32 | def ordered_messages 33 | self.messages.sort_by(:created_at, order: 'ALPHA ASC') 34 | end 35 | 36 | def before_delete 37 | self.messages.each(&:delete) 38 | end 39 | 40 | def logger 41 | @logger ||= Jobbr::Logger.new(Rails.logger, self) 42 | end 43 | 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 YOURNAME 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/assets/javascripts/jobbr/application.js.coffee: -------------------------------------------------------------------------------- 1 | #= require jquery 2 | #= require jquery_ujs 3 | #= require turbolinks 4 | #= require bootstrap 5 | 6 | #= require_self 7 | 8 | autoRefreshInterval = 3000 9 | timeout = undefined 10 | 11 | scrollToBottom = ($container) -> 12 | if $container.length > 0 13 | $container.scrollTop($container[0].scrollHeight) 14 | 15 | toggleRefreshButton = -> 16 | $('#auto-refresh').toggleClass('active') 17 | $('#auto-refresh i').toggleClass('fa-spin') 18 | $('#auto-refresh').hasClass('active') 19 | 20 | enableAutoRefresh = (force = false) -> 21 | if force || (getURLParameter('refresh') == '1') 22 | if toggleRefreshButton() 23 | timeout = setTimeout(-> 24 | Turbolinks.visit("#{document.location.pathname}?refresh=1") 25 | , autoRefreshInterval) 26 | else 27 | clearTimeout(timeout) 28 | Turbolinks.visit(document.location.pathname) 29 | 30 | init = -> 31 | scrollToBottom($('.logs')) 32 | enableAutoRefresh() 33 | $('#auto-refresh').on 'click', -> enableAutoRefresh(true) 34 | 35 | $(document).ready -> 36 | 37 | init() 38 | $(document).on 'page:load', init 39 | -------------------------------------------------------------------------------- /config/locales/jobbr.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | 3 | time: 4 | formats: 5 | default: "%d %b %Y %H:%M:%S" 6 | 7 | layouts: 8 | jobbr: 9 | application: 10 | title: JobbR 11 | auto_refresh: Auto-refresh 12 | 13 | jobbr: 14 | 15 | jobs: 16 | 17 | index: 18 | title: Job list 19 | scheduled_jobs: Scheduled Jobs 20 | delayed_jobs: Delayed Jobs 21 | 22 | job_list: 23 | status: Status 24 | job_name: Name 25 | last_run: Last ran at 26 | average_run_time: Average run time 27 | see_all_runs: See all runs 28 | see_last_run: See last run 29 | 30 | show: 31 | status: Status 32 | last_run: Last run 33 | last_run_logs: Last run logs 34 | duration: Duration 35 | scheduling: "Scheduled every %{scheduling}" 36 | average_run_time: "Average duration: %{run_time}" 37 | see_run: See details for this run 38 | 39 | runs: 40 | show: 41 | run_time: "Run time: %{run_time}" 42 | previous_run: "Previous run" 43 | next_run: "Next run" 44 | -------------------------------------------------------------------------------- /lib/jobbr/logger.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class Logger 4 | 5 | attr_accessor :run, :wrapped_logger, :level 6 | 7 | def initialize(logger, run) 8 | self.wrapped_logger = logger 9 | self.run = run 10 | self.level = 0 11 | end 12 | 13 | def debug(message) 14 | wrapped_logger.debug(message) 15 | write_message(:debug, message) 16 | end 17 | 18 | def info(message) 19 | wrapped_logger.info(message) 20 | write_message(:info, message) 21 | end 22 | 23 | def warn(message) 24 | wrapped_logger.warn(message) 25 | write_message(:warn, message) 26 | end 27 | 28 | def error(message) 29 | wrapped_logger.error(message) 30 | write_message(:error, message) 31 | end 32 | 33 | def fatal(message) 34 | wrapped_logger.error(message) 35 | write_message(:fatal, message) 36 | end 37 | 38 | protected 39 | 40 | def write_message(kind, message) 41 | if message.is_a? Array 42 | message = message.join('
') 43 | end 44 | Jobbr::LogMessage.create(kind: kind, message: message, run: run) 45 | end 46 | 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /app/assets/stylesheets/jobbr/application.css.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap'; 2 | @import 'font-awesome'; 3 | 4 | .navbar-right { 5 | margin-top: 5px; 6 | .btn { 7 | margin-right: 5px; 8 | } 9 | } 10 | 11 | .job-status { 12 | 13 | margin-left: 15px; 14 | font-size: 18px; 15 | 16 | &.waiting { 17 | color: orange; 18 | } 19 | 20 | &.running, &.success { 21 | color: limegreen; 22 | } 23 | &.failed { 24 | color: red; 25 | } 26 | 27 | } 28 | 29 | .breadcrumb { 30 | 31 | .job-status { 32 | margin-left: 5px; 33 | font-size: 15px; 34 | } 35 | 36 | } 37 | 38 | .logs { 39 | background-color: #424242; 40 | color: white; 41 | font-family: Monaco, 'Courier New'; 42 | height: 200px; 43 | overflow: auto; 44 | 45 | .kind, .date { 46 | color: #AAAAAA; 47 | } 48 | .kind, .date { 49 | &.error, &.fatal { 50 | color: #FF8E8E; 51 | } 52 | &.warn { 53 | color: orange; 54 | } 55 | } 56 | 57 | &.large { 58 | height: 600px 59 | } 60 | 61 | } 62 | 63 | .table td { 64 | padding-top: 14px; 65 | } 66 | 67 | .btn-toolbar { 68 | margin: -5px 0 0; 69 | i { 70 | font-size: 14px; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/views/jobbr/jobs/show.html.haml: -------------------------------------------------------------------------------- 1 | %ol.breadcrumb 2 | %li 3 | = link_to t('jobbr.jobs.index.title'), jobs_path 4 | %li.active= @job.name.humanize 5 | 6 | .well 7 | - if @job.scheduled? && @job.every 8 | %p= raw t('.scheduling', scheduling: display_scheduling(@job)) 9 | = raw t('.average_run_time', run_time: ChronicDuration.output(@job.average_run_time, format: :long)) 10 | 11 | = render 'jobbr/runs/logs', run: @last_run, title: t('.last_run_logs'), size: 'small' 12 | 13 | %table.table.table-striped.table-hover 14 | %thead 15 | %tr 16 | %th= t('.status') 17 | %th= t('.last_run') 18 | %th= t('.duration') 19 | %th   20 | 21 | %tbody 22 | - @runs.each do |run| 23 | %tr 24 | %td 25 | = status_icon(run.status) 26 | - if run.started_at 27 | %td= l run.started_at.localtime 28 | %td= ChronicDuration.output((run.finished_at - run.started_at).round(2)) rescue 'N/A' 29 | %td 30 | .btn-toolbar 31 | .btn-group 32 | = link_to job_run_path(@job, run), class: 'btn see-run', title: t('.see_run') do 33 | = fa_icon 'download' 34 | 35 | = paginate @runs 36 | -------------------------------------------------------------------------------- /spec/models/job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jobbr 4 | 5 | describe Job do 6 | 7 | it "behaves consistently with polymorphism" do 8 | dummy_job = DelayedJobs::DummyJob.instance 9 | other_dummy_job = DelayedJobs::OtherDummyJob.instance 10 | 11 | Run::create(job: dummy_job) 12 | dummy_job.runs.size.should == 1 13 | other_dummy_job.runs.size.should == 0 14 | 15 | Run::create(job: other_dummy_job) 16 | dummy_job.runs.size.should == 1 17 | other_dummy_job.runs.size.should == 1 18 | end 19 | 20 | it "destroys related runs and logs when deleted" do 21 | expect { 22 | DelayedJobs::DummyJob.run({}) 23 | }.to change { Job.all.count + Run.all.count + LogMessage.all.count }.from(0) 24 | 25 | expect { 26 | DelayedJobs::DummyJob.instance.delete 27 | }.to change { Job.all.count + Run.all.count + LogMessage.all.count }.to(0) 28 | end 29 | 30 | it "consistently pass params to jobs" do 31 | params = {foo: 1, bar: 2} 32 | DelayedJobs::DummyJob.any_instance.expects(:perform).with(instance_of(Jobbr::Run), params) 33 | DelayedJobs::DummyJob.run(params, false) 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/jobbr/ohm.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | module Ohm 4 | 5 | extend self 6 | 7 | require 'ohm' 8 | 9 | # Return all Ohm models. 10 | # You can also pass a module class to get all models including that module 11 | def models(parent = nil) 12 | model_paths = Dir["#{Rails.root}/app/models/*_jobs/*.rb"] 13 | model_paths.each{ |path| require path } 14 | sanitized_model_paths = model_paths.map { |path| path.gsub(/.*\/app\/models\//, '').gsub('.rb', '') } 15 | model_constants = sanitized_model_paths.map do |path| 16 | path.split('/').map { |token| token.camelize }.join('::').constantize 17 | end 18 | model_constants.select { |model| superclasses(model).include?(::Ohm::Model) } 19 | 20 | if parent 21 | model_constants.select { |model| model.included_modules.include?(parent) } 22 | else 23 | model_constants 24 | end 25 | end 26 | 27 | protected 28 | 29 | # Return all superclasses for a given class. 30 | def superclasses(klass) 31 | super_classes = [] 32 | while klass != Object 33 | klass = klass.superclass 34 | super_classes << klass 35 | end 36 | super_classes 37 | end 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /app/helpers/jobbr/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | module ApplicationHelper 3 | 4 | include FontAwesome::Rails::IconHelper 5 | 6 | def delayed_job_creation_path(delayed_job_class, params = {}) 7 | jobbr.delayed_jobs_path(params.merge(job_name: delayed_job_class.name.underscore)) 8 | end 9 | 10 | def delayed_job_polling_path(id = ':job_id') 11 | jobbr.delayed_job_path(id) 12 | end 13 | 14 | def status_icon(job_status) 15 | css_class = "job-status #{job_status}" 16 | if job_status == :waiting 17 | fa_icon 'circle-o', class: css_class 18 | elsif job_status == :running 19 | fa_icon 'refresh', class: "#{css_class} fa-spin" 20 | elsif job_status == :success 21 | fa_icon 'certificate', class: css_class 22 | else 23 | fa_icon 'exclamation-circle', class: css_class 24 | end 25 | end 26 | 27 | def display_scheduling(job) 28 | every = job.every 29 | if every 30 | scheduling = every[0].is_a?(Fixnum) ? ChronicDuration.output(every[0]) : every[0].to_s 31 | if every[1] && !every[1].empty? 32 | scheduling = "#{scheduling} at #{every[1][:at]}" 33 | end 34 | scheduling 35 | end 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/tasks/jobbr_tasks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rake' 3 | 4 | describe 'jobbr_tasks' do 5 | 6 | before do 7 | Rake.application.rake_require 'tasks/jobbr_tasks' 8 | Rake::Task.define_task(:environment) 9 | end 10 | 11 | describe 'dynamic jobbr tasks declaration' do 12 | 13 | xit 'should define jobbr tasks' do 14 | task_names = Rake.application.tasks.map(&:name) 15 | task_names.should include('jobbr:dummy_heroku_job') 16 | task_names.should include('jobbr:dummy_scheduled_job') 17 | task_names.should include('jobbr:logging_job') 18 | task_names.should include('jobbr:dummy_heroku_job') 19 | end 20 | 21 | xit 'actually run jobs' do 22 | expect { 23 | Rake.application.invoke_task 'jobbr:logging_job' 24 | }.to change { Jobbr::Run.all.count }.from(0).to(1) 25 | 26 | Jobbr::Run.all.first.status.should be :success 27 | end 28 | 29 | end 30 | 31 | describe 'job sweeping' do 32 | 33 | before do 34 | 3.times { Jobbr::Run.create(status: :running, started_at: Time.now) } 35 | 2.times { Jobbr::Run.create(status: :success, started_at: Time.now) } 36 | end 37 | 38 | it 'marks running jobs as failed' do 39 | expect { 40 | Rake.application.invoke_task 'jobbr:sweep_running_jobs' 41 | }.to change { Jobbr::Run.find(status: :failed).count }.from(0).to(3) 42 | end 43 | 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Show full error reports and disable caching 10 | config.consider_all_requests_local = true 11 | config.action_controller.perform_caching = false 12 | 13 | # Don't care if the mailer can't send 14 | config.action_mailer.raise_delivery_errors = false 15 | 16 | # Print deprecation notices to the Rails logger 17 | config.active_support.deprecation = :log 18 | 19 | # Only use best-standards-support built into browsers 20 | config.action_dispatch.best_standards_support = :builtin 21 | 22 | # Raise exception on mass assignment protection for Active Record models 23 | # config.active_record.mass_assignment_sanitizer = :strict 24 | 25 | # Log the query plan for queries taking more than this (works 26 | # with SQLite, MySQL, and PostgreSQL) 27 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 28 | 29 | # Do not compress assets 30 | config.assets.compress = false 31 | 32 | # Expands the lines which load the assets 33 | config.assets.debug = true 34 | 35 | config.eager_load = false 36 | end 37 | -------------------------------------------------------------------------------- /jobbr.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | require 'jobbr/version' 4 | 5 | Gem::Specification.new do |s| 6 | 7 | s.name = 'jobbr' 8 | s.version = Jobbr::VERSION 9 | s.authors = ['Christian Blavier'] 10 | s.email = ['cblavier@gmail.com'] 11 | s.homepage = 'https://github.com/cblavier/jobbr' 12 | s.summary = 'Rails engine to manage jobs.' 13 | s.description = 'Rails engine to manage and supervise your batch jobs. Based on sidekiq.' 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- spec/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ['lib'] 19 | 20 | s.add_runtime_dependency 'rails', '>= 4.0.0' 21 | 22 | # UI 23 | s.add_runtime_dependency 'jquery-rails' 24 | s.add_runtime_dependency 'haml' 25 | s.add_runtime_dependency 'chronic_duration' 26 | s.add_runtime_dependency 'sass-rails', '>= 4.0.2' 27 | s.add_runtime_dependency 'coffee-rails' 28 | s.add_runtime_dependency 'therubyracer' 29 | s.add_runtime_dependency 'less-rails' 30 | s.add_runtime_dependency 'bootstrap-sass' 31 | s.add_runtime_dependency 'font-awesome-rails' 32 | s.add_runtime_dependency 'turbolinks' 33 | s.add_runtime_dependency 'kaminari' 34 | 35 | # Backend 36 | s.add_runtime_dependency 'redis' 37 | s.add_runtime_dependency 'ohm', '>= 2.0.1' 38 | s.add_runtime_dependency 'ohm-contrib' 39 | s.add_runtime_dependency 'sidekiq', '>= 3.0.0' 40 | s.add_runtime_dependency 'whenever' 41 | s.add_runtime_dependency 'require_all' 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Show full error reports and disable caching 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Raise exceptions instead of rendering exception templates 19 | config.action_dispatch.show_exceptions = false 20 | 21 | # Disable request forgery protection in test environment 22 | config.action_controller.allow_forgery_protection = false 23 | 24 | # Tell Action Mailer not to deliver emails to the real world. 25 | # The :test delivery method accumulates sent emails in the 26 | # ActionMailer::Base.deliveries array. 27 | config.action_mailer.delivery_method = :test 28 | 29 | # Raise exception on mass assignment protection for Active Record models 30 | # config.active_record.mass_assignment_sanitizer = :strict 31 | 32 | # Print deprecation notices to the stderr 33 | config.active_support.deprecation = :stderr 34 | 35 | config.eager_load = false 36 | end 37 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | 3 | if ENV['CI'] == 'true' 4 | ENV['CODECLIMATE_REPO_TOKEN'] ||= 'edafaf863fa93aff340625ec4ac8d70244456849256d0b4b16383ca0a76a11ec' 5 | require "codeclimate-test-reporter" 6 | CodeClimate::TestReporter.start 7 | end 8 | 9 | require File.join(File.dirname(__FILE__), 'dummy', 'config', 'environment.rb') 10 | require 'require_all' 11 | require 'rspec/rails' 12 | require 'generator_spec' 13 | require 'capybara/rspec' 14 | require 'capybara/poltergeist' 15 | require 'timecop' 16 | 17 | require_all Rails.root.join('..','..','lib','generators','jobbr', '**/*_generator.rb') 18 | require_all File.join(File.dirname(__FILE__), 'support', '**', '*.rb') 19 | 20 | SPEC_TMP_ROOT = Pathname.new(Dir.tmpdir) 21 | 22 | RSpec.configure do |config| 23 | 24 | Capybara.javascript_driver = :poltergeist 25 | config.mock_with :mocha 26 | 27 | config.include GeneratorSpec::TestCase, type: :generator 28 | config.include GeneratorDestinationRoot, type: :generator 29 | config.include RSpec::Rails::RequestExampleGroup, type: :feature 30 | 31 | config.before(:each, type: :generator) do 32 | FileUtils.rm_rf(SPEC_TMP_ROOT) 33 | prepare_destination 34 | end 35 | 36 | config.after(:each, type: :generator) do 37 | FileUtils.rm_rf(SPEC_TMP_ROOT) 38 | end 39 | 40 | config.before(:each) do 41 | clean_redis 42 | end 43 | 44 | config.after(:all) do 45 | Timecop.return 46 | clean_redis 47 | end 48 | 49 | def clean_redis 50 | Ohm.redis.call('KEYS', 'Jobbr::*').each{ |key| Ohm.redis.call('DEL', key) } 51 | end 52 | 53 | config.infer_spec_type_from_file_location! 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/features/job_list_feature_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | feature 'Job list' do 5 | 6 | before do 7 | Timecop.travel(2.hours.ago) 8 | ScheduledJobs::DummyJob.run 9 | ScheduledJobs::LoggingJob.run 10 | DelayedJobs::DummyJob.run 11 | DelayedJobs::OtherDummyJob.run 12 | Timecop.return 13 | end 14 | 15 | it 'shows all scheduled jobs' do 16 | visit jobbr_path 17 | assert_title 'Job list' 18 | find('.table.scheduled-jobs tbody').should have_selector('tr', count: 2) 19 | end 20 | 21 | 22 | it 'shows all delayed jobs' do 23 | visit jobbr_path 24 | assert_title 'Job list' 25 | find('.table.delayed-jobs tbody').should have_selector('tr', count: 2) 26 | end 27 | 28 | it 'shows correct status for each job' do 29 | Jobbr::Run.create(status: :failed, started_at: Time.now, job: ScheduledJobs::DummyJob.instance) 30 | visit jobbr_path 31 | assert_title 'Job list' 32 | first('.table.scheduled-jobs tbody tr').should have_selector('i.failed') 33 | end 34 | 35 | it 'show all runs for a specific job' do 36 | Timecop.travel(5.minutes.ago) 37 | Jobbr::Run.create(status: :failed, started_at: Time.now, job: ScheduledJobs::DummyJob.instance) 38 | Timecop.return 39 | Jobbr::Run.create(status: :running, started_at: Time.now, job: ScheduledJobs::DummyJob.instance) 40 | 41 | visit jobbr_path 42 | assert_title 'Job list' 43 | 44 | first('.scheduled-jobs a.all-runs').click 45 | assert_title 'Dummy job' 46 | find('.table tbody').should have_selector('tr', count: 3) 47 | end 48 | 49 | it 'shows a specific run' do 50 | visit jobbr_path 51 | assert_title 'Job list' 52 | 53 | first('.scheduled-jobs a.last-run').click 54 | assert_title I18n.localize(ScheduledJobs::DummyJob.instance.ordered_runs.first.started_at) 55 | end 56 | 57 | def assert_title(title) 58 | find('ol.breadcrumb li.active').should have_content(title) 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | 68 | config.eager_load = true 69 | end 70 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "action_controller/railtie" 4 | require "action_mailer/railtie" 5 | require "sprockets/railtie" 6 | 7 | Bundler.require 8 | require "jobbr" 9 | 10 | module Dummy 11 | class Application < Rails::Application 12 | # Settings in config/environments/* take precedence over those specified here. 13 | # Application configuration should go into files in config/initializers 14 | # -- all .rb files in that directory are automatically loaded. 15 | 16 | # Custom directories with classes and modules you want to be autoloadable. 17 | # config.autoload_paths += %W(#{config.root}/extras) 18 | 19 | # Only load the plugins named here, in the order given (default is alphabetical). 20 | # :all can be used as a placeholder for all plugins not explicitly named. 21 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 22 | 23 | # Activate observers that should always be running. 24 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 25 | 26 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 27 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 28 | # config.time_zone = 'Central Time (US & Canada)' 29 | 30 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 31 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 32 | # config.i18n.default_locale = :de 33 | 34 | # Configure the default encoding used in templates for Ruby 1.9. 35 | config.encoding = "utf-8" 36 | 37 | # Configure sensitive parameters which will be filtered from the log file. 38 | config.filter_parameters += [:password] 39 | 40 | # Enable escaping HTML in JSON. 41 | config.active_support.escape_html_entities_in_json = true 42 | 43 | # Use SQL instead of Active Record's schema dumper when creating the database. 44 | # This is necessary if your schema can't be completely dumped by the schema dumper, 45 | # like if you have constraints or database-specific column types 46 | # config.active_record.schema_format = :sql 47 | 48 | # Enforce whitelist mode for mass assignment. 49 | # This will create an empty whitelist of attributes available for mass-assignment for all models 50 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 51 | # parameters by using an attr_accessible or attr_protected declaration. 52 | #config.active_record.whitelist_attributes = true 53 | 54 | # Enable the asset pipeline 55 | config.assets.enabled = true 56 | 57 | # Version of your assets, change this if you want to expire all your assets 58 | config.assets.version = '1.0' 59 | 60 | I18n.enforce_available_locales = false 61 | 62 | config.secret_key_base = '12b5558fef409d5a8e0966a8b38671b7a25708d1c8777bd75760064c8c3cbe82bdd834e7fc3b7b94cd736d03c413265b114b2414691a01ec01340a086717440b' 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/dummy/config/mongoid.yml: -------------------------------------------------------------------------------- 1 | development: 2 | # Configure available database sessions. (required) 3 | sessions: 4 | # Defines the default session. (required) 5 | default: 6 | # Defines the name of the default database that Mongoid can connect to. 7 | # (required). 8 | database: dummy_development 9 | # Provides the hosts the default session can connect to. Must be an array 10 | # of host:port pairs. (required) 11 | hosts: 12 | - localhost:27017 13 | options: 14 | # Change whether the session persists in safe mode by default. 15 | # (default: false) 16 | # safe: false 17 | 18 | # Change the default consistency model to :eventual or :strong. 19 | # :eventual will send reads to secondaries, :strong sends everything 20 | # to master. (default: :eventual) 21 | # consistency: :eventual 22 | 23 | # How many times Moped should attempt to retry an operation after 24 | # failure. (default: 30) 25 | # max_retries: 30 26 | 27 | # The time in seconds that Moped should wait before retrying an 28 | # operation on failure. (default: 1) 29 | # retry_interval: 1 30 | # Configure Mongoid specific options. (optional) 31 | options: 32 | # Configuration for whether or not to allow access to fields that do 33 | # not have a field definition on the model. (default: true) 34 | # allow_dynamic_fields: true 35 | 36 | # Enable the identity map, needed for eager loading. (default: false) 37 | # identity_map_enabled: false 38 | 39 | # Includes the root model name in json serialization. (default: false) 40 | # include_root_in_json: false 41 | 42 | # Include the _type field in serializaion. (default: false) 43 | # include_type_for_serialization: false 44 | 45 | # Preload all models in development, needed when models use 46 | # inheritance. (default: false) 47 | # preload_models: false 48 | 49 | # Protect id and type from mass assignment. (default: true) 50 | # protect_sensitive_fields: true 51 | 52 | # Raise an error when performing a #find and the document is not found. 53 | # (default: true) 54 | # raise_not_found_error: true 55 | 56 | # Raise an error when defining a scope with the same name as an 57 | # existing method. (default: false) 58 | # scope_overwrite_exception: false 59 | 60 | # Skip the database version check, used when connecting to a db without 61 | # admin access. (default: false) 62 | # skip_version_check: false 63 | 64 | # User Active Support's time zone in conversions. (default: true) 65 | # use_activesupport_time_zone: true 66 | 67 | # Ensure all times are UTC in the app side. (default: false) 68 | # use_utc: false 69 | test: 70 | sessions: 71 | default: 72 | database: dummy_test 73 | hosts: 74 | - localhost:27017 75 | options: 76 | consistency: :strong 77 | # In the test environment we lower the retries and retry interval to 78 | # low amounts for fast failures. 79 | max_retries: 1 80 | retry_interval: 0 81 | -------------------------------------------------------------------------------- /spec/models/scheduled_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Jobbr 4 | 5 | describe Scheduled do 6 | 7 | it 'creates a new job by its name' do 8 | expect { 9 | ScheduledJobs::DummyScheduledJob.run 10 | }.to change{ Job.all.count }.by(1) 11 | end 12 | 13 | it 'does not create duplicated name jobs' do 14 | expect { 15 | ScheduledJobs::DummyScheduledJob.run 16 | ScheduledJobs::DummyScheduledJob.run 17 | ScheduledJobs::DummyScheduledJob.run 18 | }.to change{ Job.all.count }.by(1) 19 | expect { 20 | ScheduledJobs::DummyJob.run 21 | ScheduledJobs::DummyJob.run 22 | }.to change{ Job.all.count }.by(1) 23 | end 24 | 25 | it 'creates a job run for each run' do 26 | ScheduledJobs::DummyScheduledJob.run 27 | job = ScheduledJobs::DummyScheduledJob.instance 28 | 29 | expect { 30 | ScheduledJobs::DummyScheduledJob.run 31 | ScheduledJobs::DummyScheduledJob.run 32 | }.to change{ job.runs.count }.from(1).to(3) 33 | end 34 | 35 | it 'does not create more run than max_run_per_job' do 36 | ScheduledJobs::DummyScheduledJob.run 37 | job = ScheduledJobs::DummyScheduledJob.instance 38 | first_run = job.runs.first 39 | max_run_per_job = 5 40 | Job.any_instance.stubs(:max_run_per_job).returns(max_run_per_job) 41 | 42 | expect { 43 | (max_run_per_job + 3).times do 44 | ScheduledJobs::DummyScheduledJob.run 45 | end 46 | }.to change{ job.runs.count }.from(1).to(max_run_per_job) 47 | 48 | # ensure that it removes first executions and not latest 49 | job.runs.should_not include(first_run) 50 | end 51 | 52 | it 'changes run status from running to success' do 53 | ScheduledJobs::DummyScheduledJob.run do 54 | ScheduledJobs::DummyScheduledJob.instance.runs.first.status.should be :running 55 | end 56 | ScheduledJobs::DummyScheduledJob.instance.runs.first.status.should be :success 57 | end 58 | 59 | it 'changes run status from running to failed in case of exception' do 60 | ScheduledJobs::DummyScheduledJob.any_instance.stubs(:perform).raises('an error') 61 | begin 62 | ScheduledJobs::DummyScheduledJob.run 63 | rescue Exception 64 | end 65 | ScheduledJobs::DummyScheduledJob.instance.runs.first.status.should be :failed 66 | end 67 | 68 | it 'sets running dates' do 69 | ScheduledJobs::DummyScheduledJob.run 70 | job_run = ScheduledJobs::DummyScheduledJob.instance.runs.first 71 | job_run.started_at.should_not be_nil 72 | job_run.finished_at.should_not be_nil 73 | end 74 | 75 | it 'sets the progress to 100% at the end' do 76 | ScheduledJobs::DummyScheduledJob.run 77 | ScheduledJobs::DummyScheduledJob.instance.runs.first.progress.should be 100 78 | end 79 | 80 | it 'creates log messages when logging' do 81 | ScheduledJobs::LoggingJob.run 82 | last_job_run = ScheduledJobs::LoggingJob.instance.runs.first 83 | last_job_run.messages.size.should == 3 84 | last_job_run.messages[2].kind.should be :debug 85 | last_job_run.messages[2].message.should == 'foo' 86 | last_job_run.messages[3].kind.should be :error 87 | last_job_run.messages[3].message.should == 'bar' 88 | end 89 | 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Jobbr 2 | 3 | Jobbr is a Rails engine for supervising your delayed jobs and scheduled jobs (think Cron). 4 | Delayed jobs will run using sidekiq. 5 | 6 | It provides a framework to abstract creation and execution of such jobs and a user interface to supervise jobs and read their logs. 7 | 8 | {}[http://travis-ci.org/cblavier/jobbr] 9 | 10 | {}[https://codeclimate.com/github/cblavier/jobbr] 11 | 12 | {}[https://codeclimate.com/github/cblavier/jobbr] 13 | 14 | == Screenshots 15 | 16 | {}[http://cl.ly/image/3r320L101c3h] 17 | {}[http://cl.ly/image/21433N411G01] 18 | 19 | == Dependencies 20 | 21 | Jobbr has strong dependencies on following components: 22 | 23 | * *Sidekiq*: the background processing framework used to run delayed jobs. 24 | * *Redis*: all jobs & logs are stored in Redis for supervision. 25 | * *Whenever*: Jobbr uses {Whenever}[https://github.com/javan/whenever] gem to automatically updates Crontab during deployment. 26 | 27 | == Setup 28 | 29 | Start by adding Jobbr to your Gemfile: 30 | 31 | gem 'jobbr' 32 | 33 | === User interface 34 | 35 | Then mount Jobbr engine to your `routes.rb` file. 36 | 37 | mount Jobbr::Engine => "/jobbr" 38 | 39 | === Scheduled Jobs 40 | 41 | Use provided generators to create a first scheduled job 42 | 43 | $> rails g jobbr:scheduled_job dummy 44 | 45 | It will create a namespaced model as a well as a Whenever configuration file. 46 | 47 | Provided you fill in description and scheduling attributes in the model, you will be able to see it in jobbr tasks: 48 | 49 | $> bundle exec jobbr --list 50 | bundle exec jobbr dummy_job # A dummy Job 51 | 52 | And to see it in your crontab preview: 53 | 54 | $> whenever 55 | 30 5 * * * /bin/bash -l -c 'cd /Users/cblavier/code/my_app && RAILS_ENV=production bundle exec jobbr dummy_job >> /Users/cblavier/code/my_app/log/cron.log 2>&1' 56 | 57 | === Heroku Scheduled Jobs 58 | 59 | You can also use Heroku Scheduler to run jobs. Unfortunately Heroku does not provide Cron-scheduling, but let you run jobs every 10 minutes, every hour or every day. 60 | 61 | Jobbr provides you with 3 tasks 'jobbr:heroku:minutely', 'jobbr:heroku:hourly' and 'jobbr:heroku:daily', that will run any Job with `heroku_run` directive. 62 | 63 | Then you will need to manually add jobs to the Heroku scheduler console 64 | 65 | {}[http://cl.ly/image/2N1T1l1w2c28] 66 | 67 | 68 | === Delayed Jobs 69 | 70 | Use generators to get a new job model: 71 | 72 | $> rails g jobbr:delayed_job dummy 73 | 74 | You will get a new model with a perform method. Perform parameters are: 75 | 76 | * params: is a hash of parameters for your job. 77 | * run: is the object that will be persisted (and polled) for this job execution. Your delayed job can use it to provide progress information (to display a progress bar) and a final result. 78 | 79 | run.progress = 100 80 | run.result = 'my job result' 81 | 82 | You can now run your delayed job as following: 83 | 84 | run_id = DelayedJobs::DummyJob.run_delayed(some_param: 37) 85 | 86 | And then get job status like this: 87 | 88 | Jobbr::Run.find(run_id).status # returns :waiting / :running / :failed / :success 89 | 90 | Jobbr also provides a controller to run and poll delayed_jobs : 91 | 92 | * Post on following url to run your job: delayed_job_creation_path(DelayedJobs::DummyJob, { some_param: 37 }) 93 | 94 | * And then poll this url (using the id returned in previous post) to get your job status: delayed_job_polling_path(run_id) 95 | 96 | This project rocks and uses MIT-LICENSE. 97 | -------------------------------------------------------------------------------- /app/models/jobbr/job.rb: -------------------------------------------------------------------------------- 1 | module Jobbr 2 | 3 | class Job < ::Ohm::Model 4 | 5 | MAX_RUN_PER_JOB = 500 6 | 7 | include ::Ohm::Timestamps 8 | include ::Ohm::DataTypes 9 | include ::Ohm::Callbacks 10 | 11 | attribute :type 12 | attribute :delayed, Type::Boolean 13 | 14 | collection :runs, 'Jobbr::Run' 15 | 16 | index :type 17 | index :delayed 18 | 19 | def self.instance(instance_class_name = nil) 20 | if instance_class_name 21 | job_class = instance_class_name.camelize.constantize 22 | else 23 | job_class = self 24 | end 25 | 26 | job = Job.find(type: job_class.to_s).first 27 | if job.nil? 28 | delayed = job_class.included_modules.include?(Jobbr::Delayed) 29 | job = Job.create(type: job_class.to_s, delayed: delayed) 30 | end 31 | job 32 | end 33 | 34 | def self.run_by_name(name, *args) 35 | instance(name).run(*args) 36 | end 37 | 38 | def self.run(*args) 39 | instance.run(*args) 40 | end 41 | 42 | def self.description(desc = nil) 43 | @description = desc if desc 44 | @description 45 | end 46 | 47 | def self.delayed 48 | find(delayed: true) 49 | end 50 | 51 | def self.scheduled 52 | find(delayed: false) 53 | end 54 | 55 | def self.count 56 | all.count 57 | end 58 | 59 | def self.by_name(name) 60 | class_name = name.underscore.camelize 61 | Job.find(type: class_name).first 62 | end 63 | 64 | # overriding Ohm find to get Sidekiq to find job instances 65 | def self.find(id) 66 | if id.instance_of?(Hash) 67 | super 68 | elsif job = Jobbr::Job[id] 69 | job.send(:typed_self) 70 | end 71 | end 72 | 73 | def run(params = {}, delayed = true) 74 | job_run = Run.create(status: :waiting, started_at: Time.now, job: self) 75 | if delayed && self.delayed && !Rails.env.test? 76 | delayed_options = { retry: 0, backtrace: true } 77 | delayed_options[:queue] = typed_self.class.queue if typed_self.class.queue 78 | typed_self.delay(delayed_options).inner_run(job_run.id, params) 79 | else 80 | self.inner_run(job_run.id, params) 81 | end 82 | job_run 83 | end 84 | 85 | def handle_process_interruption(job_run, signals) 86 | signals.each do |signal| 87 | Signal.trap(signal) do 88 | job_run.status = :failed 89 | job_run.logger.error("Job interrupted by a #{signal} signal") 90 | job_run.finished_at = Time.now 91 | job_run.save 92 | end 93 | end 94 | end 95 | 96 | def every 97 | if scheduled? 98 | require self.type.underscore 99 | Object::const_get(self.type).every 100 | else 101 | nil 102 | end 103 | end 104 | 105 | def last_run 106 | @last_run ||= self.ordered_runs.first 107 | end 108 | 109 | def average_run_time 110 | return 0 if runs.empty? 111 | (runs.map { |run| run.run_time }.compact.inject { |sum, el| sum + el }.to_f / runs.count).round(2) 112 | end 113 | 114 | def to_param 115 | self.type.underscore.dasherize.gsub('/', '::') 116 | end 117 | 118 | def name 119 | self.type.demodulize.underscore.humanize 120 | end 121 | 122 | def scheduled? 123 | !self.delayed 124 | end 125 | 126 | def delayed? 127 | self.delayed 128 | end 129 | 130 | def ordered_runs 131 | self.runs.sort_by(:started_at, order: 'ALPHA DESC') 132 | end 133 | 134 | def after_delete 135 | self.runs.each(&:delete) 136 | end 137 | 138 | def perform 139 | raise NotImplementedError.new :message => 'Must be implemented' 140 | end 141 | 142 | protected 143 | 144 | # mocking purpose 145 | def max_run_per_job 146 | MAX_RUN_PER_JOB 147 | end 148 | 149 | # prevents Run collection to grow beyond max_run_per_job 150 | def cap_runs! 151 | runs_count = self.runs.count 152 | if runs_count > max_run_per_job 153 | runs.sort_by(:started_at, order: 'ALPHA ASC', limit: [0, runs_count - max_run_per_job]).each do |run| 154 | if run.status == :failed || run.status == :success 155 | run.delete 156 | end 157 | end 158 | end 159 | end 160 | 161 | def inner_run(job_run_id, params = {}) 162 | job_run = Run[job_run_id] 163 | job_run.status = :running 164 | job_run.started_at = Time.now 165 | job_run.save 166 | 167 | cap_runs! 168 | 169 | handle_process_interruption(job_run, ['TERM', 'INT']) 170 | 171 | begin 172 | job_run.logger.debug("Starting with params #{params.inspect}") 173 | perform(job_run, params) 174 | job_run.status = :success 175 | job_run.progress = 100 176 | rescue Exception => e 177 | job_run.status = :failed 178 | job_run.logger.error(e.message) 179 | job_run.logger.error(e.backtrace) 180 | raise e 181 | ensure 182 | job_run.finished_at = Time.now 183 | job_run.save 184 | end 185 | end 186 | 187 | def perform(job_run, params) 188 | case typed_self.method(:perform).parameters.length 189 | when 0 then typed_self.perform 190 | when 1 then typed_self.perform(job_run) 191 | when 2 then typed_self.perform(job_run, params) 192 | end 193 | end 194 | 195 | # working around lack of polymorphism in Ohm 196 | # using type attributed to get a typed instance 197 | def typed_self 198 | @typed_self ||= Object::const_get(self.type).new(id: self.id) 199 | end 200 | 201 | end 202 | 203 | end 204 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | jobbr (2.0.0) 5 | bootstrap-sass 6 | chronic_duration 7 | coffee-rails 8 | font-awesome-rails 9 | haml 10 | jquery-rails 11 | kaminari 12 | less-rails 13 | ohm (>= 2.0.1) 14 | ohm-contrib 15 | rails (>= 4.0.0) 16 | redis 17 | require_all 18 | sass-rails (>= 4.0.2) 19 | sidekiq (>= 3.0.0) 20 | therubyracer 21 | turbolinks 22 | whenever 23 | 24 | GEM 25 | remote: http://rubygems.org/ 26 | specs: 27 | actionmailer (4.1.1) 28 | actionpack (= 4.1.1) 29 | actionview (= 4.1.1) 30 | mail (~> 2.5.4) 31 | actionpack (4.1.1) 32 | actionview (= 4.1.1) 33 | activesupport (= 4.1.1) 34 | rack (~> 1.5.2) 35 | rack-test (~> 0.6.2) 36 | actionview (4.1.1) 37 | activesupport (= 4.1.1) 38 | builder (~> 3.1) 39 | erubis (~> 2.7.0) 40 | activemodel (4.1.1) 41 | activesupport (= 4.1.1) 42 | builder (~> 3.1) 43 | activerecord (4.1.1) 44 | activemodel (= 4.1.1) 45 | activesupport (= 4.1.1) 46 | arel (~> 5.0.0) 47 | activesupport (4.1.1) 48 | i18n (~> 0.6, >= 0.6.9) 49 | json (~> 1.7, >= 1.7.7) 50 | minitest (~> 5.1) 51 | thread_safe (~> 0.1) 52 | tzinfo (~> 1.1) 53 | addressable (2.3.6) 54 | arel (5.0.1.20140414130214) 55 | bootstrap-sass (3.1.1.1) 56 | sass (~> 3.2) 57 | builder (3.2.2) 58 | capybara (2.3.0) 59 | mime-types (>= 1.16) 60 | nokogiri (>= 1.3.3) 61 | rack (>= 1.0.0) 62 | rack-test (>= 0.5.4) 63 | xpath (~> 2.0) 64 | celluloid (0.15.2) 65 | timers (~> 1.1.0) 66 | chronic (0.10.2) 67 | chronic_duration (0.10.4) 68 | numerizer (~> 0.1.1) 69 | cliver (0.3.2) 70 | codeclimate-test-reporter (0.3.0) 71 | simplecov (>= 0.7.1, < 1.0.0) 72 | coffee-rails (4.0.1) 73 | coffee-script (>= 2.2.0) 74 | railties (>= 4.0.0, < 5.0) 75 | coffee-script (2.2.0) 76 | coffee-script-source 77 | execjs 78 | coffee-script-source (1.7.0) 79 | commonjs (0.2.7) 80 | connection_pool (2.0.0) 81 | diff-lcs (1.2.5) 82 | docile (1.1.5) 83 | erubis (2.7.0) 84 | execjs (2.2.0) 85 | font-awesome-rails (4.1.0.0) 86 | railties (>= 3.2, < 5.0) 87 | generator_spec (0.9.2) 88 | activesupport (>= 3.0.0) 89 | railties (>= 3.0.0) 90 | haml (4.0.5) 91 | tilt 92 | hike (1.2.3) 93 | hiredis (0.5.2) 94 | i18n (0.6.9) 95 | jquery-rails (3.1.0) 96 | railties (>= 3.0, < 5.0) 97 | thor (>= 0.14, < 2.0) 98 | json (1.8.1) 99 | kaminari (0.16.1) 100 | actionpack (>= 3.0.0) 101 | activesupport (>= 3.0.0) 102 | kgio (2.9.2) 103 | launchy (2.4.2) 104 | addressable (~> 2.3) 105 | less (2.5.1) 106 | commonjs (~> 0.2.7) 107 | less-rails (2.5.0) 108 | actionpack (>= 3.1) 109 | less (~> 2.5.0) 110 | libv8 (3.16.14.3) 111 | mail (2.5.4) 112 | mime-types (~> 1.16) 113 | treetop (~> 1.4.8) 114 | metaclass (0.0.4) 115 | mime-types (1.25.1) 116 | mini_portile (0.6.0) 117 | minitest (5.3.4) 118 | mocha (1.1.0) 119 | metaclass (~> 0.0.1) 120 | msgpack (0.5.8) 121 | multi_json (1.10.1) 122 | nido (0.0.1) 123 | nokogiri (1.6.2.1) 124 | mini_portile (= 0.6.0) 125 | numerizer (0.1.1) 126 | ohm (2.0.1) 127 | msgpack 128 | nido 129 | redic 130 | ohm-contrib (2.0.0) 131 | ohm (~> 2.0.0) 132 | poltergeist (1.5.1) 133 | capybara (~> 2.1) 134 | cliver (~> 0.3.1) 135 | multi_json (~> 1.0) 136 | websocket-driver (>= 0.2.0) 137 | polyglot (0.3.5) 138 | rack (1.5.2) 139 | rack-test (0.6.2) 140 | rack (>= 1.0) 141 | rails (4.1.1) 142 | actionmailer (= 4.1.1) 143 | actionpack (= 4.1.1) 144 | actionview (= 4.1.1) 145 | activemodel (= 4.1.1) 146 | activerecord (= 4.1.1) 147 | activesupport (= 4.1.1) 148 | bundler (>= 1.3.0, < 2.0) 149 | railties (= 4.1.1) 150 | sprockets-rails (~> 2.0) 151 | railties (4.1.1) 152 | actionpack (= 4.1.1) 153 | activesupport (= 4.1.1) 154 | rake (>= 0.8.7) 155 | thor (>= 0.18.1, < 2.0) 156 | raindrops (0.13.0) 157 | rake (10.3.2) 158 | redic (1.1.1) 159 | hiredis 160 | redis (3.0.7) 161 | redis-namespace (1.4.1) 162 | redis (~> 3.0.4) 163 | ref (1.0.5) 164 | require_all (1.3.2) 165 | rspec-collection_matchers (1.0.0) 166 | rspec-expectations (>= 2.99.0.beta1) 167 | rspec-core (2.99.0) 168 | rspec-expectations (2.99.0) 169 | diff-lcs (>= 1.1.3, < 2.0) 170 | rspec-mocks (2.99.1) 171 | rspec-rails (2.99.0) 172 | actionpack (>= 3.0) 173 | activemodel (>= 3.0) 174 | activesupport (>= 3.0) 175 | railties (>= 3.0) 176 | rspec-collection_matchers 177 | rspec-core (~> 2.99.0) 178 | rspec-expectations (~> 2.99.0) 179 | rspec-mocks (~> 2.99.0) 180 | sass (3.2.19) 181 | sass-rails (4.0.3) 182 | railties (>= 4.0.0, < 5.0) 183 | sass (~> 3.2.0) 184 | sprockets (~> 2.8, <= 2.11.0) 185 | sprockets-rails (~> 2.0) 186 | sidekiq (3.1.4) 187 | celluloid (>= 0.15.2) 188 | connection_pool (>= 2.0.0) 189 | json 190 | redis (>= 3.0.6) 191 | redis-namespace (>= 1.3.1) 192 | simplecov (0.8.2) 193 | docile (~> 1.1.0) 194 | multi_json 195 | simplecov-html (~> 0.8.0) 196 | simplecov-html (0.8.0) 197 | sprockets (2.11.0) 198 | hike (~> 1.2) 199 | multi_json (~> 1.0) 200 | rack (~> 1.0) 201 | tilt (~> 1.1, != 1.3.0) 202 | sprockets-rails (2.1.3) 203 | actionpack (>= 3.0) 204 | activesupport (>= 3.0) 205 | sprockets (~> 2.8) 206 | therubyracer (0.12.1) 207 | libv8 (~> 3.16.14.0) 208 | ref 209 | thor (0.19.1) 210 | thread_safe (0.3.4) 211 | tilt (1.4.1) 212 | timecop (0.7.1) 213 | timers (1.1.0) 214 | treetop (1.4.15) 215 | polyglot 216 | polyglot (>= 0.3.1) 217 | turbolinks (2.2.2) 218 | coffee-rails 219 | tzinfo (1.2.1) 220 | thread_safe (~> 0.1) 221 | unicorn (4.8.3) 222 | kgio (~> 2.6) 223 | rack 224 | raindrops (~> 0.7) 225 | websocket-driver (0.3.3) 226 | whenever (0.9.2) 227 | activesupport (>= 2.3.4) 228 | chronic (>= 0.6.3) 229 | xpath (2.0.0) 230 | nokogiri (~> 1.3) 231 | 232 | PLATFORMS 233 | ruby 234 | 235 | DEPENDENCIES 236 | bootstrap-sass 237 | capybara 238 | chronic_duration 239 | codeclimate-test-reporter 240 | coffee-rails 241 | font-awesome-rails 242 | generator_spec 243 | haml 244 | jobbr! 245 | jquery-rails 246 | kaminari 247 | launchy 248 | less-rails 249 | mocha 250 | ohm (>= 2.0.1) 251 | ohm-contrib 252 | poltergeist 253 | rails (>= 4.0.0) 254 | redis 255 | require_all 256 | rspec-rails (~> 2.99) 257 | sass-rails (>= 4.0.2) 258 | sidekiq 259 | therubyracer 260 | timecop 261 | turbolinks 262 | unicorn 263 | whenever 264 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application framework that includes everything needed to create 4 | database-backed web applications according to the Model-View-Control pattern. 5 | 6 | This pattern splits the view (also called the presentation) into "dumb" 7 | templates that are primarily responsible for inserting pre-built data in between 8 | HTML tags. The model contains the "smart" domain objects (such as Account, 9 | Product, Person, Post) that holds all the business logic and knows how to 10 | persist themselves to a database. The controller handles the incoming requests 11 | (such as Save New Account, Update Product, Show Post) by manipulating the model 12 | and directing data to the view. 13 | 14 | In Rails, the model is handled by what's called an object-relational mapping 15 | layer entitled Active Record. This layer allows you to present the data from 16 | database rows as objects and embellish these data objects with business logic 17 | methods. You can read more about Active Record in 18 | link:files/vendor/rails/activerecord/README.html. 19 | 20 | The controller and view are handled by the Action Pack, which handles both 21 | layers by its two parts: Action View and Action Controller. These two layers 22 | are bundled in a single package due to their heavy interdependence. This is 23 | unlike the relationship between the Active Record and Action Pack that is much 24 | more separate. Each of these packages can be used independently outside of 25 | Rails. You can read more about Action Pack in 26 | link:files/vendor/rails/actionpack/README.html. 27 | 28 | 29 | == Getting Started 30 | 31 | 1. At the command prompt, create a new Rails application: 32 | rails new myapp (where myapp is the application name) 33 | 34 | 2. Change directory to myapp and start the web server: 35 | cd myapp; rails server (run with --help for options) 36 | 37 | 3. Go to http://localhost:3000/ and you'll see: 38 | "Welcome aboard: You're riding Ruby on Rails!" 39 | 40 | 4. Follow the guidelines to start developing your application. You can find 41 | the following resources handy: 42 | 43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html 44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ 45 | 46 | 47 | == Debugging Rails 48 | 49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that 50 | will help you debug it and get it back on the rails. 51 | 52 | First area to check is the application log files. Have "tail -f" commands 53 | running on the server.log and development.log. Rails will automatically display 54 | debugging and runtime information to these files. Debugging info will also be 55 | shown in the browser on requests from 127.0.0.1. 56 | 57 | You can also log your own messages directly into the log file from your code 58 | using the Ruby logger class from inside your controllers. Example: 59 | 60 | class WeblogController < ActionController::Base 61 | def destroy 62 | @weblog = Weblog.find(params[:id]) 63 | @weblog.destroy 64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") 65 | end 66 | end 67 | 68 | The result will be a message in your log file along the lines of: 69 | 70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! 71 | 72 | More information on how to use the logger is at http://www.ruby-doc.org/core/ 73 | 74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are 75 | several books available online as well: 76 | 77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) 78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) 79 | 80 | These two books will bring you up to speed on the Ruby language and also on 81 | programming in general. 82 | 83 | 84 | == Debugger 85 | 86 | Debugger support is available through the debugger command when you start your 87 | Mongrel or WEBrick server with --debugger. This means that you can break out of 88 | execution at any point in the code, investigate and change the model, and then, 89 | resume execution! You need to install ruby-debug to run the server in debugging 90 | mode. With gems, use sudo gem install ruby-debug. Example: 91 | 92 | class WeblogController < ActionController::Base 93 | def index 94 | @posts = Post.all 95 | debugger 96 | end 97 | end 98 | 99 | So the controller will accept the action, run the first line, then present you 100 | with a IRB prompt in the server window. Here you can do things like: 101 | 102 | >> @posts.inspect 103 | => "[#nil, "body"=>nil, "id"=>"1"}>, 105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" 107 | >> @posts.first.title = "hello from a debugger" 108 | => "hello from a debugger" 109 | 110 | ...and even better, you can examine how your runtime objects actually work: 111 | 112 | >> f = @posts.first 113 | => #nil, "body"=>nil, "id"=>"1"}> 114 | >> f. 115 | Display all 152 possibilities? (y or n) 116 | 117 | Finally, when you're ready to resume execution, you can enter "cont". 118 | 119 | 120 | == Console 121 | 122 | The console is a Ruby shell, which allows you to interact with your 123 | application's domain model. Here you'll have all parts of the application 124 | configured, just like it is when the application is running. You can inspect 125 | domain models, change values, and save to the database. Starting the script 126 | without arguments will launch it in the development environment. 127 | 128 | To start the console, run rails console from the application 129 | directory. 130 | 131 | Options: 132 | 133 | * Passing the -s, --sandbox argument will rollback any modifications 134 | made to the database. 135 | * Passing an environment name as an argument will load the corresponding 136 | environment. Example: rails console production. 137 | 138 | To reload your controllers and models after launching the console run 139 | reload! 140 | 141 | More information about irb can be found at: 142 | link:http://www.rubycentral.org/pickaxe/irb.html 143 | 144 | 145 | == dbconsole 146 | 147 | You can go to the command line of your database directly through rails 148 | dbconsole. You would be connected to the database with the credentials 149 | defined in database.yml. Starting the script without arguments will connect you 150 | to the development database. Passing an argument will connect you to a different 151 | database, like rails dbconsole production. Currently works for MySQL, 152 | PostgreSQL and SQLite 3. 153 | 154 | == Description of Contents 155 | 156 | The default directory structure of a generated Ruby on Rails application: 157 | 158 | |-- app 159 | | |-- assets 160 | | |-- images 161 | | |-- javascripts 162 | | `-- stylesheets 163 | | |-- controllers 164 | | |-- helpers 165 | | |-- mailers 166 | | |-- models 167 | | `-- views 168 | | `-- layouts 169 | |-- config 170 | | |-- environments 171 | | |-- initializers 172 | | `-- locales 173 | |-- db 174 | |-- doc 175 | |-- lib 176 | | `-- tasks 177 | |-- log 178 | |-- public 179 | |-- script 180 | |-- test 181 | | |-- fixtures 182 | | |-- functional 183 | | |-- integration 184 | | |-- performance 185 | | `-- unit 186 | |-- tmp 187 | | |-- cache 188 | | |-- pids 189 | | |-- sessions 190 | | `-- sockets 191 | `-- vendor 192 | |-- assets 193 | `-- stylesheets 194 | `-- plugins 195 | 196 | app 197 | Holds all the code that's specific to this particular application. 198 | 199 | app/assets 200 | Contains subdirectories for images, stylesheets, and JavaScript files. 201 | 202 | app/controllers 203 | Holds controllers that should be named like weblogs_controller.rb for 204 | automated URL mapping. All controllers should descend from 205 | ApplicationController which itself descends from ActionController::Base. 206 | 207 | app/models 208 | Holds models that should be named like post.rb. Models descend from 209 | ActiveRecord::Base by default. 210 | 211 | app/views 212 | Holds the template files for the view that should be named like 213 | weblogs/index.html.erb for the WeblogsController#index action. All views use 214 | eRuby syntax by default. 215 | 216 | app/views/layouts 217 | Holds the template files for layouts to be used with views. This models the 218 | common header/footer method of wrapping views. In your views, define a layout 219 | using the layout :default and create a file named default.html.erb. 220 | Inside default.html.erb, call <% yield %> to render the view using this 221 | layout. 222 | 223 | app/helpers 224 | Holds view helpers that should be named like weblogs_helper.rb. These are 225 | generated for you automatically when using generators for controllers. 226 | Helpers can be used to wrap functionality for your views into methods. 227 | 228 | config 229 | Configuration files for the Rails environment, the routing map, the database, 230 | and other dependencies. 231 | 232 | db 233 | Contains the database schema in schema.rb. db/migrate contains all the 234 | sequence of Migrations for your schema. 235 | 236 | doc 237 | This directory is where your application documentation will be stored when 238 | generated using rake doc:app 239 | 240 | lib 241 | Application specific libraries. Basically, any kind of custom code that 242 | doesn't belong under controllers, models, or helpers. This directory is in 243 | the load path. 244 | 245 | public 246 | The directory available for the web server. Also contains the dispatchers and the 247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web 248 | server. 249 | 250 | script 251 | Helper scripts for automation and generation. 252 | 253 | test 254 | Unit and functional tests along with fixtures. When using the rails generate 255 | command, template test files will be generated for you and placed in this 256 | directory. 257 | 258 | vendor 259 | External libraries that the application depends on. Also includes the plugins 260 | subdirectory. If the app has frozen rails, those gems also go here, under 261 | vendor/rails/. This directory is in the load path. 262 | --------------------------------------------------------------------------------