Merge pull request #40 from medialab-prado/27-answer-questions

Answer legislation questions
This commit is contained in:
Fernando Blat
2016-12-29 07:38:34 +01:00
committed by GitHub
29 changed files with 400 additions and 52 deletions

View File

@@ -50,6 +50,7 @@
//= require markdown_editor
//= require cocoon
//= require allegations
//= require legislation_questions
//= require custom
var initialize_modules = function() {
@@ -74,6 +75,7 @@ var initialize_modules = function() {
App.SocialShare.initialize();
App.MarkdownEditor.initialize();
App.Allegations.initialize();
App.LegislationQuestions.initialize();
};
$(function(){

View File

@@ -0,0 +1,8 @@
App.LegislationQuestions =
initialize: ->
$('form#new_legislation_answer input.button').hide()
$('form#new_legislation_answer input[type=radio]').on
click: ->
$('form#new_legislation_answer').submit()

View File

@@ -13,4 +13,5 @@ App.Votes =
initialize: ->
App.Votes.hoverize "div.votes"
App.Votes.hoverize "div.supports"
App.Votes.hoverize "div.debate-questions"
false

View File

@@ -296,6 +296,7 @@
}
.debate-questions {
position: relative;
list-style: none;
.control {

View File

@@ -0,0 +1,41 @@
class Legislation::AnswersController < Legislation::BaseController
before_action :authenticate_user!
before_action :verify_resident!
load_and_authorize_resource :process
load_and_authorize_resource :question, through: :process
load_and_authorize_resource :answer, through: :question
respond_to :html, :js
def create
if @process.open_phase?(:debate)
@answer.user = current_user
@answer.save
track_event
respond_to do |format|
format.js
format.html { redirect_to legislation_process_question_path(@process, @question) }
end
else
respond_to do |format|
format.js
format.html { redirect_to legislation_process_question_path(@process, @question), alert: t('legislation.questions.participation.phase_not_open') }
end
end
end
private
def answer_params
params.require(:legislation_answer).permit(
:legislation_question_option_id,
)
end
def track_event
ahoy.track "legislation_answer_created".to_sym,
"legislation_answer_id": @answer.id,
"legislation_question_option_id": @answer.legislation_question_option_id,
"legislation_question_id": @answer.legislation_question_id
end
end

View File

@@ -8,5 +8,6 @@ class Legislation::QuestionsController < Legislation::BaseController
@commentable = @question
@comment_tree = CommentTree.new(@commentable, params[:page], @current_order)
set_comment_flags(@comment_tree.comments)
@answer = @question.answer_for_user(current_user) || Legislation::Answer.new
end
end

View File

@@ -14,6 +14,7 @@ module Abilities
can [:read, :draft_publication, :allegations, :final_version_publication], Legislation::Process
can [:read], Legislation::DraftVersion
can [:read], Legislation::Question
can [:create], Legislation::Answer
end
end
end

View File

@@ -0,0 +1,9 @@
class Legislation::Answer < ActiveRecord::Base
belongs_to :question, class_name: 'Legislation::Question', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :answers, counter_cache: true
belongs_to :question_option, class_name: 'Legislation::QuestionOption', foreign_key: 'legislation_question_option_id', dependent: :destroy, inverse_of: :answers, counter_cache: true
belongs_to :user, dependent: :destroy, inverse_of: :legislation_answers
validates :question, presence: true, uniqueness: { scope: :user_id}
validates :question_option, presence: true
validates :user, presence: true
end

View File

@@ -2,9 +2,9 @@ class Legislation::Process < ActiveRecord::Base
acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases
has_many :draft_versions, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id'
has_many :draft_versions, -> { order(:id) }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id'
has_one :final_draft_version, -> { where final_version: true }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id'
has_many :questions, class_name: 'Legislation::Question', foreign_key: 'legislation_process_id'
has_many :questions, -> { order(:id) }, class_name: 'Legislation::Question', foreign_key: 'legislation_process_id'
validates :title, presence: true
validates :description, presence: true

View File

@@ -5,7 +5,8 @@ class Legislation::Question < ActiveRecord::Base
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id'
has_many :question_options, class_name: 'Legislation::QuestionOption', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question
has_many :question_options, -> { order(:id) }, class_name: 'Legislation::QuestionOption', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question
has_many :answers, class_name: 'Legislation::Answer', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question
has_many :comments, as: :commentable
accepts_nested_attributes_for :question_options, :reject_if => proc { |attributes| attributes[:value].blank? }, allow_destroy: true
@@ -16,4 +17,8 @@ class Legislation::Question < ActiveRecord::Base
def next_question_id
@next_question_id ||= process.questions.where("id > ?", id).order('id ASC').limit(1).pluck(:id).first
end
def answer_for_user(user)
answers.where(user: user).first
end
end

View File

@@ -3,6 +3,7 @@ class Legislation::QuestionOption < ActiveRecord::Base
include ActsAsParanoidAliases
belongs_to :question, class_name: 'Legislation::Question', foreign_key: 'legislation_question_id', inverse_of: :question_options
has_many :answers, class_name: 'Legislation::Answer', foreign_key: 'legislation_question_id', dependent: :destroy, inverse_of: :question
validates :question, presence: true
validates :value, presence: true, uniqueness: { scope: :legislation_question_id }

View File

@@ -25,6 +25,7 @@ class User < ActiveRecord::Base
has_many :notifications
has_many :direct_messages_sent, class_name: 'DirectMessage', foreign_key: :sender_id
has_many :direct_messages_received, class_name: 'DirectMessage', foreign_key: :receiver_id
has_many :legislation_answers, class_name: 'Legislation::Answer', dependent: :destroy, inverse_of: :user
belongs_to :geozone
validates :username, presence: true, if: :username_required?

View File

@@ -44,7 +44,7 @@
<% end %>
<% if feature?(:legislation) %>
<li <%= "class=active" if controller_name == "processes" %>>
<li <%= "class=active" if controller.class.parent == Admin::Legislation %>>
<%= link_to admin_legislation_processes_path do %>
<span class="icon-budget"></span><%= t("admin.menu.legislation") %>
<% end %>

View File

@@ -29,7 +29,9 @@
<td>
<%= content_tag :ul do %>
<% question.question_options.each do |question_option| %>
<%= content_tag :li, question_option.value %>
<%= content_tag :li do %>
<%= question_option.value %> (<%= question_option.answers_count %>)
<% end %>
<% end %>
<% end %>
</td>

View File

@@ -0,0 +1 @@
$("#legislation-answer-form").html('<%= j render("legislation/questions/answer_form", process: @process, question: @question, answer: @answer) %>');

View File

@@ -0,0 +1,28 @@
<% if question.question_options.any? %>
<% if process.open_phase?(:debate) && !answer.persisted? %>
<%= form_for answer, url: legislation_process_question_answers_path(process, question, answer), remote: true , html: { class: "controls-stacked"} do |f| %>
<% question.question_options.each do |question_option| %>
<label class="control radio <%= 'active' if @answer.legislation_question_option_id == question_option.id %>">
<%= f.radio_button :legislation_question_option_id, question_option.id, label: false %>
<span class="control-indicator"></span>
<%= question_option.value %>
</label>
<% end %>
<%= f.submit t('legislation.questions.show.answer_question'), class: "button" %>
<% end %>
<% else %>
<form class="controls-stacked disabled">
<% question.question_options.each do |question_option| %>
<label class="control radio <%= 'active' if answer.persisted? && (answer.legislation_question_option_id == question_option.id) %>">
<input id="quiz-1" name="radio" type="radio" disabled="true">
<span class="control-indicator"></span>
<%= question_option.value %>
</label>
<% end %>
</form>
<% end %>
<% end %>

View File

@@ -0,0 +1,20 @@
<% if user_signed_in? && current_user.organization? %>
<div class="participation-not-allowed" style='display:none' aria-hidden="false">
<p>
<%= t("legislation.questions.participation.organizations") %>
</p>
</div>
<% elsif user_signed_in? && current_user.unverified? %>
<div class="participation-not-allowed" style='display:none' aria-hidden="false">
<p>
<%= t("legislation.questions.participation.verified_only",
verify_account: link_to(t("legislation.questions.participation.verify_account"), verification_path )).html_safe %>
</p>
</div>
<% elsif !user_signed_in? %>
<div class="participation-not-allowed" style='display:none' aria-hidden="false">
<%= t("legislation.questions.participation.unauthenticated",
signin: link_to(t("legislation.questions.participation.signin"), new_user_session_path),
signup: link_to(t("legislation.questions.participation.signup"), new_user_registration_path)).html_safe %>
</div>
<% end %>

View File

@@ -22,17 +22,10 @@
<div class="row">
<div class="small-12 medium-9 column">
<h3 class="quiz-question"><%= @question.title %></h3>
<div class="debate-questions">
<form class="controls-stacked">
<% @question.question_options.each do |question_option| %>
<label class="control radio">
<input id="quiz-1" name="radio" type="radio">
<span class="control-indicator"></span>
<%= question_option.value %>
</label>
<% end %>
</form>
</div>
<div class="debate-questions" id="legislation-answer-form">
<%= render 'answer_form', process: @process, question: @question, answer: @answer %>
<%= render 'participation_not_allowed' %>
</div>
</div>
<aside class="small-12 medium-3 column">

View File

@@ -40,6 +40,15 @@ en:
legislation/draft_versions:
one: "Draft version"
other: "Draft versions"
legislation/questions:
one: "Question"
other: "Questions"
legislation/question_options:
one: "Question option"
other: "Question options"
legislation/answers:
one: "Answer"
other: "Answers"
attributes:
comment:
body: "Comment"
@@ -99,6 +108,10 @@ en:
changelog: Changes
status: Status
final_version: Final version
legislation/questions:
title: "Title"
legislation/question_options:
value: "Value"
errors:
models:
user:

View File

@@ -40,6 +40,15 @@ es:
legislation/draft_texts:
one: "Borrador"
other: "Borradores"
legislation/questions:
one: "Pregunta"
other: "Preguntas"
legislation/question_options:
one: "Opción de respuesta cerrada"
other: "Opciones de respuesta cerrada"
legislation/answers:
one: "Respuesta"
other: "Respuestas"
attributes:
comment:
body: "Comentario"
@@ -99,6 +108,10 @@ es:
changelog: Cambios
status: Estado
final_version: Versión final
legislation/questions:
title: "Título"
legislation/question_options:
value: "Valor"
errors:
models:
user:

View File

@@ -270,6 +270,7 @@ en:
other: "%{count} comments"
debate: Debate
show:
answer_question: Submit answer
comments: Comments
next_question: Next question
share: Share
@@ -277,6 +278,14 @@ en:
share_facebook: Share on Facebook
share_gplus: Share on Google+
title: Collaborative legislation process
participation:
phase_not_open: This phase is not open
organizations: Organisations are not permitted to participate in the debate
signin: Sign in
signup: Sign up
unauthenticated: You must %{signin} or %{signup} to participate.
verified_only: Only verified users can participate, %{verify_account}.
verify_account: verify your account
locale: English
notifications:
index:

View File

@@ -270,6 +270,7 @@ es:
other: "%{count} comentarios"
debate: Debate
show:
answer_question: Enviar respuesta
comments: Comentarios
next_question: Siguiente pregunta
share: Compartir
@@ -277,6 +278,14 @@ es:
share_facebook: Compartir en Facebook
share_gplus: Compartir en Google+
title: Proceso de legislación colaborativa
participation:
phase_not_open: Esta fase no está abierta
organizations: Las organizaciones no pueden participar en el debate
signin: iniciar sesión
signup: registrarte
unauthenticated: Necesitas %{signin} o %{signup} para participar en el debate.
verified_only: Solo los usuarios verificados pueden participar en el debate, %{verify_account}.
verify_account: verifica tu cuenta
locale: Español
notifications:
index:

View File

@@ -0,0 +1,13 @@
class CreateLegislationAnswers < ActiveRecord::Migration
def change
create_table :legislation_answers do |t|
t.references :legislation_question, index: true
t.references :legislation_question_option, index: true
t.references :user, index: true
t.datetime :hidden_at, index: true
t.timestamps null: false
end
end
end

View File

@@ -227,6 +227,20 @@ ActiveRecord::Schema.define(version: 20161222180927) do
t.datetime "updated_at", null: false
end
create_table "legislation_answers", force: :cascade do |t|
t.integer "legislation_question_id"
t.integer "legislation_question_option_id"
t.integer "user_id"
t.datetime "hidden_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "legislation_answers", ["hidden_at"], name: "index_legislation_answers_on_hidden_at", using: :btree
add_index "legislation_answers", ["legislation_question_id"], name: "index_legislation_answers_on_legislation_question_id", using: :btree
add_index "legislation_answers", ["legislation_question_option_id"], name: "index_legislation_answers_on_legislation_question_option_id", using: :btree
add_index "legislation_answers", ["user_id"], name: "index_legislation_answers_on_user_id", using: :btree
create_table "legislation_draft_versions", force: :cascade do |t|
t.integer "legislation_process_id"
t.string "title"

View File

@@ -0,0 +1,43 @@
require 'rails_helper'
describe Legislation::AnswersController do
describe 'POST create' do
before(:each) do
InvisibleCaptcha.timestamp_enabled = false
@process = create(:legislation_process, debate_start_date: Date.current - 3.day, debate_end_date: Date.current + 2.days)
@question = create(:legislation_question, process: @process, title: "Question 1")
@question_option = create(:legislation_question_option, question: @question, value: "Yes")
@user = create(:user, :level_two)
end
after(:each) do
InvisibleCaptcha.timestamp_enabled = true
end
it 'should create an ahoy event' do
sign_in @user
post :create, process_id: @process.id, question_id: @question.id, legislation_answer: { legislation_question_option_id: @question_option.id }
expect(Ahoy::Event.where(name: :legislation_answer_created).count).to eq 1
expect(Ahoy::Event.last.properties['legislation_answer_id']).to eq Legislation::Answer.last.id
end
it 'should create an answer if the process debate phase is open' do
sign_in @user
expect do
xhr :post, :create, process_id: @process.id, question_id: @question.id, legislation_answer: { legislation_question_option_id: @question_option.id }
end.to change { @question.reload.answers_count }.by(1)
end
it 'should not create an answer if the process debate phase is not open' do
sign_in @user
@process.update_attribute(:debate_end_date, Date.current - 1.day)
expect do
xhr :post, :create, process_id: @process.id, question_id: @question.id, legislation_answer: { legislation_question_option_id: @question_option.id }
end.to_not change { @question.reload.answers_count }
end
end
end

View File

@@ -411,4 +411,10 @@ FactoryGirl.define do
question factory: :legislation_question
sequence(:value) { |n| "Option #{n}" }
end
factory :legislation_answer, class: 'Legislation::Answer' do
question factory: :legislation_question
question_option factory: :legislation_question_option
user
end
end

View File

@@ -1,36 +0,0 @@
require 'rails_helper'
feature 'Legislation' do
context 'process debate page' do
scenario 'shows question list' do
process = create(:legislation_process, debate_start_date: Date.current - 1.day, debate_end_date: Date.current + 2.days)
create(:legislation_question, process: process, title: "Question 1")
create(:legislation_question, process: process, title: "Question 2")
create(:legislation_question, process: process, title: "Question 3")
visit legislation_process_path(process)
expect(page).to have_content("Participate in the debate")
expect(page).to have_content("Question 1")
expect(page).to have_content("Question 2")
expect(page).to have_content("Question 3")
click_link "Question 1"
expect(page).to have_content("Question 1")
expect(page).to have_content("Next question")
click_link "Next question"
expect(page).to have_content("Question 2")
expect(page).to have_content("Next question")
click_link "Next question"
expect(page).to have_content("Question 3")
expect(page).to_not have_content("Next question")
end
end
end

View File

@@ -0,0 +1,110 @@
require 'rails_helper'
feature 'Legislation' do
context 'process debate page' do
before(:each) do
@process = create(:legislation_process, debate_start_date: Date.current - 3.day, debate_end_date: Date.current + 2.days)
create(:legislation_question, process: @process, title: "Question 1")
create(:legislation_question, process: @process, title: "Question 2")
create(:legislation_question, process: @process, title: "Question 3")
end
scenario 'shows question list' do
visit legislation_process_path(@process)
expect(page).to have_content("Participate in the debate")
expect(page).to have_content("Question 1")
expect(page).to have_content("Question 2")
expect(page).to have_content("Question 3")
click_link "Question 1"
expect(page).to have_content("Question 1")
expect(page).to have_content("Next question")
click_link "Next question"
expect(page).to have_content("Question 2")
expect(page).to have_content("Next question")
click_link "Next question"
expect(page).to have_content("Question 3")
expect(page).to_not have_content("Next question")
end
scenario 'shows question page' do
visit legislation_process_question_path(@process, @process.questions.first)
expect(page).to have_content("Question 1")
expect(page).to have_content("Comments (0)")
end
scenario 'shows next question link in question page' do
visit legislation_process_question_path(@process, @process.questions.first)
expect(page).to have_content("Question 1")
expect(page).to have_content("Next question")
click_link "Next question"
expect(page).to have_content("Question 2")
expect(page).to have_content("Next question")
click_link "Next question"
expect(page).to have_content("Question 3")
expect(page).to_not have_content("Next question")
end
scenario 'answer question' do
question = @process.questions.first
create(:legislation_question_option, question: question, value: "Yes")
create(:legislation_question_option, question: question, value: "No")
option = create(:legislation_question_option, question: question, value: "I don't know")
user = create(:user, :level_two)
login_as(user)
visit legislation_process_question_path(@process, question)
expect(page).to have_selector(:radio_button, "Yes")
expect(page).to have_selector(:radio_button, "No")
expect(page).to have_selector(:radio_button, "I don't know")
expect(page).to have_selector(:link_or_button, "Submit answer")
choose("I don't know")
click_button "Submit answer"
within(:css, "label.active") do
expect(page).to have_content("I don't know")
expect(page).to_not have_content("Yes")
expect(page).to_not have_content("No")
end
expect(page).to_not have_selector(:link_or_button, "Submit answer")
expect(question.reload.answers_count).to eq(1)
expect(option.reload.answers_count).to eq(1)
end
scenario 'cannot answer question when phase not open' do
@process.update_attribute(:debate_end_date, Date.current - 1.day)
question = @process.questions.first
create(:legislation_question_option, question: question, value: "Yes")
create(:legislation_question_option, question: question, value: "No")
create(:legislation_question_option, question: question, value: "I don't know")
user = create(:user, :level_two)
login_as(user)
visit legislation_process_question_path(@process, question)
expect(page).to have_selector(:radio_button, "Yes", disabled: true)
expect(page).to have_selector(:radio_button, "No", disabled: true)
expect(page).to have_selector(:radio_button, "I don't know", disabled: true)
expect(page).to_not have_selector(:link_or_button, "Submit answer")
end
end
end

View File

@@ -0,0 +1,39 @@
require 'rails_helper'
RSpec.describe Legislation::Answer, type: :model do
let(:legislation_answer) { build(:legislation_answer) }
it "should be valid" do
expect(legislation_answer).to be_valid
end
it "counts answers" do
question = create(:legislation_question)
option_1 = create(:legislation_question_option, question: question, value: 'Yes')
option_2 = create(:legislation_question_option, question: question, value: 'No')
answer = create(:legislation_answer, question: question, question_option: option_2)
expect(answer).to be_valid
expect(question.answers_count).to eq 1
expect(option_2.answers_count).to eq 1
expect(option_1.answers_count).to eq 0
end
it "can't answer same question more than once" do
question = create(:legislation_question)
option_1 = create(:legislation_question_option, question: question, value: 'Yes')
option_2 = create(:legislation_question_option, question: question, value: 'No')
user = create(:user)
answer = create(:legislation_answer, question: question, question_option: option_2, user: user)
expect(answer).to be_valid
second_answer = build(:legislation_answer, question: question, question_option: option_1, user: user)
expect(second_answer).to be_invalid
expect(question.answers_count).to eq 1
expect(option_2.answers_count).to eq 1
expect(option_1.answers_count).to eq 0
end
end