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