aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoachim Filip Ignacy Bartosik <jbartosik@gmail.com>2011-05-13 18:50:23 +0200
committerJoachim Filip Ignacy Bartosik <jbartosik@gmail.com>2011-05-24 18:55:39 +0200
commita0925ae845600d7e07a672b2a1a62d749154fabc (patch)
treee3516e75b60116a7f7fa4aa7ee5e77f5a38e0f9b
parentUse fuubar (diff)
downloadcouncil-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.rb3
-rw-r--r--site/app/models/agenda.rb52
-rw-r--r--site/app/views/agendas/index.dryml5
-rw-r--r--site/app/views/agendas/show.dryml9
-rw-r--r--site/config/hobo_routes.rb8
-rw-r--r--site/config/initializers/agenda.rb12
-rw-r--r--site/db/schema.rb6
-rw-r--r--site/features/agendas.feature37
-rw-r--r--site/features/step_definitions/agenda_steps.rb26
-rw-r--r--site/features/step_definitions/within_steps.rb3
-rw-r--r--site/features/support/paths.rb2
-rw-r--r--site/spec/factories.rb2
-rw-r--r--site/spec/models/agenda_spec.rb104
-rw-r--r--site/spec/spec_helper.rb1
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