├── log └── .keep ├── .rspec ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── project.rb │ ├── story.rb │ ├── vote.rb │ └── user.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── favicon.ico │ │ ├── pivotal-tracker.png │ │ └── pivotal-tracker.svg │ ├── javascripts │ │ ├── sessions.js.coffee │ │ ├── application.js │ │ ├── dashboard.js.coffee │ │ └── common.js.coffee │ └── stylesheets │ │ ├── unauthenticated.css.scss │ │ └── authenticated.css.scss ├── controllers │ ├── concerns │ │ └── .keep │ ├── sessions_controller.rb │ ├── application_controller.rb │ └── dashboard_controller.rb ├── helpers │ ├── sessions_helper.rb │ ├── application_helper.rb │ └── dashboard_helper.rb └── views │ ├── sessions │ ├── new.html.erb │ └── _form.html.erb │ ├── dashboard │ ├── stories │ │ ├── _estimate.html.erb │ │ ├── _votes.html.erb │ │ ├── _vote.html.erb │ │ ├── _cards.html.erb │ │ ├── _list.html.erb │ │ ├── _project.html.erb │ │ ├── _info.html.erb │ │ └── _story.html.erb │ ├── ajax │ │ ├── reveal.js.erb │ │ ├── select.js.erb │ │ ├── vote.js.erb │ │ ├── reset.js.erb │ │ ├── project.js.erb │ │ ├── detail.js.erb │ │ └── update.js.erb │ └── index.html.erb │ ├── application │ ├── _overlay.html.erb │ ├── _notification.html.erb │ ├── _unauthenticated.html.erb │ ├── _authenticated.html.erb │ └── _menu.html.erb │ └── layouts │ └── application.html.erb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── .ruby-version ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── Procfile ├── spec ├── models │ ├── project_spec.rb │ ├── story_spec.rb │ ├── vote_spec.rb │ └── user_spec.rb ├── helpers │ ├── sessions_helper_spec.rb │ ├── application_helper_spec.rb │ └── dashboard_helper_spec.rb ├── support │ ├── helpers │ │ ├── application_helper.rb │ │ ├── dashboard_helper.rb │ │ ├── views_helper.rb │ │ └── sessions_helper.rb │ └── fixtures │ │ ├── basic_fixture.rb │ │ ├── project_fixture.rb │ │ └── story_fixture.rb ├── features │ ├── sessions │ │ ├── logout_spec.rb │ │ ├── login_spec.rb │ │ └── exceptions_spec.rb │ └── dashboard │ │ └── projects_spec.rb ├── factories.rb ├── spec_helper.rb └── controllers │ ├── sessions_controller_spec.rb │ └── dashboard_controller_spec.rb ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── config ├── initializers │ ├── pivotal_tracker.rb │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── filter_parameter_logging.rb │ ├── mime_types.rb │ ├── wrap_parameters.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ └── inflections.rb ├── environment.rb ├── boot.rb ├── database.yml ├── secrets.yml ├── routes.rb ├── application.rb ├── locales │ └── en.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb ├── mongoid.yml └── newrelic.yml ├── bin ├── rake ├── bundle ├── rails ├── rspec ├── autospec └── setup ├── faye.ru ├── config.ru ├── Rakefile ├── db └── seeds.rb ├── .gitignore ├── Gemfile ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.2 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/sessions.js.coffee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project 2 | end -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rails s -p $PORT 2 | -------------------------------------------------------------------------------- /app/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/models/project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Project do 4 | end -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bimovidia/planning-poker/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /spec/helpers/sessions_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SessionsHelper do 4 | end -------------------------------------------------------------------------------- /config/initializers/pivotal_tracker.rb: -------------------------------------------------------------------------------- 1 | # Enable SSL support 2 | PivotalTracker::Client.use_ssl = true 3 | -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bimovidia/planning-poker/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/assets/images/pivotal-tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bimovidia/planning-poker/HEAD/app/assets/images/pivotal-tracker.png -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | Pivotal Tracker 4 |

5 | <%= render 'form' %> 6 |
-------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /faye.ru: -------------------------------------------------------------------------------- 1 | require 'faye' 2 | 3 | Faye::WebSocket.load_adapter('thin') 4 | 5 | bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25) 6 | run bayeux -------------------------------------------------------------------------------- /app/views/dashboard/stories/_estimate.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= raw estimation(story) if show_estimation_icons?(story) %> 3 | 4 | <%= estimate(story) %> -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /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 Rails.application 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' # Set up gems listed in the Gemfile. -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_planning-poker-rails_session' -------------------------------------------------------------------------------- /spec/support/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Helpers 3 | module ApplicationHelper 4 | 5 | def t(locale) 6 | I18n.translate(locale) 7 | end 8 | 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks -------------------------------------------------------------------------------- /app/models/story.rb: -------------------------------------------------------------------------------- 1 | class Story 2 | class << self 3 | 4 | def update(params) 5 | story = PivotalTracker::Story.find( 6 | params[:story_id], 7 | params[:project_id] 8 | ) 9 | 10 | story.update(params[:story]) 11 | end 12 | 13 | end 14 | end -------------------------------------------------------------------------------- /app/views/application/_overlay.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/application/_notification.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% flash.each do |name, msg| %> 3 |
4 |
5 | <%= msg %> 6 |
7 |
8 | <% end %> 9 |
-------------------------------------------------------------------------------- /app/views/dashboard/stories/_votes.html.erb: -------------------------------------------------------------------------------- 1 | <% votes = Vote.where(story_id: story_id) %> 2 | 3 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /spec/support/helpers/dashboard_helper.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Helpers 3 | module DashboardHelper 4 | 5 | def encoded_user 6 | Base64.strict_encode64( 7 | Forgery(:internet).email_address 8 | ) 9 | end 10 | 11 | def decoded_user(user) 12 | Base64.strict_decode64(user) 13 | end 14 | 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /spec/support/helpers/views_helper.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Helpers 3 | module ViewsHelper 4 | 5 | def project_nodes 6 | page.all('#menu li') 7 | end 8 | 9 | def story_nodes 10 | page.all('#stories .story') 11 | end 12 | 13 | def story_node(id) 14 | find("#story-#{id}") 15 | end 16 | 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /spec/features/sessions/logout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Sessions::Logout' do 4 | before { skip_authentication(DashboardController) } 5 | 6 | context 'success' do 7 | before do 8 | stub_projects 9 | visit root_path 10 | logout 11 | end 12 | 13 | specify { page.should have_content t('flashes.sessions.destroy') } 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /app/views/dashboard/ajax/reveal.js.erb: -------------------------------------------------------------------------------- 1 | <% broadcast '/planning-poker/story/reveal-votes' do %> 2 | $('#cards-<%= @resource[:story_id] %>').html( 3 | "<%= j render('dashboard/stories/votes', story_id: @resource[:story_id], reveal: true) %>" 4 | ); 5 | 6 | $('.btn-votes a').removeClass('active'); 7 | $('#form-<%= @resource[:story_id] %>').find('input#estimate').val(''); 8 | 9 | window.selectCard(); 10 | <% end %> -------------------------------------------------------------------------------- /app/views/dashboard/ajax/select.js.erb: -------------------------------------------------------------------------------- 1 | <% broadcast '/planning-poker/story/select-vote' do %> 2 | 3 | var vote = $('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= @resource[:username] %>"] span'); 4 | 5 | $('.card-revealed').removeClass('card-selected') 6 | 7 | vote.addClass('card-selected').closest('form').find('input#estimate').prop( 8 | 'value', 9 | vote.find('.vote-value').text().trim() 10 | ) 11 | 12 | <% end %> -------------------------------------------------------------------------------- /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] if respond_to?(:wrap_parameters) 9 | end 10 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'rspec') 17 | -------------------------------------------------------------------------------- /app/views/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= t('story').pluralize %> 4 |

5 |
6 | <%= render 'dashboard/stories/list', project: @projects.first unless @projects.blank? %> 7 |
8 |
9 | 10 | -------------------------------------------------------------------------------- /app/views/sessions/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag login_path, as: :session do %> 2 | 3 |
4 | <%= text_field_tag :username, nil, class: 'form-control', placeholder: 'Email / Username' %> 5 |
6 | 7 |
8 | <%= password_field_tag :password, nil, class: 'form-control', placeholder: 'Password' %> 9 |
10 | 11 | <%= submit_tag t('signin').upcase, class: 'btn btn-success' %> 12 | <% end %> -------------------------------------------------------------------------------- /bin/autospec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'autospec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'autospec') 17 | -------------------------------------------------------------------------------- /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/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | 3 | factory :user do 4 | username { Forgery::Internet.email_address } 5 | password { Forgery::Basic.encrypt } 6 | salt { Forgery::Basic.encrypt } 7 | token { Forgery::Basic.encrypt } 8 | end 9 | 10 | factory :vote do 11 | user Forgery::Internet.email_address 12 | story_id Forgery::Basic.number(at_least: 1, at_most: 20) 13 | vote Forgery::Basic.number(at_least: 1, at_most: 5) 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /app/views/dashboard/stories/_vote.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 |
    4 | <% if vote %> 5 | <%= vote %> 6 | <% else %> 7 | <% if vote.nil? %> 8 | 9 | <% else %> 10 | ? 11 | <% end %> 12 | <% end %> 13 |
    14 |
    15 | <%= nickname user %> 16 |
    17 |
    18 |
  • -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile = %w( search.js ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | !/log/.keep 17 | /tmp 18 | 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /app/views/dashboard/ajax/vote.js.erb: -------------------------------------------------------------------------------- 1 | <% broadcast '/planning-poker/story/add-vote' do %> 2 | 3 | if ($('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= @resource[:user] %>"]').length) { 4 | $('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= @resource[:user] %>"]').remove(); 5 | } 6 | 7 | $('#cards-<%= @resource[:story_id] %> ul').append( 8 | "<%= j render('dashboard/stories/vote', story_id: @resource[:story_id], 9 | vote: false, user: @resource[:user], reveal: false) %>" 10 | ); 11 | 12 | $('#vote-<%= @resource[:story_id] %> .reveal').removeAttr('disabled'); 13 | 14 | <% end %> -------------------------------------------------------------------------------- /app/views/application/_unauthenticated.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 15 | 16 |
    17 |
    18 | <%= yield %> 19 |
    20 |
    21 |
    -------------------------------------------------------------------------------- /spec/support/fixtures/basic_fixture.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Fixtures 3 | module BasicFixture 4 | 5 | def stub_id 6 | rand(1000000) 7 | end 8 | 9 | def stub_text 10 | Forgery::Basic.text 11 | end 12 | 13 | def stub_words(n = 5) 14 | Forgery::LoremIpsum.words(n) 15 | end 16 | 17 | def stub_name 18 | Forgery::Name.full_name 19 | end 20 | 21 | def init_object(attrs = {}) 22 | OpenStruct.new(attrs) 23 | end 24 | 25 | def stub_user(user = nil) 26 | User.stubs(:authenticate).returns(user) 27 | end 28 | 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /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 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 -------------------------------------------------------------------------------- /spec/helpers/application_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ApplicationHelper do 4 | 5 | context '#layout_class' do 6 | it 'should return authenticated' do 7 | helper.stubs(:user_signed_in?).returns(true) 8 | helper.layout_class.should eq 'authenticated' 9 | end 10 | 11 | it 'should return unauthenticated' do 12 | helper.stubs(:user_signed_in?).returns(false) 13 | helper.layout_class.should eq 'unauthenticated' 14 | end 15 | end 16 | 17 | context '#comma_separated' do 18 | it 'should return comma separated words' do 19 | helper.comma_separated((1..5).to_a).should eq '1, 2, 3, 4, 5' 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /spec/features/sessions/login_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Sessions::Login' do 4 | let(:user) { FactoryGirl.create(:user) } 5 | 6 | context 'success' do 7 | before do 8 | stub_user(user) 9 | stub_projects 10 | login 11 | end 12 | 13 | specify { page.should have_content t('flashes.sessions.success') } 14 | end 15 | 16 | context 'failure' do 17 | before do 18 | stub_user 19 | stub_projects 20 | end 21 | 22 | context 'login' do 23 | before { login } 24 | 25 | specify { current_path.should eq login_path } 26 | specify { page.should have_content t('flashes.sessions.failed') } 27 | end 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /app/views/dashboard/ajax/reset.js.erb: -------------------------------------------------------------------------------- 1 | <% broadcast '/planning-poker/story/reset-vote' do %> 2 | 3 | if ($('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= current_user['username'] %>"]').length) { 4 | $('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= current_user['username'] %>"]').remove(); 5 | } 6 | 7 | $('#cards-<%= @resource[:story_id] %> ul').append( 8 | "<%= j render('dashboard/stories/vote', story_id: @resource[:story_id], 9 | vote: nil, user: @resource[:user], reveal: false) %>" 10 | ); 11 | 12 | $('#form-<%= @resource[:story_id] %>').find('input#estimate').val(''); 13 | 14 | $('#vote-<%= @resource[:story_id] %> .reveal').attr('disabled','disabled'); 15 | 16 | <% end %> -------------------------------------------------------------------------------- /app/views/dashboard/stories/_cards.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= render 'dashboard/stories/votes', story_id: story.id, reveal: false %> 3 |
    4 | 5 |
    6 | <% project.point_scale.split(',').each do |point| %> 7 | <%= link_to point, vote_path(story.id, point, encoded_user), class: 'btn btn-default', remote: true %> 8 | <% end %> 9 | 10 | <%= link_to t('reset'), reset_path(story.id, encoded_user), class: 'btn btn-warning action-vote', remote: true %> 11 | <%= link_to t('reveal'), reveal_path(story.id), class: 'btn btn-info pull-right action-vote reveal', remote: true, disabled: !current_user_has_voted?(story) %> 12 |
    -------------------------------------------------------------------------------- /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. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/project_fixture.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Fixtures 3 | module ProjectFixture 4 | 5 | def project_params 6 | { 7 | id: stub_id, 8 | name: stub_text, 9 | velocity_scheme: stub_text, 10 | labels: stub_labels, 11 | point_scale: stub_point_scale 12 | } 13 | end 14 | 15 | def stub_projects(projects = []) 16 | PivotalTracker::Project.stubs(:all).returns(projects) 17 | end 18 | 19 | def stub_labels(n = 5) 20 | (1..n).map { |index| "label#{index}" }.join(',') 21 | end 22 | 23 | def stub_point_scale(n = 5) 24 | (1..n).to_a.join(',') 25 | end 26 | 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | 3 | def new 4 | redirect_to :root if user_signed_in? 5 | end 6 | 7 | def create 8 | user = User.authenticate(session_params) 9 | 10 | if user 11 | session[:user] = { 12 | username: user.username, 13 | token: user.token 14 | } 15 | 16 | redirect_to :root, notice: t('flashes.sessions.success') 17 | else 18 | redirect_to :login, alert: t('flashes.sessions.failed') 19 | end 20 | end 21 | 22 | def destroy 23 | reset_session 24 | redirect_to :login, notice: t('flashes.sessions.destroy') 25 | end 26 | 27 | private 28 | 29 | def session_params 30 | params.permit( 31 | :username, :password 32 | ) 33 | end 34 | end -------------------------------------------------------------------------------- /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 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require bootstrap 16 | //= require turbolinks 17 | //= require backtop 18 | //= require_tree . 19 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: your-secret-token 15 | 16 | test: 17 | secret_key_base: your-secret-token 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> -------------------------------------------------------------------------------- /spec/support/fixtures/story_fixture.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Fixtures 3 | module StoryFixture 4 | 5 | def story_params 6 | { 7 | id: stub_id, 8 | name: stub_text, 9 | current_state: stub_state, 10 | description: stub_words, 11 | story_type: stub_text, 12 | requested_by: stub_name, 13 | owned_by: stub_name 14 | } 15 | end 16 | 17 | def stub_stories(project, n = 10) 18 | project.stubs(:stories).returns( 19 | stub( 20 | all: (1..n).map { |i| init_object(story_params) } 21 | ) 22 | ) 23 | end 24 | 25 | def stub_state 26 | ['accepted', 'delivered' , 'started', 'rejected'].sample 27 | end 28 | 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: 'dashboard#index' 3 | 4 | get 'favicon.ico', controller: :application, action: :favicon 5 | 6 | controller :sessions do 7 | get :login, action: :new 8 | post :login, action: :create 9 | delete :logout, action: :destroy 10 | end 11 | 12 | resources :dashboard, only: [:index] 13 | 14 | controller :dashboard do 15 | get 'project/:id', action: :project, as: :project 16 | get 'story/:story_id/:vote/:user', action: :vote, as: :vote 17 | get 'reset/:story_id/:user', action: :reset, as: :reset 18 | get 'detail/:story_id/:toggle/:user', action: :detail, as: :detail 19 | get 'reveal/:story_id', action: :reveal, as: :reveal 20 | post 'select-vote', action: :select, as: :select 21 | post 'update-story', action: :update, as: :update 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /app/models/vote.rb: -------------------------------------------------------------------------------- 1 | class Vote 2 | include Mongoid::Document 3 | include Mongoid::Timestamps 4 | 5 | field :story_id, type: Integer 6 | field :vote, type: Integer 7 | field :user, type: String 8 | 9 | class << self 10 | 11 | def set(params) 12 | vote = find_by( 13 | user: params[:user], 14 | story_id: params[:story_id] 15 | ) 16 | 17 | if vote 18 | vote.update(vote: params[:vote]) 19 | else 20 | create( 21 | user: params[:user], 22 | story_id: params[:story_id], 23 | vote: params[:vote] 24 | ) 25 | end 26 | end 27 | 28 | def reset(params) 29 | vote = find_by( 30 | user: params[:user], 31 | story_id: params[:story_id] 32 | ) 33 | 34 | vote.delete if vote 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/assets/javascripts/dashboard.js.coffee: -------------------------------------------------------------------------------- 1 | filterStories = -> 2 | $('#sub-menu li').on 'click', -> 3 | $(this).addClass('selected').siblings().removeClass('selected') 4 | filterState(this) 5 | 6 | filterState = (selector) -> 7 | switch $(selector).prop('id') 8 | when 'filter-unestimated' 9 | $('.story').not('.story-unestimated').hide() 10 | $('.story-unestimated').show() 11 | else $('.story').show() 12 | 13 | selectCard = -> 14 | $('.card-revealed').on 'click', -> 15 | vote = $(this).parent().data('vote').split('-') 16 | 17 | $.ajax 18 | type: 'POST' 19 | url: '/select-vote' 20 | data: { story_id: vote[0], username: vote[1] } 21 | dataType: 'script' 22 | 23 | window.filterState = filterState 24 | window.selectCard = selectCard 25 | 26 | $(document).ready(filterStories) 27 | $(document).ready(selectCard) -------------------------------------------------------------------------------- /spec/models/story_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Story do 4 | 5 | context '.class' do 6 | 7 | context '#update' do 8 | let(:story) { OpenStruct.new } 9 | 10 | let(:params) {{ 11 | story: 'story', 12 | story_id: 123, 13 | project_id: 312 14 | }} 15 | 16 | before do 17 | PivotalTracker::Story.stubs(:find).returns(story) 18 | story.stubs(:update) 19 | end 20 | 21 | it 'should call find on PivotalTracker::Story' do 22 | PivotalTracker::Story.expects(:find).with( 23 | params[:story_id], params[:project_id] 24 | ).returns(story) 25 | 26 | Story.update(params) 27 | end 28 | 29 | it 'should call update on story' do 30 | story.expects(:update).with(params[:story]) 31 | Story.update(params) 32 | end 33 | end 34 | 35 | end 36 | end -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/unauthenticated.css.scss: -------------------------------------------------------------------------------- 1 | #unauthenticated { 2 | background-color: #3E934C; 3 | color: #888; 4 | 5 | #header { 6 | background: #FFF; 7 | padding: 5px 40px; 8 | 9 | h1 { 10 | font-family: 'Rokkitt', serif; 11 | margin: 0; 12 | padding: 0; 13 | font-size: 36px; 14 | color: #3E934C; 15 | 16 | span { 17 | color: #C72543; 18 | 19 | &.logo { 20 | font-size: 20px; 21 | } 22 | } 23 | } 24 | } 25 | 26 | #signin-box { 27 | background-color: #E9E9E9; 28 | padding: 25px 30px; 29 | margin-top: 10%; 30 | 31 | h1 { 32 | background-image: asset-url('pivotal-tracker.svg'); 33 | background-position: 0 0; 34 | background-repeat: no-repeat; 35 | display: block; 36 | width: 280px; 37 | height: 45px; 38 | margin: 20px 0; 39 | text-indent: -999em; 40 | overflow: hidden; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/views/dashboard/ajax/project.js.erb: -------------------------------------------------------------------------------- 1 | $('#stories').html("<%= j render('dashboard/stories/list', project: @project) %>"); 2 | $('#sidebar').html("<%= j render('dashboard/stories/project', project: @project) %>"); 3 | 4 | $('#project-<%= @project.id %>').addClass('active'); 5 | $('.projects-list').not('#project-<%= @project.id %>').removeClass('active'); 6 | 7 | if ($('#menu .active').parents('ul').prop('id') != 'menu') { 8 | $('#more-projects').addClass('active'); 9 | $('#more-projects .dropdown-toggle').html('<%= @project.name %> '); 10 | } else { 11 | $('#more-projects').removeClass('active'); 12 | $('#more-projects .dropdown-toggle').html('<%= t("projects.more") %> '); 13 | } 14 | 15 | $('#menu-compact').html('<%= @project.name %> '); 16 | $('.project-dropdown').removeClass('open') 17 | 18 | window.hideOverlay(); 19 | window.activeVote(); 20 | window.filterState('#sub-menu li.selected'); -------------------------------------------------------------------------------- /app/views/dashboard/ajax/detail.js.erb: -------------------------------------------------------------------------------- 1 | <% if @resource[:toggle] == 'expand' %> 2 | $('#expand-<%= @resource[:story_id] %>').closest('.story-overview').hide().siblings().show(); 3 | <% else %> 4 | $('#collapse-<%= @resource[:story_id] %>').closest('.story-detail').hide().siblings().show(); 5 | <% end %> 6 | 7 | <% broadcast '/planning-poker/story/toggle-voters' do %> 8 | <% if @resource[:toggle] == 'expand' %> 9 | if (!$('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= @resource[:user] %>"]').length) { 10 | $('#cards-<%= @resource[:story_id] %> ul').append( 11 | "<%= j render('dashboard/stories/vote', story_id: @resource[:story_id], 12 | vote: nil, user: @resource[:user], reveal: false) %>" 13 | ); 14 | } 15 | <% else %> 16 | if ($('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= current_user['username'] %>"] .card-blank').length) { 17 | $('.user-vote[data-vote="<%= @resource[:story_id] %>-<%= current_user['username'] %>"]').remove(); 18 | } 19 | <% end %> 20 | <% end %> -------------------------------------------------------------------------------- /app/views/dashboard/stories/_list.html.erb: -------------------------------------------------------------------------------- 1 | <% project.stories.all.each do |story| %> 2 |
    3 |
    4 |
    5 | <%= link_to detail_path(story.id, 'expand', encoded_user), id: "expand-#{story.id}", remote: true do %> 6 | 7 | <% end %> 8 | 9 | 10 | <%= raw estimation(story) %> 11 | 12 |
    13 |
    14 | <%= story.name %> 15 | 16 | <%= story.current_state %> 17 | 18 |
    19 |
    20 | 21 | <%= render 'dashboard/stories/story', project: project, story: story %> 22 |
    23 | <% end %> -------------------------------------------------------------------------------- /spec/features/sessions/exceptions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Sessions::Exceptions' do 4 | let(:user) { FactoryGirl.create(:user) } 5 | 6 | context 'unauthenticated' do 7 | before { visit root_path } 8 | 9 | specify { current_path.should eq login_path } 10 | specify { page.should have_content t('flashes.sessions.expired') } 11 | end 12 | 13 | context 'no token' do 14 | before do 15 | PivotalTracker::Client.stubs(:token).raises( 16 | PivotalTracker::Client::NoToken 17 | ) 18 | 19 | login 20 | end 21 | 22 | specify { page.should have_content t('flashes.sessions.token') } 23 | end 24 | 25 | context 'rest client unauthorized' do 26 | before do 27 | skip_authentication(DashboardController) 28 | 29 | PivotalTracker::Project.stubs(:all).raises( 30 | RestClient::Unauthorized 31 | ) 32 | 33 | visit root_path 34 | end 35 | 36 | specify { current_path.should eq login_path } 37 | specify { page.should have_content t('flashes.sessions.unauthorized') } 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /spec/features/dashboard/projects_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Dashboard::Projects' do 4 | before { skip_authentication(DashboardController) } 5 | 6 | context 'empty' do 7 | before do 8 | stub_projects 9 | visit root_path 10 | end 11 | 12 | specify { page.should have_content t('dashboard.empty.title') } 13 | specify { page.should have_content t('dashboard.empty.subtitle') } 14 | end 15 | 16 | context 'projects' do 17 | let(:projects) { (1..5).map { |i| init_object(project_params) }} 18 | let(:project) { projects.first } 19 | 20 | before do 21 | stub_projects(projects) 22 | stub_stories(project) 23 | visit root_path 24 | end 25 | 26 | specify { project_nodes.size.should eq 5} 27 | specify { story_nodes.size.should eq 10} 28 | specify { page.should have_content project.name } 29 | specify { page.should have_content project.velocity_scheme } 30 | specify { page.should have_content project.labels.split(',').join(', ') } 31 | specify { page.should have_content project.point_scale.split(',').join(', ') } 32 | end 33 | 34 | end -------------------------------------------------------------------------------- /app/views/dashboard/ajax/update.js.erb: -------------------------------------------------------------------------------- 1 | <% broadcast '/planning-poker/story/update-story' do %> 2 | 3 | window.flashNow( 4 | 'notice', 5 | "<%= t('flashes.stories.update', id: @resource.id) %>" 6 | ); 7 | 8 | $('#story-<%= @resource.id %> .story-detail').addClass('updated'); 9 | 10 | setTimeout(function(){ 11 | $('#story-<%= @resource.id %> .story-detail').removeClass('updated'); 12 | }, 5000); 13 | 14 | $('#story-<%= @resource.id %> textarea#name').html( 15 | '<%= j @resource.name %>' 16 | ); 17 | 18 | $('#story-<%= @resource.id %> textarea#description').html( 19 | '<%= j @resource.description %>' 20 | ); 21 | 22 | <% if @resource.estimate.to_i >= 0 %> 23 | 24 | $('#estimate-<%= @resource.id %>').html( 25 | "<%= j render('dashboard/stories/estimate', story: @resource) %>" 26 | ); 27 | 28 | $('#votes-<%= @resource.id %>').remove(); 29 | $('#form-<%= @resource.id %> input#estimate').remove(); 30 | $('#form-<%= @resource.id %> .story-actions').remove(); 31 | 32 | $('#story-<%= @resource.id %> textarea#name, #story-<%= @resource.id %> textarea#description') 33 | .prop('disabled', 'disabled'); 34 | 35 | <% end %> 36 | 37 | <% end %> -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | def favicon 7 | redirect_to '/assets/favicon.ico' 8 | end 9 | 10 | # Meant to be used as before_action in any controllers that are not publicly accessible 11 | def require_authentication 12 | redirect_to login_path, alert: t('flashes.sessions.expired') unless user_signed_in? 13 | end 14 | 15 | def reset_token 16 | PivotalTracker::Client.token = current_user['token'] if user_signed_in? 17 | end 18 | 19 | helper_method :user_signed_in?, :current_user 20 | 21 | def current_user 22 | @current_user ||= session[:user] 23 | end 24 | 25 | def user_signed_in? 26 | !!session[:user] 27 | end 28 | 29 | def rescue_steps(message) 30 | reset_session 31 | redirect_to login_path, alert: message 32 | end 33 | 34 | rescue_from PivotalTracker::Client::NoToken do |exception| 35 | rescue_steps t('flashes.sessions.token') 36 | end 37 | 38 | rescue_from RestClient::Unauthorized do |exception| 39 | rescue_steps t('flashes.sessions.unauthorized') 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pivotal Tracker: Planning Poker 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 10 | 11 | 12 | 16 | 17 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 18 | <%= javascript_include_tag publisher_client_path %> 19 | 20 | <%= favicon_link_tag %> 21 | 22 | <%= csrf_meta_tags %> 23 | 24 | 25 | 26 | <%= render 'notification' %> 27 | <%= render layout_class %> 28 | 29 | 34 | 35 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module PlanningPokerRails 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | config.i18n.enforce_available_locales = true 23 | 24 | # Do not swallow errors in after_commit/after_rollback callbacks. 25 | config.active_record.raise_in_transactional_callbacks = true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Helpers 3 | module SessionsHelper 4 | 5 | # Allows to get authenticated user without going through full authentication process. 6 | # To use this method you need to pass a class of the controller, example: 7 | # 8 | # skip_authentication(DashboardController) 9 | # 10 | def skip_authentication(controller) 11 | controller.any_instance.stubs(:require_authentication).returns(false) 12 | controller.any_instance.stubs(:current_user).returns(user) 13 | 14 | page.set_rack_session( 15 | user: { 16 | username: user.username, 17 | token: user.token 18 | } 19 | ) 20 | end 21 | 22 | # Login steps to authenticate user. 23 | # This method will stub a user and logs the user in to the system 24 | # 25 | def login 26 | visit login_path 27 | 28 | fill_in 'username', with: Forgery(:internet).email_address 29 | fill_in 'password', with: Forgery(:basic).password 30 | 31 | click_button t('signin').upcase 32 | end 33 | 34 | def logout 35 | click_link t('signout') 36 | end 37 | 38 | private 39 | 40 | def user 41 | @user ||= FactoryGirl.create(:user) 42 | end 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | def layout_class 4 | user_signed_in? ? 'authenticated' : 'unauthenticated' 5 | end 6 | 7 | def publisher_client_path 8 | "//#{publisher_domain}/faye/client.js" unless Rails.env.test? 9 | end 10 | 11 | def publisher_environment 12 | ['production', 'test'].include?(Rails.env) ? Rails.env : 'development' 13 | end 14 | 15 | def publisher_domain 16 | Rails.configuration.publisher[:domain] unless Rails.env.test? 17 | end 18 | 19 | def publisher_token 20 | unless Rails.env.test? 21 | digest = OpenSSL::Digest::Digest.new('sha256') 22 | secret = Rails.configuration.publisher[:secret] 23 | data = "/#{publisher_environment}/planning-poker/subscribe" 24 | 25 | OpenSSL::HMAC.hexdigest(digest, secret, data) 26 | end 27 | end 28 | 29 | def broadcast(channel, &block) 30 | unless Rails.env.test? 31 | message = { channel: channel, data: capture(&block), ext: { token: publisher_token }} 32 | uri = URI.parse("http://#{publisher_domain}/faye") 33 | 34 | Net::HTTP.post_form(uri, message: message.to_json) 35 | end 36 | end 37 | 38 | def encoded_user 39 | Base64.strict_encode64(current_user['username']) 40 | end 41 | 42 | def comma_separated(array) 43 | array.split(',').join(', ') 44 | end 45 | 46 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '4.2.5.1' 4 | gem 'sqlite3' 5 | gem 'thin' 6 | gem 'faye' 7 | 8 | # Heroku and New Relic 9 | gem 'rails_12factor' 10 | gem 'newrelic_rpm' 11 | 12 | # Assets 13 | gem 'jquery-rails' 14 | gem 'turbolinks' 15 | gem 'sass-rails', '~> 5.0' 16 | gem 'coffee-rails', '~> 4.1.0' 17 | gem 'uglifier', '>= 1.0.3' 18 | gem 'bootstrap-sass' 19 | gem 'therubyracer', require: 'v8' 20 | gem 'yui-compressor' 21 | gem 'font-awesome-rails' 22 | gem 'responders', '~> 2.0' 23 | 24 | # Custom gems 25 | gem 'pivotal-tracker' 26 | gem 'backtop' 27 | gem 'mongoid', github: 'mongoid' 28 | gem 'bson_ext' 29 | 30 | gem 'rspec-rails', group: [:test, :development] 31 | 32 | group :test do 33 | gem 'factory_girl_rails' 34 | gem 'forgery' 35 | gem 'capybara' 36 | gem 'selenium-webdriver' 37 | gem 'database_cleaner' 38 | gem 'rack_session_access' 39 | gem 'mocha', require: false 40 | gem 'zeus', require: false 41 | end 42 | 43 | group :development do 44 | gem 'spring' 45 | gem 'better_errors' 46 | gem 'binding_of_caller' 47 | gem 'capistrano', '~> 3.0', require: false 48 | gem 'capistrano-rails', '~> 1.1', require: false 49 | gem 'capistrano-bundler', '~> 1.1', require: false 50 | gem 'capistrano-rvm', '~> 0.1', require: false 51 | gem 'codeclimate-test-reporter', require: false 52 | end 53 | 54 | gem 'jbuilder', '~> 2.0' 55 | # bundle exec rake doc:rails generates the API under doc/api. 56 | gem 'sdoc', '~> 0.4.0', group: :doc 57 | -------------------------------------------------------------------------------- /app/views/application/_authenticated.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'overlay' %> 2 | 3 |
    4 | 21 | 22 | <% if @projects.blank? %> 23 | 24 |
    25 |
    26 | 27 |

    28 | <%= t('dashboard.empty.title') %> 29 | <%= t('dashboard.empty.subtitle') %> 30 |

    31 |
    32 |
    33 | 34 | <% else %> 35 | 36 | 46 | 47 |
    48 | <%= yield %> 49 |
    50 | 51 | <% end %> 52 |
    -------------------------------------------------------------------------------- /app/views/dashboard/stories/_project.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | <%= t('projects.info') %> 4 |

    5 | 6 | 58 |
    -------------------------------------------------------------------------------- /spec/models/vote_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Vote do 4 | let(:vote) { FactoryGirl.create(:vote) } 5 | 6 | describe '.class' do 7 | 8 | context '#set' do 9 | let(:params) {{ 10 | user: vote.user, 11 | story_id: vote.story_id, 12 | vote: Forgery(:basic).number(at_least: 1, at_most: 5) 13 | }} 14 | 15 | after { Vote.set(params) } 16 | 17 | it 'should call find_by on Vote' do 18 | Vote.expects(:find_by).with( 19 | user: params[:user], 20 | story_id: params[:story_id] 21 | ) 22 | end 23 | 24 | it 'should call update on vote instance' do 25 | Vote.any_instance.expects(:update).with( 26 | vote: params[:vote] 27 | ) 28 | end 29 | 30 | it 'should create vote' do 31 | vote.destroy 32 | Vote.expects(:create).with( 33 | user: params[:user], 34 | story_id: params[:story_id], 35 | vote: params[:vote] 36 | ) 37 | end 38 | end 39 | 40 | context '#reset' do 41 | let(:params) {{ 42 | user: vote.user, 43 | story_id: vote.story_id, 44 | }} 45 | 46 | after { Vote.reset(params) } 47 | 48 | it 'should call find_by on Vote' do 49 | Vote.expects(:find_by).with( 50 | user: params[:user], 51 | story_id: params[:story_id] 52 | ) 53 | end 54 | 55 | it 'should call delete on vote instance' do 56 | Vote.any_instance.expects(:delete) 57 | end 58 | end 59 | 60 | end 61 | end -------------------------------------------------------------------------------- /app/views/application/_menu.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 15 |
    16 | 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | all: "All" 3 | cancel: "Cancel" 4 | editable: "Editable" 5 | na: "N/A" 6 | owner: "Owner" 7 | planning: "Planning" 8 | poker: "Poker" 9 | reset: "Reset" 10 | requester: "Requester" 11 | reveal: "Reveal" 12 | save: "Save" 13 | signin: "Sign In" 14 | signout: "Sign Out" 15 | story: "Story" 16 | unestimated: "Unestimated" 17 | 18 | dashboard: 19 | empty: 20 | title: "You do not own or have any projects assigned." 21 | subtitle: "Please create a project in Pivotal Tracker first!" 22 | 23 | projects: 24 | id: "Project ID" 25 | info: "Project Info" 26 | labels: "Project Labels" 27 | more: "More Project(s)" 28 | name: "Project Name" 29 | scheme: "Project Scheme" 30 | scale: "Project Point Scale" 31 | 32 | stories: 33 | id: "Story ID" 34 | type: "Story Type" 35 | points: "Story Points" 36 | state: "Story State" 37 | 38 | # Flashes 39 | flashes: 40 | sessions: 41 | destroy: "You have successfully logged out" 42 | expired: "Your session has expired" 43 | failed: "Invalid username or password" 44 | token: "Your API Key / Token is not yet set up. Please set one up in Pivotal Tracker!" 45 | signin: "You will need to sign in first" 46 | success: "You have successfully logged in" 47 | unauthorized: "No API Key / Token found. Please re-login!" 48 | 49 | stories: 50 | update: "Story ID#%{id} has been updated successfully" -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 | div> 61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/views/dashboard/stories/_info.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

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

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.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 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Debug mode disables concatenation and preprocessing of assets. 23 | # This option may cause significant delays in view rendering with a large 24 | # number of complex assets. 25 | config.assets.debug = true 26 | 27 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 28 | # yet still be able to expire them through the digest params. 29 | config.assets.digest = true 30 | 31 | # Adds additional error checking when serving assets at runtime. 32 | # Checks for improperly declared sprockets dependencies. 33 | # Raises helpful error messages. 34 | config.assets.raise_runtime_errors = true 35 | 36 | # Raises error for missing translations 37 | # config.action_view.raise_on_missing_translations = true 38 | 39 | # Publisher for push notification 40 | config.publisher = { 41 | # development faye server 42 | domain: 'localhost:9292', 43 | # secret key 44 | secret: 'secret' 45 | } 46 | end 47 | -------------------------------------------------------------------------------- /app/views/dashboard/stories/_story.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

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

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | let(:username) { Forgery(:internet).email_address } 5 | let(:password) { 'password' } 6 | 7 | let(:params) {{ 8 | username: username, 9 | password: password 10 | }} 11 | 12 | let(:user) { FactoryGirl.create(:user) } 13 | 14 | describe '.class' do 15 | 16 | context '#authenticate' do 17 | before { User.stubs(:create).returns(user) } 18 | 19 | it 'should return nil' do 20 | User.authenticate({}).should be_nil 21 | end 22 | 23 | it 'should call find_by on User' do 24 | User.expects(:find_by).with( 25 | username: params[:username] 26 | ) 27 | 28 | User.authenticate(params) 29 | end 30 | 31 | it 'should return user' do 32 | User.authenticate(params).should eq user 33 | end 34 | 35 | it 'should create user' do 36 | user.delete 37 | User.expects(:create).with(params) 38 | User.authenticate(params) 39 | end 40 | end 41 | 42 | context '#create' do 43 | before do 44 | PivotalTracker::Client.stubs(:token).returns(user.token) 45 | User.stubs(:salted).returns(user.salt) 46 | end 47 | 48 | after { User.create(params) } 49 | 50 | it 'should call token on PivotalTracker::Client' do 51 | PivotalTracker::Client.expects(:token).with( 52 | params[:username], 53 | params[:password] 54 | ).returns(user.token) 55 | end 56 | 57 | it 'should call salted' do 58 | User.expects(:salted).with(params[:username]) 59 | end 60 | 61 | it 'should call new' do 62 | User.expects(:new).with(params.merge(token: user.token)).returns(user) 63 | end 64 | 65 | it 'should call save' do 66 | User.any_instance.expects(:save) 67 | end 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.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 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Access to rack session 41 | config.middleware.use RackSessionAccess::Middleware 42 | 43 | # Raises error for missing translations 44 | # config.action_view.raise_on_missing_translations = true 45 | end 46 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | prefork = -> { 2 | ENV["RAILS_ENV"] = 'test' 3 | 4 | require 'codeclimate-test-reporter' 5 | CodeClimate::TestReporter.start 6 | require File.expand_path("../../config/environment", __FILE__) 7 | require 'rspec/rails' 8 | require 'forgery/forgery' 9 | require 'mocha/setup' 10 | 11 | # Add this to load Capybara integration: 12 | require 'capybara/rspec' 13 | require 'capybara/rails' 14 | require 'rack_session_access/capybara' 15 | 16 | # Requires supporting ruby files with custom matchers and macros, etc, 17 | # in spec/support/ and its subdirectories. 18 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 19 | 20 | RSpec.configure do |config| 21 | config.mock_with :mocha 22 | config.infer_base_class_for_anonymous_controllers = false 23 | config.order = "random" 24 | 25 | ['Basic', 'Project', 'Story'].each do |fixture| 26 | config.include "Support::Fixtures::#{fixture}Fixture".constantize 27 | end 28 | 29 | ['Application', 'Sessions', 'Dashboard', 'Views'].each do |helper| 30 | config.include "Support::Helpers::#{helper}Helper".constantize 31 | end 32 | 33 | config.before(:each) do 34 | DatabaseCleaner.clean_with :truncation 35 | DatabaseCleaner.strategy = :truncation 36 | end 37 | 38 | config.before(:each) do 39 | DatabaseCleaner.start 40 | end 41 | 42 | config.after(:each) do 43 | DatabaseCleaner.clean 44 | end 45 | end 46 | } 47 | 48 | each_run = -> { 49 | # Reload routes in config/routes.rb. 50 | Rails.application.reload_routes! 51 | 52 | # Reload factories in spec/factories. 53 | FactoryGirl.reload 54 | 55 | # Reload language bundles in config/locales. 56 | I18n.backend.reload! 57 | } 58 | 59 | if defined?(Zeus) 60 | prefork.call 61 | $each_run = each_run 62 | class << Zeus.plan 63 | def after_fork_with_test 64 | after_fork_without_test 65 | $each_run.call 66 | end 67 | alias_method_chain :after_fork, :test 68 | end 69 | else 70 | prefork.call 71 | each_run.call 72 | end -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include Mongoid::Document 3 | include Mongoid::Timestamps 4 | 5 | field :username, type: String 6 | field :password, type: String 7 | field :salt, type: String 8 | field :token, type: String 9 | 10 | # user registration logic specific callback 11 | before_save { |record| record.encrypt_password } 12 | 13 | class << self 14 | # Authenticates a user by their username and 15 | # unencrypted password. Returns the user or nil. 16 | def authenticate(params) 17 | return if params[:password].blank? 18 | 19 | user = find_by( 20 | username: params[:username] 21 | ) # need to get the salt 22 | 23 | user && user.authenticated?(params[:password]) ? user : create(params) 24 | end 25 | 26 | def create(params) 27 | # Set Pivotal Tracker token 28 | token = PivotalTracker::Client.token( 29 | params[:username], 30 | params[:password] 31 | ) 32 | 33 | salt = salted( 34 | params[:username] 35 | ) 36 | 37 | user = new( 38 | username: params[:username], 39 | password: params[:password], 40 | token: token 41 | ) 42 | 43 | user.save 44 | 45 | user 46 | end 47 | 48 | # Creates the salt 49 | def salted(username) 50 | Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{username}--") 51 | end 52 | 53 | # Encrypts some data with the salt. 54 | def encrypt(password, salt) 55 | Digest::SHA1.hexdigest("--#{salt}--#{password}--") 56 | end 57 | end 58 | 59 | # Checks passwords against crypted password 60 | def authenticated?(password) 61 | self.password == encrypt(password) 62 | end 63 | 64 | # Encrypts the password with the user salt 65 | def encrypt(password) 66 | self.class.encrypt(password, salt) 67 | end 68 | 69 | protected 70 | # before filter 71 | def encrypt_password 72 | return if password.blank? 73 | 74 | self.salt = self.class.salted(username) if new_record? 75 | self.password = encrypt(password) 76 | end 77 | 78 | end -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SessionsController do 4 | 5 | let(:user) { FactoryGirl.build(:user) } 6 | 7 | before do 8 | SessionsController.any_instance.stubs(:user_signed_in?).returns(true) 9 | end 10 | 11 | def valid_session 12 | {} 13 | end 14 | 15 | describe 'GET new' do 16 | 17 | it 'should redirect to root' do 18 | get :new, {}, valid_session 19 | response.should redirect_to root_path 20 | end 21 | end 22 | 23 | describe 'POST create' do 24 | let(:params) {{ 25 | 'username' => user.username, 26 | 'password' => user.password 27 | }} 28 | 29 | context 'user exist' do 30 | before { User.stubs(:authenticate).returns(user) } 31 | 32 | it 'should call authenticate on User' do 33 | User.expects(:authenticate).with(params) 34 | post :create, params, valid_session 35 | end 36 | 37 | it 'should set user session' do 38 | post :create, params, valid_session 39 | session[:user].should eq({ username: user.username, token: user.token }) 40 | end 41 | 42 | it 'should redirect to root' do 43 | post :create, params, valid_session 44 | response.should redirect_to root_path 45 | end 46 | end 47 | 48 | context 'user does not exist' do 49 | before { User.stubs(:authenticate).returns(nil) } 50 | 51 | it 'should redirect to login path' do 52 | post :create, params, valid_session 53 | response.should redirect_to login_path 54 | end 55 | 56 | end 57 | end 58 | 59 | describe 'DELETE destroy' do 60 | before do 61 | session[:user] = { 62 | username: user.username, 63 | token: user.token 64 | } 65 | end 66 | 67 | it 'should reset session' do 68 | delete :destroy, {}, valid_session 69 | session.should_not have_key(:user) 70 | end 71 | 72 | it 'should redirect to login' do 73 | delete :destroy, {}, valid_session 74 | response.should redirect_to login_path 75 | end 76 | end 77 | 78 | end -------------------------------------------------------------------------------- /app/controllers/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class DashboardController < ApplicationController 2 | before_action :require_authentication, :reset_token, :decode_user 3 | respond_to :js 4 | 5 | def index 6 | @projects = PivotalTracker::Project.all 7 | end 8 | 9 | # Ajax 10 | def project 11 | @project = PivotalTracker::Project.find(params[:id].to_i) 12 | 13 | respond_with @project do |format| 14 | format.js { render 'dashboard/ajax/project' } 15 | end 16 | end 17 | 18 | def vote 19 | Vote.set(params) 20 | 21 | @resource = { 22 | story_id: params[:story_id], 23 | vote: params[:vote], 24 | user: params[:user] 25 | } 26 | 27 | respond_with @resource do |format| 28 | format.js { render 'dashboard/ajax/vote' } 29 | end 30 | end 31 | 32 | def reset 33 | Vote.reset(params) 34 | 35 | @resource = { 36 | story_id: params[:story_id], 37 | user: params[:user] 38 | } 39 | 40 | respond_with @resource do |format| 41 | format.js { render 'dashboard/ajax/reset' } 42 | end 43 | end 44 | 45 | def detail 46 | @resource = { 47 | story_id: params[:story_id], 48 | toggle: params[:toggle], 49 | user: params[:user] 50 | } 51 | 52 | respond_with @resource do |format| 53 | format.js { render 'dashboard/ajax/detail' } 54 | end 55 | end 56 | 57 | def reveal 58 | @resource = { 59 | story_id: params[:story_id] 60 | } 61 | 62 | respond_with @resource do |format| 63 | format.js { render 'dashboard/ajax/reveal' } 64 | end 65 | end 66 | 67 | def update 68 | @resource = Story.update(params) 69 | 70 | respond_with @resource do |format| 71 | format.js { render 'dashboard/ajax/update' } 72 | end 73 | end 74 | 75 | def select 76 | @resource = { 77 | story_id: params[:story_id], 78 | username: params[:username] 79 | } 80 | 81 | respond_with @resource do |format| 82 | format.js { render 'dashboard/ajax/select' } 83 | end 84 | end 85 | 86 | protected 87 | 88 | def decode_user 89 | params[:user] = Base64.strict_decode64(params[:user]) if params and params[:user] 90 | end 91 | end -------------------------------------------------------------------------------- /app/assets/javascripts/common.js.coffee: -------------------------------------------------------------------------------- 1 | flashMsg = -> 2 | if $('.flash').length 3 | $('.flash').last().fadeIn('slow').siblings().remove() 4 | 5 | setTimeout( 6 | -> 7 | $('.flash').fadeOut('slow') 8 | 3000 9 | ) 10 | 11 | flashNow = (flag, msg) -> 12 | $('#flash-container').append( 13 | "
    " + 14 | "
    " + msg + "
    " 15 | ) 16 | 17 | flashMsg() 18 | 19 | displayOverlay = -> 20 | $('#overlay').fadeIn() 21 | 22 | hideOverlay = -> 23 | $('#overlay').fadeOut() 24 | 25 | menuClick = -> 26 | $('#menu a, #menu-compact-list a').on 'click', () -> displayOverlay() 27 | 28 | activeVote = -> 29 | $('.btn-votes a').on 'click', () -> 30 | $(this).not('.action-vote').addClass('active') 31 | $(this).siblings().removeClass('active') 32 | 33 | fayeSetup = -> 34 | publisher = new Faye.Client('//' + window.publisher + '/faye') 35 | 36 | publisher.addExtension outgoing: (message, callback) -> 37 | message.ext = token: window.pubtoken if message.channel is '/meta/subscribe' 38 | callback message 39 | 40 | publisher 41 | 42 | fayeSubscribers = -> 43 | unless window.environment is 'test' 44 | if $('#authenticated').length 45 | fayeSetup().subscribe '/planning-poker/story/add-vote', (data) -> eval(data) 46 | fayeSetup().subscribe '/planning-poker/story/reset-vote', (data) -> eval(data) 47 | fayeSetup().subscribe '/planning-poker/story/select-vote', (data) -> eval(data) 48 | fayeSetup().subscribe '/planning-poker/story/reveal-votes', (data) -> eval(data) 49 | fayeSetup().subscribe '/planning-poker/story/update-story', (data) -> eval(data) 50 | fayeSetup().subscribe '/planning-poker/story/toggle-voters', (data) -> eval(data) 51 | 52 | window.hideOverlay = hideOverlay 53 | window.activeVote = activeVote 54 | window.flashNow = flashNow 55 | 56 | $(document).ready(flashMsg) 57 | $(document).ready(menuClick) 58 | $(document).ready(activeVote) 59 | $(document).ready(fayeSubscribers) 60 | 61 | $(document).on('page:load', flashMsg) 62 | $(document).on('page:load', menuClick) 63 | $(document).on('page:load', fayeSubscribers) 64 | 65 | document.addEventListener('page:fetch', displayOverlay) 66 | document.addEventListener('page:receive', hideOverlay) 67 | document.addEventListener('page:change', hideOverlay) -------------------------------------------------------------------------------- /app/helpers/dashboard_helper.rb: -------------------------------------------------------------------------------- 1 | module DashboardHelper 2 | 3 | def icon(story) 4 | case story.story_type 5 | when 'feature' then 'fa fa-star orange' 6 | when 'bug' then 'fa fa-bug red' 7 | when 'chore' then 'fa fa-cog' 8 | when 'release' then 'fa fa-flag-checkered green' 9 | else 10 | story.story_type 11 | end 12 | end 13 | 14 | def bar_icons(size) 15 | '' * size 16 | end 17 | 18 | def show_estimation_icons?(story) 19 | story.estimate.to_i <= 5 20 | end 21 | 22 | def small_strong_text(text) 23 | "#{text}" 24 | end 25 | 26 | 27 | def estimation(story) 28 | if estimated?(story) 29 | show_estimation_icons?(story) ? bar_icons(story.estimate) : small_strong_text(story.estimate) 30 | end 31 | end 32 | 33 | def estimation_class(story) 34 | if type_chore?(story) 35 | 'unestimateable' 36 | else 37 | (story.estimate.to_i >= 0) ? 'estimated' : 'unestimated' 38 | end 39 | end 40 | 41 | def state(story) 42 | case story.current_state 43 | when 'accepted' then "label label-success #{story.current_state}" 44 | when 'delivered' then "label label-warning #{story.current_state}" 45 | when 'started' then "label label-info #{story.current_state}" 46 | when 'rejected' then "label label-danger #{story.current_state}" 47 | else 48 | "label label-default #{story.current_state}" 49 | end 50 | end 51 | 52 | def card_class(vote, reveal) 53 | if vote.nil? 54 | 'card card-blank' 55 | elsif vote.is_a?(FalseClass) 56 | 'card card-hidden' 57 | elsif reveal 58 | 'card card-revealed' 59 | else 60 | 'card' 61 | end 62 | end 63 | 64 | # This method is used to check story state or type 65 | # state_accepted? - returns true if story current state is accepted 66 | # type_chore? - returns true if story type is chore 67 | # - etc - 68 | # 69 | def method_missing(meth_id, *args) 70 | if meth_id =~ /state_([a-z_]+)?/ 71 | args.first.current_state == meth_id.to_s.chop.split('state_').last 72 | elsif meth_id =~ /type_([a-z_]+)?/ 73 | args.first.story_type == meth_id.to_s.chop.split('type_').last 74 | else 75 | super 76 | end 77 | end 78 | 79 | def unestimateable?(story) 80 | estimate_not_applicable?(story) or 81 | not estimateable?(story) 82 | end 83 | 84 | def estimateable?(story) 85 | story.estimate and story.estimate.to_i < 0 86 | end 87 | 88 | def estimated?(story) 89 | story.estimate and story.estimate.to_i > 0 90 | end 91 | 92 | def estimate(story) 93 | estimate_not_applicable?(story) ? t('na') : story.estimate 94 | end 95 | 96 | def estimate_not_applicable?(story) 97 | type_chore?(story) or 98 | type_bug?(story) 99 | end 100 | 101 | def voting(vote) 102 | (vote.user == current_user['username']) ? vote.vote : false 103 | end 104 | 105 | def nickname(user) 106 | user.split('@').first.split('.').first 107 | end 108 | 109 | def current_user_has_voted?(story) 110 | Vote.find_by(story_id: story.id, user: current_user['username']).present? 111 | end 112 | 113 | end -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.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 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | # Compress JavaScripts and CSS. 27 | config.assets.js_compressor = :uglifier 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Generate digests for assets URLs. 34 | config.assets.digest = true 35 | 36 | # Version of your assets, change this if you want to expire all your assets. 37 | config.assets.version = '1.0' 38 | 39 | # Specifies the header that your server uses for sending files. 40 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 41 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 42 | 43 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 44 | # config.force_ssl = true 45 | 46 | # Use the lowest log level to ensure availability of diagnostic information 47 | # when problems arise. 48 | config.log_level = :debug 49 | 50 | # Prepend all log lines with the following tags. 51 | # config.log_tags = [ :subdomain, :uuid ] 52 | 53 | # Use a different logger for distributed setups. 54 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 60 | # config.action_controller.asset_host = 'http://assets.example.com' 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation can not be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Disable automatic flushing of the log to improve performance. 74 | # config.autoflush_log = false 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Do not dump schema after migrations. 80 | config.active_record.dump_schema_after_migration = false 81 | 82 | # Publisher for push notification 83 | config.publisher = { 84 | # production faye server - change the URL so that it points to the correct server. 85 | domain: (ENV['FAYE_URL'] || 'localhost:9292'), 86 | # secret key 87 | secret: (ENV['FAYE_SECRET'] || 'secret') 88 | } 89 | end 90 | -------------------------------------------------------------------------------- /spec/controllers/dashboard_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DashboardController do 4 | 5 | before do 6 | skip_authentication(DashboardController) 7 | end 8 | 9 | def valid_session 10 | {} 11 | end 12 | 13 | describe 'GET index' do 14 | before { PivotalTracker::Project.stubs(:all) } 15 | 16 | it 'should call all on PivotalTracker::Project' do 17 | PivotalTracker::Project.expects(:all) 18 | get :index, {}, valid_session 19 | end 20 | end 21 | 22 | describe 'GET project' do 23 | before { PivotalTracker::Project.stubs(:find) } 24 | let(:params) {{ id: 1 }} 25 | 26 | it 'should call find on PivotalTracker::Project' do 27 | PivotalTracker::Project.expects(:find).with(params[:id]) 28 | xhr :get, :project, params, valid_session, format: :js 29 | end 30 | 31 | it 'should render ajax project' do 32 | xhr :get, :project, params, valid_session, format: :js 33 | response.should render_template 'dashboard/ajax/project' 34 | end 35 | end 36 | 37 | describe 'GET reset' do 38 | before { Vote.stubs(:reset) } 39 | 40 | let(:params) {{ 41 | 'story_id' => '123', 42 | 'user' => encoded_user, 43 | 'controller' => 'dashboard', 44 | 'action' => 'reset' 45 | }} 46 | 47 | it 'should call reset on Vote' do 48 | Vote.expects(:reset).with(params.merge('user' => decoded_user(params['user']))) 49 | xhr :get, :reset, params, valid_session, format: :js 50 | end 51 | 52 | it 'should assign resource' do 53 | xhr :get, :reset, params, valid_session, format: :js 54 | assigns(:resource).should eq({ story_id: params['story_id'], user: decoded_user(params['user']) }) 55 | end 56 | 57 | it 'should render ajax reset' do 58 | xhr :get, :reset, params, valid_session, format: :js 59 | response.should render_template 'dashboard/ajax/reset' 60 | end 61 | end 62 | 63 | describe 'GET detail' do 64 | let(:params) {{ 65 | story_id: '123', 66 | toggle: 'dashboard', 67 | user: encoded_user 68 | }} 69 | 70 | it 'should assign resource' do 71 | xhr :get, :detail, params, valid_session, format: :js 72 | assigns(:resource).should eq params.merge(user: decoded_user(params[:user])) 73 | end 74 | 75 | it 'should render ajax detail' do 76 | xhr :get, :detail, params, valid_session, format: :js 77 | response.should render_template 'dashboard/ajax/detail' 78 | end 79 | end 80 | 81 | describe 'GET reveal' do 82 | let(:params) {{ 83 | story_id: '123' 84 | }} 85 | 86 | it 'should assign resource' do 87 | xhr :get, :reveal, params, valid_session, format: :js 88 | assigns(:resource).should eq params 89 | end 90 | 91 | it 'should render ajax reveal' do 92 | xhr :get, :reveal, params, valid_session, format: :js 93 | response.should render_template 'dashboard/ajax/reveal' 94 | end 95 | end 96 | 97 | describe 'POST update' do 98 | let(:params) {{ 99 | 'id' => '1', 100 | 'controller' => 'dashboard', 101 | 'action' => 'update' 102 | }} 103 | 104 | before { Story.stubs(:update) } 105 | 106 | it 'should call update on Story' do 107 | Story.expects(:update).with(params) 108 | xhr :post, :update, params, valid_session, format: :js 109 | end 110 | 111 | it 'should render ajax update' do 112 | xhr :post, :update, params, valid_session, format: :js 113 | response.should render_template 'dashboard/ajax/update' 114 | end 115 | end 116 | 117 | describe 'POST select' do 118 | let(:params) {{ 119 | story_id: '123', 120 | username: 'username' 121 | }} 122 | 123 | it 'should assign resource' do 124 | xhr :post, :select, params, valid_session, format: :js 125 | assigns(:resource).should eq params 126 | end 127 | 128 | it 'should render ajax select' do 129 | xhr :post, :select, params, valid_session, format: :js 130 | response.should render_template 'dashboard/ajax/select' 131 | end 132 | end 133 | 134 | end -------------------------------------------------------------------------------- /app/assets/images/pivotal-tracker.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pivotal Tracker: Planning Poker 2 | 3 | [![Code Quality](https://codeclimate.com/github/Foodee/planning-poker/badges/gpa.svg)](https://codeclimate.com/github/Foodee/planning-poker) 4 | [![Test Coverage](https://codeclimate.com/github/Foodee/planning-poker/badges/coverage.svg)](https://codeclimate.com/github/Foodee/planning-poker) 5 | 6 | Planning poker, also called Scrum poker, is a consensus-based technique for estimating, mostly used to estimate effort or relative size of user stories in software development. In planning poker, members of the group make estimates by playing numbered cards in the app, instead of speaking them aloud. The cards are revealed, and the estimates are then discussed. By hiding the figures in this way, the group can avoid the cognitive bias of anchoring, where the first number spoken aloud sets a precedent for subsequent estimates. 7 | 8 | ## Features 9 | * Authentication from Pivotal Tracker 10 | * Displays all user's projects in Pivotal Tracker 11 | * Push Notifications when playing cards 12 | * Bootstrap + Font-Awesome for Responsive Design 13 | 14 | ## Dependencies 15 | * Ruby 2.1.3 16 | * MongoDB 17 | * Twitter Bootstrap 18 | * Font-Awesome 19 | * Pivotal Tracker gem (currently v3) 20 | * Thin webserver for Faye 21 | * Faye for Push Notifications 22 | 23 | ## Frameworks 24 | This application uses the following frameworks: 25 | 26 | * [Ruby on Rails](http://rubyonrails.org/) (4.2.5.1) 27 | * [Twitter Bootstrap](http://twitter.github.com/bootstrap/) (Front-end) 28 | * [jQuery](http://jquery.com/) (Javascript) 29 | 30 | ## Gems 31 | This application uses the following gems: 32 | 33 | * [Pivotal Tracker](https://github.com/jsmestad/pivotal-tracker) (Pivotal Tracker gem) 34 | * [Faye](http://faye.jcoglan.com/) (Faye) 35 | * [Mongoid](https://github.com/mongoid/mongoid) (Mongoid) 36 | * [Twitter Bootstrap SASS](https://github.com/twbs/bootstrap-sass) (Twitter Bootstrap port to SASS) 37 | * [Font-Awesome](https://github.com/bokmann/font-awesome-rails) (Font-Awesome Rails) 38 | * [BackTop](https://github.com/bimovidia/backtop) (Back To Top functionality) 39 | 40 | ## Getting Started 41 | 42 | Planning Poker uses MongoDB as its database. To install it, please refer to the following link: 43 | 44 | * [Install MongoDB](http://docs.mongodb.org/manual/installation/) - MongoDB installation tutorial 45 | * [Mongoid](http://mongoid.org/en/mongoid/index.html) - Mongoid official website for documentations 46 | 47 | Planning Poker uses push notification with Faye. There are configuration files for each environments in config/environments: 48 | 49 | config.publisher = { 50 | # production faye server - change the URL so that it points to the correct server. 51 | domain: 'localhost:9292', 52 | # secret key 53 | secret: 'secret' 54 | } 55 | 56 | Change the domain into the actual production domain when deploying the app. You might want to change the secret key as well. 57 | 58 | To get started, clone this repository to your local machine, and install dependencies: 59 | 60 | ```shell 61 | git clone git@github.com:bimovidia/planning-poker.git 62 | cd planning-poker 63 | bundle install 64 | ``` 65 | 66 | Open a new terminal and start the faye server: 67 | 68 | ```shell 69 | rackup faye.ru -s thin -E production -p 9292 70 | ``` 71 | 72 | Run the application locally with: 73 | 74 | ```shell 75 | rails s 76 | ``` 77 | 78 | ## Rule of the Game 79 | Once you have the application set up, you can: 80 | 81 | * Login using your Pivotal Tracker credential 82 | * Click any project name in the header menu that has unestimated stories 83 | * Click on **UNESTIMATED** in the submenu to see the list of unestimated stories for that project 84 | * Click / Expand the story to start the planning poker session 85 | 86 | Each estimator can select any point depending on the story scale point setting in pivotal tracker. The values represent the number of story points, ideal days, or other units in which the team agrees to estimate. 87 | 88 | The estimators discuss the feature, asking questions of the product owner as needed. When the feature has been fully discussed, each estimator privately selects one card to represent his or her estimate. 89 | 90 | Once all selections are in, any estimator can then hit the **REVEAL** button to reveal the cards at the same time. 91 | 92 | If all estimators selected the same value, that becomes the estimate. If not, the estimators discuss their estimates. The high and low estimators should especially share their reasons. After further discussion, once the estimation score has been agreed on, any estimator can then click on any card representing that number. After that, any user can update the story name and description (if necessary) and then hit **SAVE** to update the story in Pivotal Tracker. 93 | 94 | ## Contributing 95 | Contributions are encouraged. You can contribute in many ways. For example, you might: 96 | 97 | * add documentation and "how-to" articles to the README or Wiki 98 | * create an extension that provides additional functionality above and beyond Planning Poker itself 99 | * fix bugs you've found in the Issue Tracker or improve / add new features to Planning Poker 100 | 101 | When contributing, you will need to: 102 | 103 | * follow the same style / convention used throughout the app (as much as you can) 104 | * make sure your codes are well-tested and reviewed 105 | * when fixing a bug, provide a failing test case that your patch solves 106 | * provide proper documentation when necessary 107 | 108 | ## Copyright 109 | 110 | Planning Poker is licensed under the [MIT License](http://opensource.org/licenses/mit-license.html) 111 | -------------------------------------------------------------------------------- /config/mongoid.yml: -------------------------------------------------------------------------------- 1 | development: 2 | # Configure available database clients. (required) 3 | clients: 4 | # Defines the default client. (required) 5 | default: 6 | # Defines the name of the default database that Mongoid can connect to. 7 | # (required). 8 | database: planning_poker_rails_development 9 | # Provides the hosts the default client can connect to. Must be an array 10 | # of host:port pairs. (required) 11 | hosts: 12 | - localhost:27017 13 | options: 14 | # Change the default write concern. (default = { w: 1 }) 15 | # write: 16 | # w: 1 17 | 18 | # Change the default read preference. Valid options for mode are: :secondary, 19 | # :secondary_preferred, :primary, :primary_preferred, :nearest 20 | # (default: primary) 21 | # read: 22 | # mode: :secondary_preferred 23 | 24 | # The name of the user for authentication. 25 | # user: 'user' 26 | 27 | # The password of the user for authentication. 28 | # password: 'password' 29 | 30 | # The user's database roles. 31 | # roles: 32 | # - 'dbOwner' 33 | 34 | # Change the default authentication mechanism. Valid options are: :scram, 35 | # :mongodb_cr, :mongodb_x509, and :plain. (default on 3.0 is :scram, default 36 | # on 2.4 and 2.6 is :plain) 37 | # auth_mech: :scram 38 | 39 | # The database or source to authenticate the user against. (default: admin) 40 | # auth_source: admin 41 | 42 | # Force a the driver cluster to behave in a certain manner instead of auto- 43 | # discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct 44 | # when connecting to hidden members of a replica set. 45 | # connect: :direct 46 | 47 | # Changes the default time in seconds the server monitors refresh their status 48 | # via ismaster commands. (default: 10) 49 | # heartbeat_frequency: 10 50 | 51 | # The time in seconds for selecting servers for a near read preference. (default: 5) 52 | # local_threshold: 5 53 | 54 | # The timeout in seconds for selecting a server for an operation. (default: 30) 55 | # server_selection_timeout: 30 56 | 57 | # The maximum number of connections in the connection pool. (default: 5) 58 | # max_pool_size: 5 59 | 60 | # The minimum number of connections in the connection pool. (default: 1) 61 | # min_pool_size: 1 62 | 63 | # The time to wait, in seconds, in the connection pool for a connection 64 | # to be checked in before timing out. (default: 5) 65 | # wait_queue_timeout: 5 66 | 67 | # The time to wait to establish a connection before timing out, in seconds. 68 | # (default: 5) 69 | # connect_timeout: 5 70 | 71 | # The timeout to wait to execute operations on a socket before raising an error. 72 | # (default: 5) 73 | # socket_timeout: 5 74 | 75 | # The name of the replica set to connect to. Servers provided as seeds that do 76 | # not belong to this replica set will be ignored. 77 | # replica_set: name 78 | 79 | # Whether to connect to the servers via ssl. (default: false) 80 | # ssl: true 81 | 82 | # The certificate file used to identify the connection against MongoDB. 83 | # ssl_cert: /path/to/my.cert 84 | 85 | # The private keyfile used to identify the connection against MongoDB. 86 | # Note that even if the key is stored in the same file as the certificate, 87 | # both need to be explicitly specified. 88 | # ssl_key: /path/to/my.key 89 | 90 | # A passphrase for the private key. 91 | # ssl_key_pass_phrase: password 92 | 93 | # Whether or not to do peer certification validation. (default: false) 94 | # ssl_verify: true 95 | 96 | # The file containing a set of concatenated certification authority certifications 97 | # used to validate certs passed from the other end of the connection. 98 | # ssl_ca_cert: /path/to/ca.cert 99 | 100 | 101 | # Configure Mongoid specific options. (optional) 102 | options: 103 | # Includes the root model name in json serialization. (default: false) 104 | # include_root_in_json: false 105 | 106 | # Include the _type field in serialization. (default: false) 107 | # include_type_for_serialization: false 108 | 109 | # Preload all models in development, needed when models use 110 | # inheritance. (default: false) 111 | # preload_models: false 112 | 113 | # Raise an error when performing a #find and the document is not found. 114 | # (default: true) 115 | raise_not_found_error: false 116 | 117 | # Raise an error when defining a scope with the same name as an 118 | # existing method. (default: false) 119 | # scope_overwrite_exception: false 120 | 121 | # Use Active Support's time zone in conversions. (default: true) 122 | # use_activesupport_time_zone: true 123 | 124 | # Ensure all times are UTC in the app side. (default: false) 125 | # use_utc: false 126 | 127 | test: 128 | clients: 129 | default: 130 | database: planning_poker_rails_test 131 | hosts: 132 | - localhost:27017 133 | options: 134 | read: primary 135 | # In the test environment we lower the retries and retry interval to 136 | # low amounts for fast failures. 137 | max_retries: 1 138 | retry_interval: 0 139 | options: 140 | raise_not_found_error: false 141 | 142 | production: 143 | clients: 144 | default: 145 | uri: <%= ENV['MONGOHQ_URL'] %> 146 | options: 147 | max_retries: 1 148 | retry_interval: 0 149 | options: 150 | raise_not_found_error: false 151 | 152 | staging: 153 | clients: 154 | default: 155 | database: planning_poker_rails_staging 156 | hosts: 157 | - localhost:27017 158 | options: 159 | options: 160 | raise_not_found_error: false 161 | 162 | -------------------------------------------------------------------------------- /spec/helpers/dashboard_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DashboardHelper do 4 | let(:story) { OpenStruct.new } 5 | let(:vote) { OpenStruct.new } 6 | 7 | context '#icon' do 8 | [ 9 | ['feature', 'fa fa-star orange'], 10 | ['bug', 'fa fa-bug red'], 11 | ['chore', 'fa fa-cog'], 12 | ['release', 'fa fa-flag-checkered green'], 13 | ['sometype', 'sometype'] 14 | ].each do |state| 15 | it "should return #{state[0]} icon" do 16 | story.story_type = state[0] 17 | helper.icon(story).should eq state[1] 18 | end 19 | end 20 | end 21 | 22 | context '#bar_icons' do 23 | it 'should return 10 bar icons' do 24 | helper.bar_icons(10).should eq('' * 10) 25 | end 26 | end 27 | 28 | context '#show_estimation_icons?' do 29 | it 'should return true' do 30 | story.estimate = 5 31 | helper.show_estimation_icons?(story).should be_true 32 | end 33 | 34 | it 'should return false' do 35 | story.estimate = 10 36 | helper.show_estimation_icons?(story).should be_false 37 | end 38 | end 39 | 40 | context '#small_strong_text' do 41 | it 'should return small strong text' do 42 | helper.small_strong_text('something').should eq 'something' 43 | end 44 | end 45 | 46 | context '#estimated?' do 47 | it 'should return true' do 48 | story.estimate = 5 49 | helper.estimated?(story).should be_true 50 | end 51 | 52 | it 'should return false' do 53 | story.estimate = 0 54 | helper.estimated?(story).should be_false 55 | end 56 | end 57 | 58 | context '#estimateable' do 59 | it 'should return true' do 60 | story.estimate = -1 61 | helper.estimateable?(story).should be_true 62 | end 63 | 64 | it 'should return false' do 65 | story.estimate = 5 66 | helper.estimateable?(story).should be_false 67 | end 68 | end 69 | 70 | context '#state' do 71 | [ 72 | ['accepted', 'success'], 73 | ['delivered', 'warning'], 74 | ['started', 'info'], 75 | ['rejected', 'danger'], 76 | ['somestate', 'default'] 77 | ].each do |state| 78 | it 'should return accepter state' do 79 | story.current_state = state[0] 80 | helper.state(story).should eq "label label-#{state[1]} #{story.current_state}" 81 | end 82 | end 83 | end 84 | 85 | context '#card_class' do 86 | it 'should return card blank' do 87 | helper.card_class(nil, true).should eq 'card card-blank' 88 | end 89 | 90 | it 'should return card hidden' do 91 | helper.card_class(false, true).should eq 'card card-hidden' 92 | end 93 | 94 | it 'should return card reveal' do 95 | helper.card_class(true, true).should eq 'card card-revealed' 96 | end 97 | 98 | it 'should return card' do 99 | helper.card_class(true, false).should eq 'card' 100 | end 101 | end 102 | 103 | context '#method_missing' do 104 | ['accepted', 'delivered', 'rejected'].each do |state| 105 | it 'should return true' do 106 | story.current_state = state 107 | helper.send("state_#{state}?", story).should be_true 108 | end 109 | 110 | it 'should return false' do 111 | story.current_state = 'somestate' 112 | helper.send("state_#{state}?", story).should be_false 113 | end 114 | end 115 | 116 | ['feature', 'bug', 'chore'].each do |type| 117 | it 'should return true' do 118 | story.story_type = type 119 | helper.send("type_#{type}?", story).should be_true 120 | end 121 | 122 | it 'should return false' do 123 | story.story_type = 'sometype' 124 | helper.send("type_#{type}?", story).should be_false 125 | end 126 | end 127 | end 128 | 129 | context '#estimate_not_applicable?' do 130 | ['chore', 'bug'].each do |type| 131 | it 'should return true' do 132 | story.story_type = type 133 | helper.estimate_not_applicable?(story).should be_true 134 | end 135 | end 136 | 137 | it 'should return false' do 138 | story_type = 'sometype' 139 | helper.estimate_not_applicable?(story).should be_false 140 | end 141 | end 142 | 143 | context '#estimation_class' do 144 | it 'should return unestimateable' do 145 | helper.stubs(:type_chore?).returns(true) 146 | helper.estimation_class(story).should eq 'unestimateable' 147 | end 148 | 149 | it 'should return estimated' do 150 | helper.stubs(:type_chore?).returns(false) 151 | story.estimate = 5 152 | helper.estimation_class(story).should eq 'estimated' 153 | end 154 | 155 | it 'should return unestimated' do 156 | helper.stubs(:type_chore?).returns(false) 157 | story.estimate = -1 158 | helper.estimation_class(story).should eq 'unestimated' 159 | end 160 | end 161 | 162 | context '#nickname' do 163 | it 'should return first' do 164 | helper.nickname('first.last@domain.com').should eq 'first' 165 | end 166 | 167 | it 'should return name' do 168 | helper.nickname('name@domain.com').should eq 'name' 169 | end 170 | 171 | it 'should return nickname' do 172 | helper.nickname('nickname').should eq 'nickname' 173 | end 174 | end 175 | 176 | context '#vote' do 177 | before { helper.stubs(:current_user).returns(username: 'username') } 178 | 179 | it 'should return vote' do 180 | vote.user = 'username' 181 | vote.vote = 5 182 | helper.voting(vote).should eq 5 183 | end 184 | 185 | it 'should return false' do 186 | vote.user = 'otheruser' 187 | helper.voting(vote).should be_false 188 | end 189 | end 190 | 191 | context '#current_user_has_voted?' do 192 | before do 193 | helper.stubs(:current_user).returns(username: 'username') 194 | Vote.stubs(:find_by).returns(vote) 195 | end 196 | 197 | it 'should call find_by on Vote' do 198 | story.id = 123 199 | Vote.expects(:find_by).with(story_id: 123, user: 'username') 200 | helper.current_user_has_voted?(story) 201 | end 202 | 203 | it 'should return true' do 204 | helper.current_user_has_voted?(story).should be_true 205 | end 206 | 207 | it 'should return false' do 208 | Vote.stubs(:find_by).returns(nil) 209 | helper.current_user_has_voted?(story).should be_false 210 | end 211 | end 212 | 213 | context '#estimation' do 214 | before do 215 | helper.stubs(:estimated?).returns(true) 216 | story.estimate = 5 217 | end 218 | 219 | it 'should call bar icons' do 220 | helper.stubs(:show_estimation_icons?).returns(true) 221 | helper.expects(:bar_icons).with(story.estimate) 222 | helper.estimation(story) 223 | end 224 | 225 | it 'should call small strong text' do 226 | helper.stubs(:show_estimation_icons?).returns(false) 227 | helper.expects(:small_strong_text).with(story.estimate) 228 | helper.estimation(story) 229 | end 230 | end 231 | 232 | context '#unestimateable' do 233 | it 'should return true' do 234 | helper.stubs(:estimate_not_applicable?).returns(true) 235 | helper.stubs(:estimateable?).returns(false) 236 | helper.unestimateable?(story).should be_true 237 | end 238 | 239 | it 'should return false' do 240 | helper.stubs(:estimate_not_applicable?).returns(false) 241 | helper.stubs(:estimateable?).returns(true) 242 | helper.unestimateable?(story).should be_false 243 | end 244 | end 245 | end -------------------------------------------------------------------------------- /config/newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java, 3 | # .NET, PHP, Python and Node applications with deep visibility and low 4 | # overhead. For more information, visit www.newrelic.com. 5 | # 6 | # Generated October 04, 2014 7 | # 8 | # This configuration file is custom generated for app30333834@heroku.com 9 | 10 | 11 | # Here are the settings that are common to all environments 12 | common: &default_settings 13 | # ============================== LICENSE KEY =============================== 14 | 15 | # You must specify the license key associated with your New Relic 16 | # account. This key binds your Agent's data to your account in the 17 | # New Relic service. 18 | license_key: ENV['NEW_RELIC_LICENSE_KEY'] 19 | 20 | # Agent Enabled (Ruby/Rails Only) 21 | # Use this setting to force the agent to run or not run. 22 | # Default is 'auto' which means the agent will install and run only 23 | # if a valid dispatcher such as Mongrel is running. This prevents 24 | # it from running with Rake or the console. Set to false to 25 | # completely turn the agent off regardless of the other settings. 26 | # Valid values are true, false and auto. 27 | # 28 | # agent_enabled: auto 29 | 30 | # Application Name Set this to be the name of your application as 31 | # you'd like it show up in New Relic. The service will then auto-map 32 | # instances of your application into an "application" on your 33 | # dashboard page. If you want to map this instance into multiple 34 | # apps, like "AJAX Requests" and "All UI" then specify a semicolon 35 | # separated list of up to three distinct names, or a yaml list. 36 | # Defaults to the capitalized RAILS_ENV or RACK_ENV (i.e., 37 | # Production, Staging, etc) 38 | # 39 | # Example: 40 | # 41 | # app_name: 42 | # - Ajax Service 43 | # - All Services 44 | # 45 | # Caution: If you change this name, a new application will appear in the New 46 | # Relic user interface with the new name, and data will stop reporting to the 47 | # app with the old name. 48 | # 49 | # See https://newrelic.com/docs/site/renaming-applications for more details 50 | # on renaming your New Relic applications. 51 | # 52 | app_name: Scrum Poker 53 | 54 | # When "true", the agent collects performance data about your 55 | # application and reports this data to the New Relic service at 56 | # newrelic.com. This global switch is normally overridden for each 57 | # environment below. (formerly called 'enabled') 58 | monitor_mode: true 59 | 60 | # Developer mode should be off in every environment but 61 | # development as it has very high overhead in memory. 62 | developer_mode: false 63 | 64 | # The newrelic agent generates its own log file to keep its logging 65 | # information separate from that of your application. Specify its 66 | # log level here. 67 | log_level: info 68 | 69 | # Optionally set the path to the log file This is expanded from the 70 | # root directory (may be relative or absolute, e.g. 'log/' or 71 | # '/var/log/') The agent will attempt to create this directory if it 72 | # does not exist. 73 | # log_file_path: 'log' 74 | 75 | # Optionally set the name of the log file, defaults to 'newrelic_agent.log' 76 | # log_file_name: 'newrelic_agent.log' 77 | 78 | # The newrelic agent communicates with the service via https by default. This 79 | # prevents eavesdropping on the performance metrics transmitted by the agent. 80 | # The encryption required by SSL introduces a nominal amount of CPU overhead, 81 | # which is performed asynchronously in a background thread. If you'd prefer 82 | # to send your metrics over http uncomment the following line. 83 | # ssl: false 84 | 85 | #============================== Browser Monitoring =============================== 86 | # New Relic Real User Monitoring gives you insight into the performance real users are 87 | # experiencing with your website. This is accomplished by measuring the time it takes for 88 | # your users' browsers to download and render your web pages by injecting a small amount 89 | # of JavaScript code into the header and footer of each page. 90 | browser_monitoring: 91 | # By default the agent automatically injects the monitoring JavaScript 92 | # into web pages. Set this attribute to false to turn off this behavior. 93 | auto_instrument: true 94 | 95 | # Proxy settings for connecting to the New Relic server. 96 | # 97 | # If a proxy is used, the host setting is required. Other settings 98 | # are optional. Default port is 8080. 99 | # 100 | # proxy_host: hostname 101 | # proxy_port: 8080 102 | # proxy_user: 103 | # proxy_pass: 104 | 105 | # The agent can optionally log all data it sends to New Relic servers to a 106 | # separate log file for human inspection and auditing purposes. To enable this 107 | # feature, change 'enabled' below to true. 108 | # See: https://newrelic.com/docs/ruby/audit-log 109 | audit_log: 110 | enabled: false 111 | 112 | # Tells transaction tracer and error collector (when enabled) 113 | # whether or not to capture HTTP params. When true, frameworks can 114 | # exclude HTTP parameters from being captured. 115 | # Rails: the RoR filter_parameter_logging excludes parameters 116 | # Java: create a config setting called "ignored_params" and set it to 117 | # a comma separated list of HTTP parameter names. 118 | # ex: ignored_params: credit_card, ssn, password 119 | capture_params: false 120 | 121 | # Transaction tracer captures deep information about slow 122 | # transactions and sends this to the New Relic service once a 123 | # minute. Included in the transaction is the exact call sequence of 124 | # the transactions including any SQL statements issued. 125 | transaction_tracer: 126 | 127 | # Transaction tracer is enabled by default. Set this to false to 128 | # turn it off. This feature is only available at the Professional 129 | # and above product levels. 130 | enabled: true 131 | 132 | # Threshold in seconds for when to collect a transaction 133 | # trace. When the response time of a controller action exceeds 134 | # this threshold, a transaction trace will be recorded and sent to 135 | # New Relic. Valid values are any float value, or (default) "apdex_f", 136 | # which will use the threshold for an dissatisfying Apdex 137 | # controller action - four times the Apdex T value. 138 | transaction_threshold: apdex_f 139 | 140 | # When transaction tracer is on, SQL statements can optionally be 141 | # recorded. The recorder has three modes, "off" which sends no 142 | # SQL, "raw" which sends the SQL statement in its original form, 143 | # and "obfuscated", which strips out numeric and string literals. 144 | record_sql: obfuscated 145 | 146 | # Threshold in seconds for when to collect stack trace for a SQL 147 | # call. In other words, when SQL statements exceed this threshold, 148 | # then capture and send to New Relic the current stack trace. This is 149 | # helpful for pinpointing where long SQL calls originate from. 150 | stack_trace_threshold: 0.500 151 | 152 | # Determines whether the agent will capture query plans for slow 153 | # SQL queries. Only supported in mysql and postgres. Should be 154 | # set to false when using other adapters. 155 | # explain_enabled: true 156 | 157 | # Threshold for query execution time below which query plans will 158 | # not be captured. Relevant only when `explain_enabled` is true. 159 | # explain_threshold: 0.5 160 | 161 | # Error collector captures information about uncaught exceptions and 162 | # sends them to New Relic for viewing 163 | error_collector: 164 | 165 | # Error collector is enabled by default. Set this to false to turn 166 | # it off. This feature is only available at the Professional and above 167 | # product levels. 168 | enabled: true 169 | 170 | # To stop specific errors from reporting to New Relic, set this property 171 | # to comma-separated values. Default is to ignore routing errors, 172 | # which are how 404's get triggered. 173 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound" 174 | 175 | # If you're interested in capturing memcache keys as though they 176 | # were SQL uncomment this flag. Note that this does increase 177 | # overhead slightly on every memcached call, and can have security 178 | # implications if your memcached keys are sensitive 179 | # capture_memcache_keys: true 180 | 181 | # Application Environments 182 | # ------------------------------------------ 183 | # Environment-specific settings are in this section. 184 | # For Rails applications, RAILS_ENV is used to determine the environment. 185 | # For Java applications, pass -Dnewrelic.environment to set 186 | # the environment. 187 | 188 | # NOTE if your application has other named environments, you should 189 | # provide newrelic configuration settings for these environments here. 190 | 191 | development: 192 | <<: *default_settings 193 | # Turn on communication to New Relic service in development mode 194 | monitor_mode: true 195 | app_name: My Application (Development) 196 | 197 | # Rails Only - when running in Developer Mode, the New Relic Agent will 198 | # present performance information on the last 100 transactions you have 199 | # executed since starting the mongrel. 200 | # NOTE: There is substantial overhead when running in developer mode. 201 | # Do not use for production or load testing. 202 | developer_mode: true 203 | 204 | test: 205 | <<: *default_settings 206 | # It almost never makes sense to turn on the agent when running 207 | # unit, functional or integration tests or the like. 208 | monitor_mode: false 209 | 210 | # Turn on the agent in production for 24x7 monitoring. NewRelic 211 | # testing shows an average performance impact of < 5 ms per 212 | # transaction, you can leave this on all the time without 213 | # incurring any user-visible performance degradation. 214 | production: 215 | <<: *default_settings 216 | monitor_mode: true 217 | 218 | # Many applications have a staging environment which behaves 219 | # identically to production. Support for that environment is provided 220 | # here. By default, the staging environment has the agent turned on. 221 | staging: 222 | <<: *default_settings 223 | monitor_mode: true 224 | app_name: My Application (Staging) 225 | 226 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/mongoid/mongoid.git 3 | revision: 63ece9973709a837a1817824d202b367db3e3688 4 | specs: 5 | mongoid (5.0.0.beta) 6 | activemodel (~> 4.0) 7 | mongo (= 2.1.0.beta) 8 | origin (~> 2.1) 9 | tzinfo (>= 0.3.37) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | actionmailer (4.2.5.1) 15 | actionpack (= 4.2.5.1) 16 | actionview (= 4.2.5.1) 17 | activejob (= 4.2.5.1) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 1.0, >= 1.0.5) 20 | actionpack (4.2.5.1) 21 | actionview (= 4.2.5.1) 22 | activesupport (= 4.2.5.1) 23 | rack (~> 1.6) 24 | rack-test (~> 0.6.2) 25 | rails-dom-testing (~> 1.0, >= 1.0.5) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (4.2.5.1) 28 | activesupport (= 4.2.5.1) 29 | builder (~> 3.1) 30 | erubis (~> 2.7.0) 31 | rails-dom-testing (~> 1.0, >= 1.0.5) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 33 | activejob (4.2.5.1) 34 | activesupport (= 4.2.5.1) 35 | globalid (>= 0.3.0) 36 | activemodel (4.2.5.1) 37 | activesupport (= 4.2.5.1) 38 | builder (~> 3.1) 39 | activerecord (4.2.5.1) 40 | activemodel (= 4.2.5.1) 41 | activesupport (= 4.2.5.1) 42 | arel (~> 6.0) 43 | activesupport (4.2.5.1) 44 | i18n (~> 0.7) 45 | json (~> 1.7, >= 1.7.7) 46 | minitest (~> 5.1) 47 | thread_safe (~> 0.3, >= 0.3.4) 48 | tzinfo (~> 1.1) 49 | addressable (2.5.1) 50 | public_suffix (~> 2.0, >= 2.0.2) 51 | airbrussh (1.3.0) 52 | sshkit (>= 1.6.1, != 1.7.0) 53 | arel (6.0.4) 54 | autoprefixer-rails (7.1.2.3) 55 | execjs 56 | backtop (1.0.1) 57 | coffee-rails (> 3.0.0) 58 | font-awesome-rails 59 | rails (> 3.2.0) 60 | sass-rails (> 3.0.0) 61 | better_errors (2.1.1) 62 | coderay (>= 1.0.0) 63 | erubis (>= 2.6.6) 64 | rack (>= 0.9.0) 65 | binding_of_caller (0.7.2) 66 | debug_inspector (>= 0.0.1) 67 | bootstrap-sass (3.3.7) 68 | autoprefixer-rails (>= 5.2.1) 69 | sass (>= 3.3.4) 70 | bson (3.2.7) 71 | bson_ext (1.5.1) 72 | builder (3.2.3) 73 | capistrano (3.8.2) 74 | airbrussh (>= 1.0.0) 75 | i18n 76 | rake (>= 10.0.0) 77 | sshkit (>= 1.9.0) 78 | capistrano-bundler (1.2.0) 79 | capistrano (~> 3.1) 80 | sshkit (~> 1.2) 81 | capistrano-rails (1.3.0) 82 | capistrano (~> 3.1) 83 | capistrano-bundler (~> 1.1) 84 | capistrano-rvm (0.1.2) 85 | capistrano (~> 3.0) 86 | sshkit (~> 1.2) 87 | capybara (2.14.4) 88 | addressable 89 | mime-types (>= 1.16) 90 | nokogiri (>= 1.3.3) 91 | rack (>= 1.0.0) 92 | rack-test (>= 0.5.4) 93 | xpath (~> 2.0) 94 | childprocess (0.7.1) 95 | ffi (~> 1.0, >= 1.0.11) 96 | codeclimate-test-reporter (1.0.8) 97 | simplecov (<= 0.13) 98 | coderay (1.1.1) 99 | coffee-rails (4.1.1) 100 | coffee-script (>= 2.2.0) 101 | railties (>= 4.0.0, < 5.1.x) 102 | coffee-script (2.4.1) 103 | coffee-script-source 104 | execjs 105 | coffee-script-source (1.12.2) 106 | concurrent-ruby (1.0.5) 107 | cookiejar (0.3.3) 108 | crack (0.4.3) 109 | safe_yaml (~> 1.0.0) 110 | daemons (1.2.4) 111 | database_cleaner (1.6.1) 112 | debug_inspector (0.0.3) 113 | diff-lcs (1.3) 114 | docile (1.1.5) 115 | domain_name (0.5.20170404) 116 | unf (>= 0.0.5, < 1.0.0) 117 | em-http-request (1.1.5) 118 | addressable (>= 2.3.4) 119 | cookiejar (!= 0.3.1) 120 | em-socksify (>= 0.3) 121 | eventmachine (>= 1.0.3) 122 | http_parser.rb (>= 0.6.0) 123 | em-socksify (0.3.1) 124 | eventmachine (>= 1.0.0.beta.4) 125 | erubis (2.7.0) 126 | eventmachine (1.2.3) 127 | execjs (2.7.0) 128 | factory_girl (4.8.0) 129 | activesupport (>= 3.0.0) 130 | factory_girl_rails (4.8.0) 131 | factory_girl (~> 4.8.0) 132 | railties (>= 3.0.0) 133 | faye (1.2.4) 134 | cookiejar (>= 0.3.0) 135 | em-http-request (>= 0.3.0) 136 | eventmachine (>= 0.12.0) 137 | faye-websocket (>= 0.9.1) 138 | multi_json (>= 1.0.0) 139 | rack (>= 1.0.0) 140 | websocket-driver (>= 0.5.1) 141 | faye-websocket (0.10.7) 142 | eventmachine (>= 0.12.0) 143 | websocket-driver (>= 0.5.1) 144 | ffi (1.9.18) 145 | font-awesome-rails (4.7.0.2) 146 | railties (>= 3.2, < 5.2) 147 | forgery (0.6.0) 148 | globalid (0.4.0) 149 | activesupport (>= 4.2.0) 150 | http-cookie (1.0.3) 151 | domain_name (~> 0.5) 152 | http_parser.rb (0.6.0) 153 | i18n (0.8.6) 154 | jbuilder (2.7.0) 155 | activesupport (>= 4.2.0) 156 | multi_json (>= 1.2) 157 | jquery-rails (4.3.1) 158 | rails-dom-testing (>= 1, < 3) 159 | railties (>= 4.2.0) 160 | thor (>= 0.14, < 2.0) 161 | json (1.8.6) 162 | libv8 (3.16.14.19) 163 | loofah (2.0.3) 164 | nokogiri (>= 1.5.9) 165 | mail (2.6.6) 166 | mime-types (>= 1.16, < 4) 167 | metaclass (0.0.4) 168 | method_source (0.8.2) 169 | mime-types (3.1) 170 | mime-types-data (~> 3.2015) 171 | mime-types-data (3.2016.0521) 172 | mini_portile2 (2.2.0) 173 | minitest (5.10.3) 174 | mocha (1.2.1) 175 | metaclass (~> 0.0.1) 176 | mongo (2.1.0.beta) 177 | bson (~> 3.0) 178 | multi_json (1.12.1) 179 | net-scp (1.2.1) 180 | net-ssh (>= 2.6.5) 181 | net-ssh (4.1.0) 182 | netrc (0.11.0) 183 | newrelic_rpm (4.3.0.335) 184 | nokogiri (1.8.0) 185 | mini_portile2 (~> 2.2.0) 186 | nokogiri-happymapper (0.5.9) 187 | nokogiri (~> 1.5) 188 | origin (2.3.1) 189 | pivotal-tracker (0.5.13) 190 | builder 191 | crack 192 | nokogiri (>= 1.5.5) 193 | nokogiri-happymapper (>= 0.5.4) 194 | rest-client (>= 1.8.0) 195 | public_suffix (2.0.5) 196 | rack (1.6.8) 197 | rack-test (0.6.3) 198 | rack (>= 1.0) 199 | rack_session_access (0.1.1) 200 | builder (>= 2.0.0) 201 | rack (>= 1.0.0) 202 | rails (4.2.5.1) 203 | actionmailer (= 4.2.5.1) 204 | actionpack (= 4.2.5.1) 205 | actionview (= 4.2.5.1) 206 | activejob (= 4.2.5.1) 207 | activemodel (= 4.2.5.1) 208 | activerecord (= 4.2.5.1) 209 | activesupport (= 4.2.5.1) 210 | bundler (>= 1.3.0, < 2.0) 211 | railties (= 4.2.5.1) 212 | sprockets-rails 213 | rails-deprecated_sanitizer (1.0.3) 214 | activesupport (>= 4.2.0.alpha) 215 | rails-dom-testing (1.0.8) 216 | activesupport (>= 4.2.0.beta, < 5.0) 217 | nokogiri (~> 1.6) 218 | rails-deprecated_sanitizer (>= 1.0.1) 219 | rails-html-sanitizer (1.0.3) 220 | loofah (~> 2.0) 221 | rails_12factor (0.0.3) 222 | rails_serve_static_assets 223 | rails_stdout_logging 224 | rails_serve_static_assets (0.0.5) 225 | rails_stdout_logging (0.0.5) 226 | railties (4.2.5.1) 227 | actionpack (= 4.2.5.1) 228 | activesupport (= 4.2.5.1) 229 | rake (>= 0.8.7) 230 | thor (>= 0.18.1, < 2.0) 231 | rake (12.0.0) 232 | rb-fsevent (0.10.2) 233 | rb-inotify (0.9.10) 234 | ffi (>= 0.5.0, < 2) 235 | rdoc (4.3.0) 236 | ref (2.0.0) 237 | responders (2.4.0) 238 | actionpack (>= 4.2.0, < 5.3) 239 | railties (>= 4.2.0, < 5.3) 240 | rest-client (2.0.2) 241 | http-cookie (>= 1.0.2, < 2.0) 242 | mime-types (>= 1.16, < 4.0) 243 | netrc (~> 0.8) 244 | rspec-core (3.6.0) 245 | rspec-support (~> 3.6.0) 246 | rspec-expectations (3.6.0) 247 | diff-lcs (>= 1.2.0, < 2.0) 248 | rspec-support (~> 3.6.0) 249 | rspec-mocks (3.6.0) 250 | diff-lcs (>= 1.2.0, < 2.0) 251 | rspec-support (~> 3.6.0) 252 | rspec-rails (3.6.0) 253 | actionpack (>= 3.0) 254 | activesupport (>= 3.0) 255 | railties (>= 3.0) 256 | rspec-core (~> 3.6.0) 257 | rspec-expectations (~> 3.6.0) 258 | rspec-mocks (~> 3.6.0) 259 | rspec-support (~> 3.6.0) 260 | rspec-support (3.6.0) 261 | rubyzip (1.2.1) 262 | safe_yaml (1.0.4) 263 | sass (3.5.1) 264 | sass-listen (~> 4.0.0) 265 | sass-listen (4.0.0) 266 | rb-fsevent (~> 0.9, >= 0.9.4) 267 | rb-inotify (~> 0.9, >= 0.9.7) 268 | sass-rails (5.0.6) 269 | railties (>= 4.0.0, < 6) 270 | sass (~> 3.1) 271 | sprockets (>= 2.8, < 4.0) 272 | sprockets-rails (>= 2.0, < 4.0) 273 | tilt (>= 1.1, < 3) 274 | sdoc (0.4.2) 275 | json (~> 1.7, >= 1.7.7) 276 | rdoc (~> 4.0) 277 | selenium-webdriver (3.4.4) 278 | childprocess (~> 0.5) 279 | rubyzip (~> 1.0) 280 | simplecov (0.13.0) 281 | docile (~> 1.1.0) 282 | json (>= 1.8, < 3) 283 | simplecov-html (~> 0.10.0) 284 | simplecov-html (0.10.1) 285 | spring (2.0.2) 286 | activesupport (>= 4.2) 287 | sprockets (3.7.1) 288 | concurrent-ruby (~> 1.0) 289 | rack (> 1, < 3) 290 | sprockets-rails (3.2.0) 291 | actionpack (>= 4.0) 292 | activesupport (>= 4.0) 293 | sprockets (>= 3.0.0) 294 | sqlite3 (1.3.13) 295 | sshkit (1.14.0) 296 | net-scp (>= 1.1.2) 297 | net-ssh (>= 2.8.0) 298 | therubyracer (0.12.3) 299 | libv8 (~> 3.16.14.15) 300 | ref 301 | thin (1.7.2) 302 | daemons (~> 1.0, >= 1.0.9) 303 | eventmachine (~> 1.0, >= 1.0.4) 304 | rack (>= 1, < 3) 305 | thor (0.19.4) 306 | thread_safe (0.3.6) 307 | tilt (2.0.8) 308 | turbolinks (5.0.1) 309 | turbolinks-source (~> 5) 310 | turbolinks-source (5.0.3) 311 | tzinfo (1.2.3) 312 | thread_safe (~> 0.1) 313 | uglifier (3.2.0) 314 | execjs (>= 0.3.0, < 3) 315 | unf (0.1.4) 316 | unf_ext 317 | unf_ext (0.0.7.4) 318 | websocket-driver (0.6.5) 319 | websocket-extensions (>= 0.1.0) 320 | websocket-extensions (0.1.2) 321 | xpath (2.1.0) 322 | nokogiri (~> 1.3) 323 | yui-compressor (0.12.0) 324 | zeus (0.15.14) 325 | method_source (>= 0.6.7) 326 | 327 | PLATFORMS 328 | ruby 329 | 330 | DEPENDENCIES 331 | backtop 332 | better_errors 333 | binding_of_caller 334 | bootstrap-sass 335 | bson_ext 336 | capistrano (~> 3.0) 337 | capistrano-bundler (~> 1.1) 338 | capistrano-rails (~> 1.1) 339 | capistrano-rvm (~> 0.1) 340 | capybara 341 | codeclimate-test-reporter 342 | coffee-rails (~> 4.1.0) 343 | database_cleaner 344 | factory_girl_rails 345 | faye 346 | font-awesome-rails 347 | forgery 348 | jbuilder (~> 2.0) 349 | jquery-rails 350 | mocha 351 | mongoid! 352 | newrelic_rpm 353 | pivotal-tracker 354 | rack_session_access 355 | rails (= 4.2.5.1) 356 | rails_12factor 357 | responders (~> 2.0) 358 | rspec-rails 359 | sass-rails (~> 5.0) 360 | sdoc (~> 0.4.0) 361 | selenium-webdriver 362 | spring 363 | sqlite3 364 | therubyracer 365 | thin 366 | turbolinks 367 | uglifier (>= 1.0.3) 368 | yui-compressor 369 | zeus 370 | 371 | BUNDLED WITH 372 | 1.13.1 373 | -------------------------------------------------------------------------------- /app/assets/stylesheets/authenticated.css.scss: -------------------------------------------------------------------------------- 1 | #authenticated { 2 | background-color: #484F57; 3 | color: #333; 4 | 5 | #header { 6 | background: #3E934C; 7 | padding: 5px 40px 0; 8 | 9 | h1 { 10 | font-family: 'Rokkitt', serif; 11 | margin: 5px 0 10px; 12 | padding: 0; 13 | font-size: 18px; 14 | color: #FFF; 15 | 16 | span { 17 | &.logo { 18 | font-size: 14px; 19 | } 20 | } 21 | } 22 | 23 | .header-links { 24 | margin: 5px 0; 25 | font-size: 12px; 26 | text-transform: uppercase; 27 | } 28 | 29 | .dropdown { 30 | width: 200px; 31 | margin: 0 auto; 32 | 33 | #menu-compact { 34 | background: #EEE; 35 | color: #777; 36 | display: none; 37 | font-size: 12px; 38 | font-weight: bold; 39 | width: 100%; 40 | padding-top: 10px; 41 | 42 | -webkit-border-bottom-right-radius: 0; 43 | -webkit-border-bottom-left-radius: 0; 44 | -moz-border-radius-bottomright: 0; 45 | -moz-border-radius-bottomleft: 0; 46 | border-bottom-right-radius: 0; 47 | border-bottom-left-radius: 0; 48 | } 49 | } 50 | } 51 | 52 | #content { 53 | margin: 0; 54 | padding: 10px; 55 | } 56 | 57 | #sidebar { 58 | padding-right: 0; 59 | } 60 | 61 | #menu { 62 | display: block; 63 | padding-left: 0; 64 | margin: 0; 65 | 66 | #more-projects { 67 | cursor: pointer; 68 | } 69 | 70 | > li { 71 | padding: 5px 10px; 72 | list-style: none; 73 | margin: 0 5px; 74 | float: left; 75 | font-weight: 600; 76 | font-size: 12px; 77 | text-shadow: 1px 1px 1px #51A335; 78 | 79 | &.active { 80 | text-shadow: none; 81 | 82 | a { color: #FFF; &:hover { text-decoration: none; }} 83 | 84 | .more-projects { 85 | .dropdown-toggle, .caret { 86 | color: #666; 87 | } 88 | } 89 | } 90 | 91 | a { color: rgba(255, 255, 255, .7); } 92 | 93 | .more-projects { 94 | display: block; 95 | 96 | .dropdown-toggle, .caret { 97 | color: #FFF; 98 | } 99 | 100 | &.open { 101 | .dropdown-toggle { 102 | -webkit-box-shadow: none; 103 | box-shadow: none; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | .project-dropdown li { 111 | &.active { 112 | a { background: #EEE; } 113 | } 114 | 115 | a { 116 | color: #666 !important; 117 | text-shadow: none; 118 | font-size: 11px; 119 | padding: 10px 20px; 120 | } 121 | } 122 | 123 | #sub-menu { 124 | background: #EEE; 125 | border-bottom: 1px solid #212121; 126 | padding: 10px; 127 | 128 | li { 129 | float: left; 130 | margin: 0 5px; 131 | list-style: none; 132 | color: #999; 133 | font-size: 11px; 134 | padding: 5px 10px; 135 | cursor: pointer; 136 | 137 | &.selected { 138 | background-color: #FFF; 139 | -webkit-border-radius: 4px; 140 | -moz-border-radius: 4px; 141 | -ms-border-radius: 4px; 142 | -o-border-radius: 4px; 143 | border-radius: 4px; 144 | color: #256188; 145 | } 146 | } 147 | } 148 | 149 | .stories { 150 | padding: 0; 151 | border: 1px solid #111; 152 | 153 | .story { 154 | font-size: 12px; 155 | background-color: #F3F3D1; 156 | border-bottom: 1px solid #DBDBB0; 157 | border-top: 1px solid #FFF; 158 | 159 | &:hover { 160 | background: #f0f0c6; 161 | } 162 | 163 | .story-overview { 164 | padding: 10px; 165 | } 166 | } 167 | 168 | .story-icons { 169 | width: 10%; 170 | min-width: 65px; 171 | max-width: 65px; 172 | 173 | a:hover { 174 | text-decoration: none; 175 | } 176 | } 177 | 178 | .story-description { 179 | width: 90%; 180 | position: relative; 181 | 182 | > .label { 183 | position: absolute; 184 | right: -25px; 185 | top: -3px; 186 | padding: 5px; 187 | text-transform: uppercase; 188 | font-size: .7em; 189 | } 190 | } 191 | 192 | .story-icon { 193 | width: 30%; 194 | color: #949693; 195 | } 196 | 197 | .story-unstarted, .story-unscheduled { 198 | background-color: #E4EFF7; 199 | border-bottom: 1px solid #DDD; 200 | 201 | &:hover { 202 | background: #D1E0ED; 203 | } 204 | } 205 | 206 | .story-rejected { 207 | background-color: #F3D1D1; 208 | border-bottom: 1px solid #DBB0B0; 209 | } 210 | 211 | .story-accepted { 212 | background: #daebcf; 213 | border-top: 1px solid #eefce2; 214 | border-bottom: 1px solid #c3d5b4; 215 | 216 | &:hover { 217 | background: #c6d9b7; 218 | } 219 | } 220 | } 221 | 222 | .icon-bar { 223 | display: block; 224 | width: 10px; 225 | height: 3px; 226 | background-color: #739DBC; 227 | margin-bottom: 1px; 228 | } 229 | 230 | .expand, .collapse { 231 | font-size: .75em; 232 | color: #AFAFAF; 233 | cursor: pointer; 234 | } 235 | 236 | .collapse { 237 | position: absolute; 238 | z-index: 10; 239 | top: 5px; 240 | left: 0; 241 | } 242 | 243 | .titles { 244 | background-color: #2C2D2F; 245 | border-top :1px solid #393A3C; 246 | color: #FFF; 247 | line-height: 27px; 248 | overflow: hidden; 249 | padding: 5px 10px; 250 | text-transform: uppercase; 251 | font-size: 12px; 252 | font-weight: bold; 253 | margin: 0; 254 | } 255 | 256 | .story-detail { 257 | background-color: #E9E7DD; 258 | background-image: -webkit-gradient(linear, 0 0, 100% 100%, color-stop(.25, rgba(255, 255, 255, 0.2)), color-stop(.25, rgba(0, 0, 0, 0)), color-stop(.5, rgba(0, 0, 0, 0)), color-stop(.5, rgba(255, 255, 255, 0.2)), color-stop(.75, rgba(255, 255, 255, 0.2)), color-stop(.75, rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0))); 259 | background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0) 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 0)); 260 | background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent); 261 | background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent); 262 | background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent); 263 | background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0) 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 0)); 264 | 265 | -webkit-background-size: 5px 5px; 266 | -moz-background-size: 5px 5px; 267 | background-size: 5px 5px; 268 | 269 | box-shadow: inset 0px 0px 8px #CCC; 270 | 271 | padding: 10px; 272 | position: relative; 273 | 274 | &.updated { 275 | background-color: #FFDE76; 276 | } 277 | 278 | textarea { 279 | font-size: 11px; 280 | } 281 | 282 | .project-name { 283 | margin-left: 15px; 284 | } 285 | 286 | .story-info { 287 | list-style-type: none; 288 | background-color: #F6F6F6; 289 | border: 1px solid #CCC; 290 | border-bottom: none; 291 | padding: 0; 292 | 293 | li { 294 | border-top: 1px solid #FFF; 295 | border-bottom: 1px solid #CCC; 296 | font-size: 11px; 297 | padding: 5px 10px; 298 | color: #505050; 299 | } 300 | } 301 | } 302 | 303 | .default-state { 304 | margin: 5% 0; 305 | 306 | .fa { 307 | font-size: 20em; 308 | color: #EEE; 309 | } 310 | 311 | h2 { 312 | color: #999; 313 | 314 | small { 315 | display: block; 316 | color: #CCC; 317 | margin: 5px 0; 318 | } 319 | } 320 | } 321 | 322 | .story-labels { text-transform: uppercase; } 323 | 324 | .widget { 325 | padding: 0; 326 | border: 1px solid #111; 327 | 328 | ul { 329 | padding: 0; 330 | margin: 0; 331 | 332 | li { 333 | overflow: hidden; 334 | font-size: 11px; 335 | color: #666; 336 | background-color: #FFF; 337 | border-bottom: 1px solid #DDD; 338 | list-style-type: none; 339 | padding: 10px; 340 | } 341 | } 342 | } 343 | 344 | .cards { 345 | padding: 10px 5px; 346 | background: #DFDCD0; 347 | -webkit-border-radius: 5px; 348 | -moz-border-radius: 5px; 349 | border-radius: 5px; 350 | 351 | ul { 352 | margin: 0; 353 | padding: 0 5px; 354 | 355 | li { 356 | list-style-type: none; 357 | display: inline-block; 358 | zoom: 1; 359 | *display: inline; 360 | margin: 5px; 361 | 362 | .card { 363 | background-color: #2F96B4; 364 | border: 5px solid #067596; 365 | min-width: 90px; 366 | min-height: 100px; 367 | display: inline-block; 368 | zoom: 1; 369 | *display: inline; 370 | color: #FFF; 371 | text-align: center; 372 | padding: 10px; 373 | vertical-align: top; 374 | -webkit-border-radius: 10px; 375 | -moz-border-radius: 10px; 376 | border-radius: 10px; 377 | 378 | &.card-blank { 379 | background-color: #E9E9E9; 380 | border: 5px solid #D3D3D3; 381 | color: #D3D3D3; 382 | } 383 | 384 | &.card-hidden { 385 | background: #5BC0DE; 386 | border-color: #46B8DA; 387 | } 388 | 389 | &.card-revealed { 390 | cursor: pointer; 391 | 392 | &.card-selected, &:hover { 393 | background: #5CB85C; 394 | border-color: #4CAE4C; 395 | } 396 | } 397 | 398 | .vote-value { 399 | font-size: 32px; 400 | font-weight: bold; 401 | padding: 5px 0px 0px 0px; 402 | margin: 0px; 403 | } 404 | 405 | .voter { 406 | font-size: 14px; 407 | } 408 | } 409 | } 410 | } 411 | } 412 | 413 | .btn-votes { 414 | margin: 10px 0 20px; 415 | } 416 | 417 | @media all and (max-width: 991px) { 418 | #header { 419 | h1 { 420 | text-align: center; 421 | font-size: 30px; 422 | } 423 | 424 | #header-links { 425 | margin-bottom: 10px; 426 | text-align: center; 427 | } 428 | 429 | #menu { 430 | display: none; 431 | } 432 | 433 | .dropdown { 434 | #menu-compact { 435 | display: block; 436 | } 437 | } 438 | } 439 | 440 | #sidebar { 441 | margin: 20px 0; 442 | padding: 0; 443 | } 444 | } 445 | } --------------------------------------------------------------------------------