diff options
author | Joachim Filip Ignacy Bartosik <jbartosik@gmail.com> | 2011-05-13 18:50:23 +0200 |
---|---|---|
committer | Joachim Filip Ignacy Bartosik <jbartosik@gmail.com> | 2011-05-24 18:55:39 +0200 |
commit | a0925ae845600d7e07a672b2a1a62d749154fabc (patch) | |
tree | e3516e75b60116a7f7fa4aa7ee5e77f5a38e0f9b | |
parent | Use fuubar (diff) | |
download | council-webapp-a0925ae845600d7e07a672b2a1a62d749154fabc.tar.gz council-webapp-a0925ae845600d7e07a672b2a1a62d749154fabc.tar.bz2 council-webapp-a0925ae845600d7e07a672b2a1a62d749154fabc.zip |
Agenda lifecycle (semi-automatically managed)
When starting application create new (open) agenda if there are 0
non-archival agendas. When archiving agenda create a new open agenda.
Agenda is valid only if it's archival agenda or if there are no other
non-archival agendas.
-rw-r--r-- | site/app/controllers/agendas_controller.rb | 3 | ||||
-rw-r--r-- | site/app/models/agenda.rb | 52 | ||||
-rw-r--r-- | site/app/views/agendas/index.dryml | 5 | ||||
-rw-r--r-- | site/app/views/agendas/show.dryml | 9 | ||||
-rw-r--r-- | site/config/hobo_routes.rb | 8 | ||||
-rw-r--r-- | site/config/initializers/agenda.rb | 12 | ||||
-rw-r--r-- | site/db/schema.rb | 6 | ||||
-rw-r--r-- | site/features/agendas.feature | 37 | ||||
-rw-r--r-- | site/features/step_definitions/agenda_steps.rb | 26 | ||||
-rw-r--r-- | site/features/step_definitions/within_steps.rb | 3 | ||||
-rw-r--r-- | site/features/support/paths.rb | 2 | ||||
-rw-r--r-- | site/spec/factories.rb | 2 | ||||
-rw-r--r-- | site/spec/models/agenda_spec.rb | 104 | ||||
-rw-r--r-- | site/spec/spec_helper.rb | 1 |
14 files changed, 241 insertions, 29 deletions
diff --git a/site/app/controllers/agendas_controller.rb b/site/app/controllers/agendas_controller.rb index 438fff7..bfc32a4 100644 --- a/site/app/controllers/agendas_controller.rb +++ b/site/app/controllers/agendas_controller.rb @@ -4,4 +4,7 @@ class AgendasController < ApplicationController auto_actions :all + def index + hobo_index Agenda.state_is(:old) + end end diff --git a/site/app/models/agenda.rb b/site/app/models/agenda.rb index a6fe828..def1643 100644 --- a/site/app/models/agenda.rb +++ b/site/app/models/agenda.rb @@ -7,6 +7,19 @@ class Agenda < ActiveRecord::Base timestamps end + lifecycle do + state :open, :default => true + state :submissions_closed, :meeting_ongoing, :old + + transition :close, {:open => :submissions_closed}, :available_to => '::Agenda.transitions_available(acting_user)' + transition :reopen, {:submissions_closed=> :open}, :available_to => '::Agenda.transitions_available(acting_user)' + transition :archive, {:submissions_closed => :old}, :available_to => '::Agenda.transitions_available(acting_user)' do + Agenda.new.save! + end + end + + validate :there_is_only_one_non_archival_agenda + # --- Permissions --- # def create_permitted? @@ -25,4 +38,43 @@ class Agenda < ActiveRecord::Base true end + before_create do |a| + a.meeting_time ||= Time.now + end + + def self.current + Agenda.state_is_not(:old).first + end + + def self.transitions_available(user) + return user if user.council_member? + return user if user.administrator? + false + end + + def possible_transitions + transitions = case state + when 'open' + ['close'] + when 'submissions_closed' + ['reopen', 'archive'] + else + [] + end + + transitions.collect do |transition| + ["#{transition.camelize} this agenda.", "agenda_#{transition}_path"] + end + end + + protected + def there_is_only_one_non_archival_agenda + return if(state.to_s == 'old') + if id.nil? + return if Agenda.state_is_not(:old).count == 0 + else + return if Agenda.state_is_not(:old).id_is_not(id).count == 0 + end + errors.add(:state, 'There can be only one non-archival agenda at time.') + end end diff --git a/site/app/views/agendas/index.dryml b/site/app/views/agendas/index.dryml new file mode 100644 index 0000000..9eff469 --- /dev/null +++ b/site/app/views/agendas/index.dryml @@ -0,0 +1,5 @@ +<index-page> + <before-collection:> + Current agenda: <view with="&Agenda.current" class="current-agenda"/> + </before-collection:> +</index-page> diff --git a/site/app/views/agendas/show.dryml b/site/app/views/agendas/show.dryml new file mode 100644 index 0000000..7df2b72 --- /dev/null +++ b/site/app/views/agendas/show.dryml @@ -0,0 +1,9 @@ +<show-page> + <append-content-body:> + <div class="transition" if="&Agenda.transitions_available(current_user)"> + <collection:possible_transitions> + <a href="&send(this.second)"><view:first/></a> + </collection> + </div> + </append-content-body:> +</show-page> diff --git a/site/config/hobo_routes.rb b/site/config/hobo_routes.rb index 7035490..33b292b 100644 --- a/site/config/hobo_routes.rb +++ b/site/config/hobo_routes.rb @@ -27,6 +27,14 @@ Council::Application.routes.draw do match 'forgot_password(.:format)' => 'users#forgot_password', :as => 'user_forgot_password' + # Lifecycle routes for controller "agendas" + put 'agendas/:id/close(.:format)' => 'agendas#do_close', :as => 'do_agenda_close' + get 'agendas/:id/close(.:format)' => 'agendas#close', :as => 'agenda_close' + put 'agendas/:id/reopen(.:format)' => 'agendas#do_reopen', :as => 'do_agenda_reopen' + get 'agendas/:id/reopen(.:format)' => 'agendas#reopen', :as => 'agenda_reopen' + put 'agendas/:id/archive(.:format)' => 'agendas#do_archive', :as => 'do_agenda_archive' + get 'agendas/:id/archive(.:format)' => 'agendas#archive', :as => 'agenda_archive' + # Resource routes for controller "agendas" get 'agendas(.:format)' => 'agendas#index', :as => 'agendas' get 'agendas/new(.:format)', :as => 'new_agenda' diff --git a/site/config/initializers/agenda.rb b/site/config/initializers/agenda.rb new file mode 100644 index 0000000..dccc29f --- /dev/null +++ b/site/config/initializers/agenda.rb @@ -0,0 +1,12 @@ +# If there are no active agendas create one +begin + return unless ['development', 'production'].include? Rails.env + return unless Agenda.state_is_not(:old).count > 0 + Agenda.create! +rescue + # Just ignore it. It will happen when: + # * Everything is fine, but database is missing (eg. rake db:schema:load) + # * It's safe to ignore this then + # * Something is seriously wrong (like broken db) + # * Users will notice this anyway +end diff --git a/site/db/schema.rb b/site/db/schema.rb index 812d821..b308e67 100644 --- a/site/db/schema.rb +++ b/site/db/schema.rb @@ -10,14 +10,18 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20110520150527) do +ActiveRecord::Schema.define(:version => 20110523175740) do create_table "agendas", :force => true do |t| t.datetime "meeting_time" t.datetime "created_at" t.datetime "updated_at" + t.string "state", :default => "open" + t.datetime "key_timestamp" end + add_index "agendas", ["state"], :name => "index_agendas_on_state" + create_table "users", :force => true do |t| t.string "salt", :limit => 40 t.string "remember_token" diff --git a/site/features/agendas.feature b/site/features/agendas.feature index 77267a5..582f904 100644 --- a/site/features/agendas.feature +++ b/site/features/agendas.feature @@ -3,13 +3,42 @@ Feature: Agendas I want to have agendas Scenario: View agendas listing as a guest + Given an agenda + Given an old agenda When I am on the homepage And I follow "Agendas" - Then I should not see "Agenda" in the content body - - Given an agenda - When I follow "Agendas" Then I should see "Agenda" in the agendas collection + And I should see "Agenda" as current agenda When I follow link to first agenda Then I should see current date as meeting time + + Scenario: Change current agenda state as a council member + Given an agenda + When I am logged in as a council member + And I follow "Agendas" + And I follow link to current agenda + Then I should see "open" as agenda state + And I should see "Close this agenda" as transition + + When I close current agenda + Then I should see "submissions_closed" as agenda state + And I should see "Reopen this agenda" as transition + And I should see "Archive this agenda" as transition + + When I reopen current agenda + Then I should see "open" as agenda state + + When I close current agenda + When I archive current agenda + Then I should see "old" as agenda state + + Scenario: Change current agenda state as a council member + Given an closed agenda + When I am logged in as a council member + And I am on the current agenda page + And I archive current agenda + + When I follow "Agendas" + And I follow link to current agenda + Then I should see "open" as agenda state diff --git a/site/features/step_definitions/agenda_steps.rb b/site/features/step_definitions/agenda_steps.rb index 95c7406..8613fb1 100644 --- a/site/features/step_definitions/agenda_steps.rb +++ b/site/features/step_definitions/agenda_steps.rb @@ -1,5 +1,8 @@ -Given /^an agenda$/ do - Agenda.new(:meeting_time => Time.now).save! +Given /^an ?(\w*) agenda$/ do |state| + a = Agenda.new + state = 'submissions_closed' if state == 'closed' + a.state = state unless state.empty? + a.save! if a.valid? end Then /^I should see current date as meeting time$/ do @@ -10,3 +13,22 @@ When /^I follow link to first agenda$/ do link_text = page.find(:xpath, "//a[contains(@class, 'agenda-link')]").text When "I follow \"#{link_text}\"" end + +When /^I follow link to current agenda$/ do + a = Agenda.current + When "I follow \"Agenda #{a.id}\"" +end + +When /^I am logged in as a council member$/ do + Given 'example user' + user = User.last + user.council_member = true + user.save + When 'I am on the login page' + When 'I login as example user' +end + +When /^I (\w+) current agenda$/ do |action| + When "I follow \"#{action.camelize} this agenda\"" + When "I press \"#{action.camelize}\"" +end diff --git a/site/features/step_definitions/within_steps.rb b/site/features/step_definitions/within_steps.rb index 3f42f37..80bb8f0 100644 --- a/site/features/step_definitions/within_steps.rb +++ b/site/features/step_definitions/within_steps.rb @@ -1,4 +1,7 @@ { + 'as current agenda' => '.current-agenda', + 'as agenda state' => '.state-tag.view.agenda-state', + 'as transition' => '.transition', 'in the notices' => '.flash.notice', 'in the errors' => '.error-messages', 'in the content body' => '.content-body', diff --git a/site/features/support/paths.rb b/site/features/support/paths.rb index 90b611b..515a7ec 100644 --- a/site/features/support/paths.rb +++ b/site/features/support/paths.rb @@ -17,6 +17,8 @@ module NavigationHelpers when /the signup page/ user_signup_path + when /the current agenda page/ + agenda_path(Agenda.current) # Add more mappings here. # Here is an example that pulls values out of the Regexp: # diff --git a/site/spec/factories.rb b/site/spec/factories.rb index a7256c0..b41c25c 100644 --- a/site/spec/factories.rb +++ b/site/spec/factories.rb @@ -8,6 +8,4 @@ Factory.define :user, :class => User do |u| u.email { |u| "#{u.name}@example.com" } end -Factory.define :guest do |g|; end - Factory.define :agenda do |a|; end diff --git a/site/spec/models/agenda_spec.rb b/site/spec/models/agenda_spec.rb index 9fd3c68..0a69551 100644 --- a/site/spec/models/agenda_spec.rb +++ b/site/spec/models/agenda_spec.rb @@ -1,32 +1,96 @@ require 'spec_helper' describe Agenda do - it "shouldn't allow anyone to create and destroy" do - agendas = [Agenda.new, Factory(:agenda)] - for a in agendas - for u in users_factory(AllRoles) - a.should_not be_creatable_by(u) - a.should_not be_destroyable_by(u) - end + it 'shouldn not allow anyone to create' do + a = Agenda.new + for u in users_factory(AllRoles) + a.should_not be_creatable_by(u) + a.should_not be_destroyable_by(u) end end - it "shouldn allow everybody to view" do - agendas = [Agenda.new, Factory(:agenda)] - for a in agendas - for u in users_factory(AllRoles) - a.should be_viewable_by(u) - end + it 'shouldn not allow anyone to destory' do + a = Factory(:agenda) + for u in users_factory(AllRoles) + a.should_not be_destroyable_by(u) end end - it "should allow only administrators and council members to edit and update" do - agendas = [Agenda.new, Factory(:agenda)] - for a in agendas - for u in users_factory(AllRoles) - a.should_not be_editable_by(u) - a.should_not be_updatable_by(u) - end + it 'should allow everybody to view' do + a = Factory(:agenda) + for u in users_factory(AllRoles) + a.should be_viewable_by(u) end end + + it 'should allow only administrators and council members to edit and update' do + a = Factory(:agenda) + for u in users_factory(:guest, :user) + a.should_not be_editable_by(u) + a.should_not be_updatable_by(u) + end + end + + def test_migration(object, migration, prohibited, allowed, final_state) + # object - object to migrate + # migration - migration name + # prohibited - array of users who can not perform migration + # allowed - *one* user who can perform migration + # final_state - state of object after migration + for user in prohibited + lambda { object.lifecycle.send("#{migration}!", user) }.should raise_error + end + + object.lifecycle.send("#{migration}!", allowed) + object.state.should == final_state + end + + it 'should have working transitions, available only to council members and administrators' do + Factory(:agenda) + prohibited = users_factory(:guest, :user) + allowed = users_factory(:council, :admin, :council_admin) + + for user in allowed + agenda = Agenda.last + test_migration(agenda, :close, prohibited, user, 'submissions_closed') + test_migration(agenda, :reopen, prohibited, user, 'open') + test_migration(agenda, :close, prohibited, user, 'submissions_closed') + test_migration(agenda, :archive, prohibited, user, 'old') + end + + end + + it 'that is non-archival should not be valid if there other open agenda' do + a = Factory(:agenda) + Agenda.new.should_not be_valid + Agenda.new(:state => 'submissions_closed').should_not be_valid + end + + it 'that is non-archival should not be valid if there other closed agenda' do + a = Factory(:agenda, :state => 'submissions_closed') + Agenda.new.should_not be_valid + Agenda.new(:state => 'submissions_closed').should_not be_valid + end + + it 'that is archival should be valid' do + a = Factory(:agenda, :state => 'old') + a.should be_valid + end + + it 'should create new open agenda, when current agenda is archived' do + a = Factory(:agenda) + u = users_factory(:admin) + a.lifecycle.close! u + a.lifecycle.archive! u + Agenda.last.state.should == 'open' + end + + it 'should set meeting time to now by default' do + a1 = Factory(:agenda) + a2 = Factory(:agenda, :meeting_time => 2.days.ago, :state => 'old') + today = Time.now.strftime('%Y-%m-%d') + + a1.meeting_time.strftime('%Y-%m-%d').should == today + a2.meeting_time.strftime('%Y-%m-%d').should_not == today + end end diff --git a/site/spec/spec_helper.rb b/site/spec/spec_helper.rb index 53f0ed3..a8b8aea 100644 --- a/site/spec/spec_helper.rb +++ b/site/spec/spec_helper.rb @@ -13,4 +13,5 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} RSpec.configure do |config| config.mock_with :rspec + config.use_transactional_fixtures = true end |