diff --git a/app/controllers/polls/questions_controller.rb b/app/controllers/polls/questions_controller.rb
new file mode 100644
index 000000000..dcf459485
--- /dev/null
+++ b/app/controllers/polls/questions_controller.rb
@@ -0,0 +1,17 @@
+class Polls::QuestionsController < ApplicationController
+
+ load_and_authorize_resource :poll
+ load_and_authorize_resource :question, class: 'Poll::Question', through: :poll
+
+ def answer
+ partial_result = @question.partial_results.find_or_initialize_by(author: current_user,
+ amount: 1,
+ origin: 'web')
+
+ partial_result.answer = params[:answer]
+ partial_result.save!
+
+ @answers_by_question_id = {@question.id => params[:answer]}
+ end
+
+end
diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb
new file mode 100644
index 000000000..9c83bc3e3
--- /dev/null
+++ b/app/controllers/polls_controller.rb
@@ -0,0 +1,22 @@
+class PollsController < ApplicationController
+
+ load_and_authorize_resource
+
+ has_filters %w{current expired incoming}
+
+ def index
+ @polls = @polls.send(@current_filter).sort_for_list.page(params[:page])
+ end
+
+ def show
+ @answerable_questions = @poll.questions.answerable_by(current_user).for_render.sort_for_list
+ @non_answerable_questions = @poll.questions.where.not(id: @answerable_questions.map(&:id)).for_render.sort_for_list
+
+ @answers_by_question_id = {}
+ poll_partial_results = Poll::PartialResult.by_question(@poll.question_ids).by_author(current_user.try(:id))
+ poll_partial_results.each do |result|
+ @answers_by_question_id[result.question_id] = result.answer
+ end
+ end
+
+end
diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb
index f82e78b58..36ce698b1 100644
--- a/app/models/abilities/common.rb
+++ b/app/models/abilities/common.rb
@@ -48,6 +48,10 @@ module Abilities
can :create, SpendingProposal
can :create, DirectMessage
can :show, DirectMessage, sender_id: user.id
+ can(:answer, Poll, Poll.answerable_by(user)){ |poll| poll.answerable_by?(user) }
+ can(:answer, Poll::Question, Poll::Question.answerable_by(user)) do |question|
+ question.answerable_by?(user)
+ end
end
can [:create, :show], ProposalNotification, proposal: { author_id: user.id }
diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb
index 39a7f69f5..8424500a3 100644
--- a/app/models/abilities/everyone.rb
+++ b/app/models/abilities/everyone.rb
@@ -6,6 +6,7 @@ module Abilities
can [:read, :map], Debate
can [:read, :map, :summary], Proposal
can :read, Comment
+ can :read, Poll
can :read, SpendingProposal
can :read, Legislation
can :read, User
diff --git a/app/models/poll.rb b/app/models/poll.rb
index 4543a2544..caa374dc8 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -1,6 +1,34 @@
class Poll < ActiveRecord::Base
has_many :booths
has_many :voters, through: :booths, class_name: "Poll::Voter"
+ has_many :questions
validates :name, presence: true
-end
\ No newline at end of file
+
+ scope :current, -> { where('starts_at <= ? and ? <= ends_at', Time.now, Time.now) }
+ scope :incoming, -> { where('? < starts_at', Time.now) }
+ scope :expired, -> { where('ends_at < ?', Time.now) }
+
+ scope :sort_for_list, -> { order(:starts_at) }
+
+ def current?(timestamp = DateTime.now)
+ starts_at <= timestamp && timestamp <= ends_at
+ end
+
+ def incoming?(timestamp = DateTime.now)
+ timestamp < starts_at
+ end
+
+ def expired?(timestamp = DateTime.now)
+ ends_at < timestamp
+ end
+
+ def answerable_by?(user)
+ user.present? && user.level_two_or_three_verified? && current?
+ end
+
+ def self.answerable_by(user)
+ return none if user.nil? || user.unverified?
+ current
+ end
+end
diff --git a/app/models/poll/partial_result.rb b/app/models/poll/partial_result.rb
new file mode 100644
index 000000000..202f147bd
--- /dev/null
+++ b/app/models/poll/partial_result.rb
@@ -0,0 +1,18 @@
+class Poll::PartialResult < ActiveRecord::Base
+
+ VALID_ORIGINS = %w{ web }
+
+ belongs_to :question, -> { with_hidden }
+ belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
+
+ validates :question, presence: true
+ validates :author, presence: true
+ validates :answer, presence: true
+ validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }}
+ validates :origin, inclusion: {in: VALID_ORIGINS}
+
+ scope :by_author, -> (author_id) { where(author_id: author_id) }
+ scope :by_question, -> (question_id) { where(question_id: question_id) }
+
+
+end
diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb
new file mode 100644
index 000000000..68f0be910
--- /dev/null
+++ b/app/models/poll/question.rb
@@ -0,0 +1,66 @@
+class Poll::Question < ActiveRecord::Base
+ include Measurable
+
+ acts_as_paranoid column: :hidden_at
+ include ActsAsParanoidAliases
+
+ belongs_to :poll
+ belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
+
+ has_many :comments, as: :commentable
+ has_many :answers
+ has_many :partial_results
+ has_and_belongs_to_many :geozones
+ belongs_to :proposal
+
+ validates :title, presence: true
+ validates :question, presence: true
+ validates :summary, presence: true
+ validates :author, presence: true
+
+ validates :title, length: { in: 4..Poll::Question.title_max_length }
+ validates :description, length: { maximum: Poll::Question.description_max_length }
+ validates :question, length: { in: 10..Poll::Question.question_max_length }
+
+ scope :sort_for_list, -> { order('poll_questions.proposal_id IS NULL', :created_at)}
+ scope :for_render, -> { includes(:author, :proposal) }
+ scope :by_geozone, -> (geozone_id) { joins(:geozones).where(geozones: {id: geozone_id}) }
+
+ def description
+ super.try :html_safe
+ end
+
+ def valid_answers
+ (super.try(:split, ',').compact || []).map(&:strip)
+ end
+
+ def copy_attributes_from_proposal(proposal)
+ if proposal.present?
+ self.author = proposal.author
+ self.author_visible_name = proposal.author.name
+ self.proposal_id = proposal.id
+ self.title = proposal.title
+ self.description = proposal.description
+ self.summary = proposal.summary
+ self.question = proposal.question
+ self.all_geozones = true
+ self.valid_answers = I18n.t('poll_questions.default_valid_answers')
+ end
+ end
+
+ def answerable_by?(user)
+ poll.answerable_by?(user) && (self.all_geozones || self.geozone_ids.include?(user.geozone_id))
+ end
+
+ def self.answerable_by(user)
+ return none if user.nil? || user.unverified?
+
+ joins('LEFT JOIN "geozones_poll_questions" ON "geozones_poll_questions"."question_id" = "poll_questions"."id"')
+ .where('poll_questions.poll_id IN (?) AND (poll_questions.all_geozones = ? OR geozones_poll_questions.geozone_id = ?)',
+ Poll.answerable_by(user).pluck(:id),
+ true,
+ user.geozone_id || -1) # user.geozone_id can be nil, which would throw errors on sql
+ .group('poll_questions.id')
+ end
+
+end
diff --git a/app/views/polls/index.html.erb b/app/views/polls/index.html.erb
new file mode 100644
index 000000000..4b39e09af
--- /dev/null
+++ b/app/views/polls/index.html.erb
@@ -0,0 +1,7 @@
+<%= render 'shared/filter_subnav', i18n_namespace: "polls.index" %>
+
+<% @polls.each do |poll| %>
+ <%= link_to poll.name, poll %>
+<% end %>
+
+<%= paginate @polls %>
diff --git a/app/views/polls/questions/_answers.html.erb b/app/views/polls/questions/_answers.html.erb
new file mode 100644
index 000000000..ee76706fc
--- /dev/null
+++ b/app/views/polls/questions/_answers.html.erb
@@ -0,0 +1,23 @@
+
+ <% if can? :answer, question %>
+
+ <% question.valid_answers.each do |answer| %>
+ <% if @answers_by_question_id[question.id] == answer %>
+
+ <%= answer %>
+
+ <% else %>
+ <%= link_to answer,
+ answer_poll_question_path(poll_id: question.poll_id, id: question.id, answer: answer),
+ method: :post,
+ remote: true,
+ class: "button secondary hollow" %>
+ <% end %>
+ <% end %>
+
+ <% else %>
+ <% question.valid_answers.each do |answer| %>
+
<%= answer %>
+ <% end %>
+ <% end %>
+
diff --git a/app/views/polls/questions/answer.js.erb b/app/views/polls/questions/answer.js.erb
new file mode 100644
index 000000000..aabbd8d89
--- /dev/null
+++ b/app/views/polls/questions/answer.js.erb
@@ -0,0 +1 @@
+$("#<%= dom_id(@question) %>_answers").html('<%= j render("polls/questions/answers", question: @question) %>');
diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb
new file mode 100644
index 000000000..ad7818a78
--- /dev/null
+++ b/app/views/polls/show.html.erb
@@ -0,0 +1,53 @@
+<%= @poll.name %>
+
+<% unless can?(:answer, @poll) %>
+
+ <% if current_user.nil? %>
+
+ <%= t("polls.show.cant_answer_not_logged_in",
+ signin: link_to(t("polls.show.signin"), new_user_session_path, class: "probe-message"),
+ signup: link_to(t("polls.show.signup"), new_user_registration_path, class: "probe-message")).html_safe %>
+
+ <% elsif current_user.unverified? %>
+
+ <%= t('polls.show.cant_answer_verify_html',
+ verify_link: link_to(t('polls.show.verify_link'), verification_path)) %>
+
+ <% elsif @poll.incoming? %>
+
+ <%= t('polls.show.cant_answer_incoming') %>
+
+ <% elsif @poll.expired? %>
+
+ <%= t('polls.show.cant_answer_expired') %>
+
+ <% end %>
+
+<% end %>
+
+<% @answerable_questions.each do |question| %>
+
+ <%= question.title %>
+
+
+ <%= render 'polls/questions/answers', question: question %>
+
+
+<% end %>
+
+<% if can?(:answer, @poll) &&
+ @non_answerable_questions.present? %>
+
+ <%= t('polls.show.cant_answer_wrong_geozone') %>
+
+<% end %>
+
+<% @non_answerable_questions.each do |question| %>
+
+ <%= question.title %>
+
+
+ <%= render 'polls/questions/answers', question: question %>
+
+
+<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9230ecbf8..7c9abd2b9 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -375,6 +375,18 @@ en:
update:
form:
submit_button: Save changes
+ polls:
+ show:
+ cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate."
+ signin: Sign in
+ signup: Sign up
+ cant_answer_verify_html: "You must %{verify_link} in order to answer."
+ verify_link: "verify your account"
+ cant_answer_incoming: "This poll has not yet started."
+ cant_answer_expired: "This poll has finished."
+ cant_answer_wrong_geozone: "The following questions are not available in your geozone."
+ poll_questions:
+ default_valid_answers: "Yes, No"
proposal_ballots:
title: "Votings"
description_html: "The following citizen proposals that have reached the required supports and will be voted."
diff --git a/config/routes.rb b/config/routes.rb
index 16625aa7c..95e5a87b1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -85,6 +85,12 @@ Rails.application.routes.draw do
get :search, on: :collection
end
+ resources :polls, only: [:show, :index] do
+ resources :questions, only: [], controller: 'polls/questions' do
+ post :answer, on: :member
+ end
+ end
+
resources :users, only: [:show] do
resources :direct_messages, only: [:new, :create, :show]
end
diff --git a/db/migrate/20161028104156_create_poll_questions.rb b/db/migrate/20161028104156_create_poll_questions.rb
new file mode 100644
index 000000000..13c8ac1e8
--- /dev/null
+++ b/db/migrate/20161028104156_create_poll_questions.rb
@@ -0,0 +1,21 @@
+class CreatePollQuestions < ActiveRecord::Migration
+ def change
+ create_table :poll_questions do |t|
+ t.references :proposal, index: true, foreign_key: true
+ t.references :poll, index: true, foreign_key: true
+ t.references :author, index: true # foreign key added later due to rails 4
+ t.string :author_visible_name
+ t.string :title
+ t.string :question
+ t.string :summary
+ t.string :valid_answers
+ t.text :description
+ t.integer :comments_count
+ t.datetime :hidden_at
+
+ t.timestamps
+ end
+
+ add_foreign_key :poll_questions, :users, column: :author_id
+ end
+end
diff --git a/db/migrate/20161028143204_create_geozones_poll_questions.rb b/db/migrate/20161028143204_create_geozones_poll_questions.rb
new file mode 100644
index 000000000..68077c510
--- /dev/null
+++ b/db/migrate/20161028143204_create_geozones_poll_questions.rb
@@ -0,0 +1,10 @@
+class CreateGeozonesPollQuestions < ActiveRecord::Migration
+ def change
+ create_table :geozones_poll_questions do |t|
+ t.references :geozone, index: true, foreign_key: true
+ t.integer :question_id, index: true
+ end
+
+ add_foreign_key :geozones_poll_questions, :poll_questions, column: :question_id
+ end
+end
diff --git a/db/migrate/20161107124207_add_all_geozones_to_poll_questions.rb b/db/migrate/20161107124207_add_all_geozones_to_poll_questions.rb
new file mode 100644
index 000000000..15ed41ba5
--- /dev/null
+++ b/db/migrate/20161107124207_add_all_geozones_to_poll_questions.rb
@@ -0,0 +1,5 @@
+class AddAllGeozonesToPollQuestions < ActiveRecord::Migration
+ def change
+ add_column :poll_questions, :all_geozones, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20161107174423_create_poll_partial_result.rb b/db/migrate/20161107174423_create_poll_partial_result.rb
new file mode 100644
index 000000000..a4f91a4f9
--- /dev/null
+++ b/db/migrate/20161107174423_create_poll_partial_result.rb
@@ -0,0 +1,14 @@
+class CreatePollPartialResult < ActiveRecord::Migration
+ def change
+ create_table :poll_partial_results do |t|
+ t.integer :question_id, index: true
+ t.integer :author_id, index: true
+ t.string :answer, index: true
+ t.integer :amount
+ t.string :origin, index: true
+ end
+
+ add_foreign_key(:poll_partial_results, :users, column: :author_id)
+ add_foreign_key(:poll_partial_results, :poll_questions, column: :question_id)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b5891df82..48341e5d1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161102133838) do
+ActiveRecord::Schema.define(version: 20161107174423) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -211,6 +211,14 @@ ActiveRecord::Schema.define(version: 20161102133838) do
t.string "census_code"
end
+ create_table "geozones_poll_questions", force: :cascade do |t|
+ t.integer "geozone_id"
+ t.integer "question_id"
+ end
+
+ add_index "geozones_poll_questions", ["geozone_id"], name: "index_geozones_poll_questions_on_geozone_id", using: :btree
+ add_index "geozones_poll_questions", ["question_id"], name: "index_geozones_poll_questions_on_question_id", using: :btree
+
create_table "identities", force: :cascade do |t|
t.integer "user_id"
t.string "provider"
@@ -287,6 +295,40 @@ ActiveRecord::Schema.define(version: 20161102133838) do
t.datetime "updated_at", null: false
end
+ create_table "poll_partial_results", force: :cascade do |t|
+ t.integer "question_id"
+ t.integer "author_id"
+ t.string "answer"
+ t.integer "amount"
+ t.string "origin"
+ end
+
+ add_index "poll_partial_results", ["answer"], name: "index_poll_partial_results_on_answer", using: :btree
+ add_index "poll_partial_results", ["author_id"], name: "index_poll_partial_results_on_author_id", using: :btree
+ add_index "poll_partial_results", ["origin"], name: "index_poll_partial_results_on_origin", using: :btree
+ add_index "poll_partial_results", ["question_id"], name: "index_poll_partial_results_on_question_id", using: :btree
+
+ create_table "poll_questions", force: :cascade do |t|
+ t.integer "proposal_id"
+ t.integer "poll_id"
+ t.integer "author_id"
+ t.string "author_visible_name"
+ t.string "title"
+ t.string "question"
+ t.string "summary"
+ t.string "valid_answers"
+ t.text "description"
+ t.integer "comments_count"
+ t.datetime "hidden_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "all_geozones", default: false
+ end
+
+ add_index "poll_questions", ["author_id"], name: "index_poll_questions_on_author_id", using: :btree
+ add_index "poll_questions", ["poll_id"], name: "index_poll_questions_on_poll_id", using: :btree
+ add_index "poll_questions", ["proposal_id"], name: "index_poll_questions_on_proposal_id", using: :btree
+
create_table "poll_voters", force: :cascade do |t|
t.integer "booth_id"
t.string "document_number"
@@ -583,12 +625,19 @@ ActiveRecord::Schema.define(version: 20161102133838) do
add_foreign_key "annotations", "users"
add_foreign_key "failed_census_calls", "users"
add_foreign_key "flags", "users"
+ add_foreign_key "geozones_poll_questions", "geozones"
+ add_foreign_key "geozones_poll_questions", "poll_questions", column: "question_id"
add_foreign_key "identities", "users"
add_foreign_key "locks", "users"
add_foreign_key "managers", "users"
add_foreign_key "moderators", "users"
add_foreign_key "notifications", "users"
add_foreign_key "organizations", "users"
+ add_foreign_key "poll_partial_results", "poll_questions", column: "question_id"
+ add_foreign_key "poll_partial_results", "users", column: "author_id"
+ add_foreign_key "poll_questions", "polls"
+ add_foreign_key "poll_questions", "proposals"
+ add_foreign_key "poll_questions", "users", column: "author_id"
add_foreign_key "users", "geozones"
add_foreign_key "valuators", "users"
end
diff --git a/spec/factories.rb b/spec/factories.rb
index b88af100c..78c6241d5 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -265,10 +265,21 @@ FactoryGirl.define do
factory :poll do
sequence(:name) { |n| "Poll #{n}" }
+
+ starts_at { 1.month.ago }
+ ends_at { 1.month.from_now }
+
+ trait :incoming do
+ starts_at { 2.days.from_now }
+ ends_at { 1.month.from_now }
+ end
+
+ trait :expired do
+ starts_at { 1.month.ago }
+ ends_at { 15.days.ago }
+ end
end
-<<<<<<< HEAD
-=======
factory :poll_officer, class: 'Poll::Officer' do
user
end
@@ -278,7 +289,6 @@ FactoryGirl.define do
association :booth, factory: :poll_booth
end
->>>>>>> assigns officers to booths
factory :poll_booth, class: 'Poll::Booth' do
sequence(:name) { |n| "Booth #{n}" }
sequence(:location) { |n| "Street #{n}" }
@@ -299,7 +309,23 @@ FactoryGirl.define do
end
end
->>>>>>> validates voter in census
+ factory :poll_question, class: 'Poll::Question' do
+ poll
+ association :author, factory: :user
+ sequence(:title) { |n| "Question title #{n}" }
+ sequence(:summary) { |n| "Question summary #{n}" }
+ sequence(:description) { |n| "Question description #{n}" }
+ sequence(:question) { |n| "Question question #{n}" }
+ valid_answers { Faker::Lorem.words(3).join(', ') }
+ end
+
+ factory :poll_partial_result, class: 'Poll::PartialResult' do
+ association :question, factory: :poll_question
+ association :author, factory: :user
+ origin { 'web' }
+ answer { question.verified_answers.sample }
+ end
+
factory :organization do
user
responsible_name "Johnny Utah"
diff --git a/spec/features/polls_spec.rb b/spec/features/polls_spec.rb
new file mode 100644
index 000000000..9270d5696
--- /dev/null
+++ b/spec/features/polls_spec.rb
@@ -0,0 +1,187 @@
+# coding: utf-8
+require 'rails_helper'
+
+feature 'Polls' do
+
+ context '#index' do
+
+ scenario 'Polls can be listed' do
+ polls = create_list(:poll, 3)
+
+ visit polls_path
+
+ polls.each do |poll|
+ expect(page).to have_link(poll.name)
+ end
+ end
+
+ scenario 'Filtering polls' do
+ create(:poll, name: "Current poll")
+ create(:poll, :incoming, name: "Incoming poll")
+ create(:poll, :expired, name: "Expired poll")
+
+ visit polls_path
+ expect(page).to have_content('Current poll')
+ expect(page).to_not have_content('Incoming poll')
+ expect(page).to_not have_content('Expired poll')
+
+ visit polls_path(filter: 'incoming')
+ expect(page).to_not have_content('Current poll')
+ expect(page).to have_content('Incoming poll')
+ expect(page).to_not have_content('Expired poll')
+
+ visit polls_path(filter: 'expired')
+ expect(page).to_not have_content('Current poll')
+ expect(page).to_not have_content('Incoming poll')
+ expect(page).to have_content('Expired poll')
+ end
+
+ scenario "Current filter is properly highlighted" do
+ visit polls_path
+ expect(page).to_not have_link('Current')
+ expect(page).to have_link('Incoming')
+ expect(page).to have_link('Expired')
+
+ visit polls_path(filter: 'incoming')
+ expect(page).to have_link('Current')
+ expect(page).to_not have_link('Incoming')
+ expect(page).to have_link('Expired')
+
+ visit polls_path(filter: 'expired')
+ expect(page).to have_link('Current')
+ expect(page).to have_link('Incoming')
+ expect(page).to_not have_link('Expired')
+ end
+ end
+
+ context 'Show' do
+ let(:geozone) { create(:geozone) }
+ let(:poll) { create(:poll) }
+
+ scenario 'Lists questions from proposals as well as regular ones' do
+ normal_question = create(:poll_question, poll: poll)
+ proposal_question = create(:poll_question, poll: poll, proposal: create(:proposal))
+
+ visit poll_path(poll)
+ expect(page).to have_content(poll.name)
+
+ expect(page).to have_content(normal_question.title)
+ expect(page).to have_content(proposal_question.title)
+ end
+
+ scenario 'Non-logged in users' do
+ create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca')
+ visit poll_path(poll)
+
+ expect(page).to have_content('Han Solo')
+ expect(page).to have_content('Chewbacca')
+ expect(page).to have_content('You must Sign in or Sign up to participate')
+
+ expect(page).to_not have_link('Han Solo')
+ expect(page).to_not have_link('Chewbacca')
+ end
+
+ scenario 'Level 1 users' do
+ create(:poll_question, poll: poll, geozone_ids: [geozone.id], valid_answers: 'Han Solo, Chewbacca')
+ login_as(create(:user, geozone: geozone))
+ visit poll_path(poll)
+
+ expect(page).to have_content('You must verify your account in order to answer')
+
+ expect(page).to have_content('Han Solo')
+ expect(page).to have_content('Chewbacca')
+
+ expect(page).to_not have_link('Han Solo')
+ expect(page).to_not have_link('Chewbacca')
+ end
+
+ scenario 'Level 2 users in an incoming question' do
+ incoming_poll = create(:poll, :incoming)
+ create(:poll_question, poll: incoming_poll, geozone_ids: [geozone.id], valid_answers: 'Rey, Finn')
+ login_as(create(:user, :level_two, geozone: geozone))
+
+ visit poll_path(incoming_poll)
+
+ expect(page).to have_content('Rey')
+ expect(page).to have_content('Finn')
+ expect(page).to_not have_link('Rey')
+ expect(page).to_not have_link('Finn')
+
+ expect(page).to have_content('This poll has not yet started')
+ end
+
+ scenario 'Level 2 users in an expired question' do
+ expired_poll = create(:poll, :expired)
+ create(:poll_question, poll: expired_poll, geozone_ids: [geozone.id], valid_answers: 'Luke, Leia')
+ login_as(create(:user, :level_two, geozone: geozone))
+
+ visit poll_path(expired_poll)
+
+ expect(page).to have_content('Luke')
+ expect(page).to have_content('Leia')
+ expect(page).to_not have_link('Luke')
+ expect(page).to_not have_link('Leia')
+
+ expect(page).to have_content('This poll has finished')
+ end
+
+ scenario 'Level 2 users in a poll with questions for a geozone which is not theirs' do
+ create(:poll_question, poll: poll, geozone_ids: [], valid_answers: 'Vader, Palpatine')
+ login_as(create(:user, :level_two))
+
+ visit poll_path(poll)
+
+ expect(page).to have_content('The following questions are not available in your geozone')
+
+ expect(page).to have_content('Vader')
+ expect(page).to have_content('Palpatine')
+ expect(page).to_not have_link('Vader')
+ expect(page).to_not have_link('Palpatine')
+ end
+
+ scenario 'Level 2 users reading a same-geozone question' do
+ create(:poll_question, poll: poll, geozone_ids: [geozone.id], valid_answers: 'Han Solo, Chewbacca')
+ login_as(create(:user, :level_two, geozone: geozone))
+ visit poll_path(poll)
+
+ expect(page).to have_link('Han Solo')
+ expect(page).to have_link('Chewbacca')
+ end
+
+ scenario 'Level 2 users reading a all-geozones question' do
+ create(:poll_question, poll: poll, all_geozones: true, valid_answers: 'Han Solo, Chewbacca')
+ login_as(create(:user, :level_two, geozone: geozone))
+ visit poll_path(poll)
+
+ expect(page).to have_link('Han Solo')
+ expect(page).to have_link('Chewbacca')
+ end
+
+ scenario 'Level 2 users who have already answered' do
+ question = create(:poll_question, poll: poll, geozone_ids:[geozone.id], valid_answers: 'Han Solo, Chewbacca')
+ user = create(:user, :level_two, geozone: geozone)
+ create(:poll_partial_result, question: question, author: user, answer: 'Chewbacca')
+ login_as user
+ visit poll_path(poll)
+
+ expect(page).to have_link('Han Solo')
+ expect(page).to_not have_link('Chewbacca')
+ expect(page).to have_content('Chewbacca')
+ end
+
+ scenario 'Level 2 users answering', :js do
+ create(:poll_question, poll: poll, geozone_ids: [geozone.id], valid_answers: 'Han Solo, Chewbacca')
+ user = create(:user, :level_two, geozone: geozone)
+ login_as user
+ visit poll_path(poll)
+
+ click_link 'Han Solo'
+
+ expect(page).to_not have_link('Han Solo')
+ expect(page).to have_link('Chewbacca')
+ end
+
+ end
+
+end
+
diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb
index ab14c01bd..59618f785 100644
--- a/spec/models/abilities/common_spec.rb
+++ b/spec/models/abilities/common_spec.rb
@@ -3,8 +3,9 @@ require 'cancan/matchers'
describe "Abilities::Common" do
subject(:ability) { Ability.new(user) }
+ let(:geozone) { create(:geozone) }
- let(:user) { create(:user) }
+ let(:user) { create(:user, geozone: geozone) }
let(:debate) { create(:debate) }
let(:comment) { create(:comment) }
@@ -13,6 +14,20 @@ describe "Abilities::Common" do
let(:own_comment) { create(:comment, author: user) }
let(:own_proposal) { create(:proposal, author: user) }
+ let(:current_poll) { create(:poll) }
+ let(:incoming_poll) { create(:poll, :incoming) }
+ let(:expired_poll) { create(:poll, :expired) }
+
+ let(:poll_question_from_own_geozone) { create(:poll_question, geozones: [geozone]) }
+ let(:poll_question_from_other_geozone) { create(:poll_question, geozones: [create(:geozone)]) }
+ let(:poll_question_from_all_geozones) { create(:poll_question, all_geozones: true) }
+ let(:expired_poll_question_from_own_geozone) { create(:poll_question, poll: expired_poll, geozones: [geozone]) }
+ let(:expired_poll_question_from_other_geozone) { create(:poll_question, poll: expired_poll, geozones: [create(:geozone)]) }
+ let(:expired_poll_question_from_all_geozones) { create(:poll_question, poll: expired_poll, all_geozones: true) }
+ let(:incoming_poll_question_from_own_geozone) { create(:poll_question, poll: incoming_poll, geozones: [geozone]) }
+ let(:incoming_poll_question_from_other_geozone) { create(:poll_question, poll: incoming_poll, geozones: [create(:geozone)]) }
+ let(:incoming_poll_question_from_all_geozones) { create(:poll_question, poll: incoming_poll, all_geozones: true) }
+
it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) }
it { should be_able_to(:vote, debate) }
@@ -103,6 +118,33 @@ describe "Abilities::Common" do
it { should be_able_to(:create, DirectMessage) }
it { should be_able_to(:show, own_direct_message) }
it { should_not be_able_to(:show, create(:direct_message)) }
+
+ it { should be_able_to(:answer, current_poll) }
+ it { should_not be_able_to(:answer, expired_poll) }
+ it { should_not be_able_to(:answer, incoming_poll) }
+
+ it { should be_able_to(:answer, poll_question_from_own_geozone ) }
+ it { should be_able_to(:answer, poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
+
+ context "without geozone" do
+ before(:each) { user.geozone = nil }
+ it { should_not be_able_to(:answer, poll_question_from_own_geozone ) }
+ it { should be_able_to(:answer, poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
+ end
end
describe "when level 3 verified" do
@@ -121,5 +163,32 @@ describe "Abilities::Common" do
it { should be_able_to(:create, DirectMessage) }
it { should be_able_to(:show, own_direct_message) }
it { should_not be_able_to(:show, create(:direct_message)) }
+
+ it { should be_able_to(:answer, current_poll) }
+ it { should_not be_able_to(:answer, expired_poll) }
+ it { should_not be_able_to(:answer, incoming_poll) }
+
+ it { should be_able_to(:answer, poll_question_from_own_geozone ) }
+ it { should be_able_to(:answer, poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
+
+ context "without geozone" do
+ before(:each) { user.geozone = nil }
+ it { should_not be_able_to(:answer, poll_question_from_own_geozone ) }
+ it { should be_able_to(:answer, poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
+ it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
+ end
end
end
diff --git a/spec/models/poll/partial_result_spec.rb b/spec/models/poll/partial_result_spec.rb
new file mode 100644
index 000000000..ef47ecd8c
--- /dev/null
+++ b/spec/models/poll/partial_result_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+describe Poll::PartialResult, type: :model do
+
+ describe "validations" do
+ it "validates that the answers are included in the Enquiry's list" do
+ q = create(:poll_question, valid_answers: 'One, Two, Three')
+ expect(build(:poll_partial_result, question: q, answer: 'One')).to be_valid
+ expect(build(:poll_partial_result, question: q, answer: 'Two')).to be_valid
+ expect(build(:poll_partial_result, question: q, answer: 'Three')).to be_valid
+
+ expect(build(:poll_partial_result, question: q, answer: 'Four')).to_not be_valid
+ end
+ end
+
+end
diff --git a/spec/models/poll/question_spec.rb b/spec/models/poll/question_spec.rb
new file mode 100644
index 000000000..6cb38d023
--- /dev/null
+++ b/spec/models/poll/question_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.describe Poll::Question, type: :model do
+
+ describe "#valid_answers" do
+ it "gets a comma-separated string, but returns an array" do
+ q = create(:poll_question, valid_answers: "Yes, No")
+ expect(q.valid_answers).to eq(["Yes", "No"])
+ end
+ end
+
+ describe "#copy_attributes_from_proposal" do
+ it "copies the attributes from the proposal" do
+ create_list(:geozone, 3)
+ p = create(:proposal)
+ q = create(:poll_question)
+ q.copy_attributes_from_proposal(p)
+ expect(q.valid_answers).to eq(['Yes', 'No'])
+ expect(q.author).to eq(p.author)
+ expect(q.author_visible_name).to eq(p.author.name)
+ expect(q.proposal_id).to eq(p.id)
+ expect(q.title).to eq(p.title)
+ expect(q.question).to eq(p.question)
+ expect(q.all_geozones).to be_true
+ end
+ end
+
+end