diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot index c1afd24fd..5f29191f7 100644 Binary files a/app/assets/fonts/icons.eot and b/app/assets/fonts/icons.eot differ diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg index 68d878942..2b884a912 100644 --- a/app/assets/fonts/icons.svg +++ b/app/assets/fonts/icons.svg @@ -62,4 +62,5 @@ + diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf index a9443499b..252457611 100644 Binary files a/app/assets/fonts/icons.ttf and b/app/assets/fonts/icons.ttf differ diff --git a/app/assets/fonts/icons.woff b/app/assets/fonts/icons.woff index 09a6f6dc5..2cf6dc4b7 100644 Binary files a/app/assets/fonts/icons.woff and b/app/assets/fonts/icons.woff differ diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index e55832667..135b9fe12 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -97,10 +97,6 @@ content: '\72'; } -.icon-documents::before { - content: '\68'; -} - .icon-proposals::before { content: '\68'; } @@ -197,10 +193,6 @@ content: '\53'; } -.icon-image::before { - content: '\68'; -} - .icon-notification::before { content: '\6e'; } @@ -264,3 +256,7 @@ .icon-instagram::before { content: '\32'; } + +.icon-image::before { + content: '\33'; +} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 67abd0f1f..44b450779 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -354,6 +354,18 @@ a { vertical-align: top; } +.aling-middle { + vertical-align: middle; +} + +.table { + display: table; +} + +.table-cell { + display: table-cell; +} + // 02. Header // ---------- diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss index 0028683c2..1aa51d1a6 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -64,8 +64,8 @@ @mixin direct-uploads { .cached-image { - max-width: 150px; - max-height: 150px; + max-width: rem-calc(150); + max-height: rem-calc(150); } .progress-bar-placeholder { @@ -78,15 +78,23 @@ .document-attachment, .image-attachment { - padding-left:0; + padding-left: 0; - p{ + p { margin-bottom: 0; } } - input.js-document-attachment, - input.js-image-attachment{ - display: none; + + .attachment-errors { + + > .js-image-attachment, + > .js-document-attachment { + display: none; + + ~ .error { + display: inline-block; + } + } } } diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index fbcda0ebb..b1edc246d 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -255,7 +255,6 @@ .icon-debates, .icon-proposals, .icon-budget, - .icon-documents, .icon-image { font-size: rem-calc(50); line-height: $line-height; @@ -267,7 +266,6 @@ } .icon-proposals, - .icon-documents, .icon-image { color: $proposals; } @@ -312,12 +310,10 @@ .budget-investment-new, .proposal-form, .proposal-edit, -.new_poll_question, -.edit_poll_question { +.poll-question-form { @include direct-uploads; } - // 03. Show participation // ---------------------- @@ -358,8 +354,7 @@ width: rem-calc(48); } - .edit-debate, - .edit-proposal { + .edit-debate { margin-bottom: 0; } @@ -661,20 +656,17 @@ .proposals-list .proposal { @include breakpoint(small) { + .no-image { width: 100%; - max-width: 300px; + max-width: rem-calc(300); margin: 0 auto; - } - .no-image::before { - content: ''; - display: block; - padding-top: 100%; - } - - h3 { - font-size: 1.3rem; + &::before { + content: ''; + display: block; + padding-top: 100%; + } } .column:first-child { @@ -683,19 +675,17 @@ } @include breakpoint(medium) { + .panel { - padding: 0 0.75rem 0 0; + padding: 0 $line-height / 2 0 0; .no-image { - height: 245px; - width: 140px; + height: 100%; + min-height: rem-calc(245); + width: rem-calc(140); } } - h3 { - font-size: 1.4rem; - } - .column:first-child { overflow: hidden; } @@ -705,7 +695,7 @@ } .column:last-child:not(:first-child) { - padding-top: 0.75rem; + padding-top: $line-height / 2; } img { @@ -1701,9 +1691,13 @@ } .section-title-divider { - border-bottom: 2px solid $brand; - color: $brand; - margin-bottom: $line-height; + border-bottom: 1px solid #eee; + color: #000; + margin: $line-height 0; + + span { + border-bottom: 1px solid #000; + } } .poll-question { diff --git a/app/controllers/officing/voters_controller.rb b/app/controllers/officing/voters_controller.rb index dee1e00bd..a5343f83d 100644 --- a/app/controllers/officing/voters_controller.rb +++ b/app/controllers/officing/voters_controller.rb @@ -12,7 +12,8 @@ class Officing::VotersController < Officing::BaseController @voter = Poll::Voter.new(document_type: @user.document_type, document_number: @user.document_number, user: @user, - poll: @poll) + poll: @poll, + origin: "booth") @voter.save! end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index bfccc5d52..1bc8d5e0c 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -45,7 +45,7 @@ module Abilities can [:read, :update, :valuate, :destroy, :summary], SpendingProposal - can [:index, :read, :new, :create, :update, :destroy, :calculate_winners], Budget + can [:index, :read, :new, :create, :update, :destroy, :calculate_winners, :read_results], Budget can [:read, :create, :update, :destroy], Budget::Group can [:read, :create, :update, :destroy], Budget::Heading can [:hide, :update, :toggle_selection], Budget::Investment diff --git a/app/models/poll.rb b/app/models/poll.rb index 091c7fbb4..624cfc704 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -59,6 +59,10 @@ class Poll < ActiveRecord::Base voters.where(document_number: document_number, document_type: document_type).exists? end + def voted_in_booth?(user) + Poll::Voter.where(poll: self, user: user, origin: "booth").exists? + end + def date_range unless starts_at.present? && ends_at.present? && starts_at <= ends_at errors.add(:starts_at, I18n.t('errors.messages.invalid_date_range')) diff --git a/app/models/poll/answer.rb b/app/models/poll/answer.rb index a1ff30f0a..01eebd553 100644 --- a/app/models/poll/answer.rb +++ b/app/models/poll/answer.rb @@ -8,12 +8,13 @@ class Poll::Answer < ActiveRecord::Base validates :question, presence: true validates :author, presence: true validates :answer, presence: true - validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }} + validates :answer, inclusion: { in: ->(a) { a.question.valid_answers }}, + unless: ->(a) { a.question.blank? } scope :by_author, ->(author_id) { where(author_id: author_id) } scope :by_question, ->(question_id) { where(question_id: question_id) } def record_voter_participation - Poll::Voter.create!(user: author, poll: poll) + Poll::Voter.find_or_create_by!(user: author, poll: poll, origin: "web") end end \ No newline at end of file diff --git a/app/models/poll/officer_assignment.rb b/app/models/poll/officer_assignment.rb index 9fcf02890..417e6a150 100644 --- a/app/models/poll/officer_assignment.rb +++ b/app/models/poll/officer_assignment.rb @@ -8,7 +8,7 @@ class Poll validates :officer_id, presence: true validates :booth_assignment_id, presence: true - validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_assignment_id] } + validates :date, presence: true delegate :poll_id, :booth_id, to: :booth_assignment diff --git a/app/models/poll/recount.rb b/app/models/poll/recount.rb index 97a909d1f..74a6fe937 100644 --- a/app/models/poll/recount.rb +++ b/app/models/poll/recount.rb @@ -1,6 +1,6 @@ class Poll::Recount < ActiveRecord::Base - VALID_ORIGINS = %w{ web booth letter } + VALID_ORIGINS = %w{web booth letter}.freeze belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :booth_assignment @@ -22,7 +22,7 @@ class Poll::Recount < ActiveRecord::Base [:white, :null, :total].each do |amount| next unless send("#{amount}_amount_changed?") && send("#{amount}_amount_was").present? - self["#{amount}_amount_log"] += ":#{send("#{amount}_amount_was").to_s}" + self["#{amount}_amount_log"] += ":#{send("#{amount}_amount_was")}" amounts_changed = true end @@ -30,7 +30,7 @@ class Poll::Recount < ActiveRecord::Base end def update_officer_author - self.officer_assignment_id_log += ":#{officer_assignment_id_was.to_s}" - self.author_id_log += ":#{author_id_was.to_s}" + self.officer_assignment_id_log += ":#{officer_assignment_id_was}" + self.author_id_log += ":#{author_id_was}" end end diff --git a/app/models/poll/shift.rb b/app/models/poll/shift.rb index d64bce9ac..4edeb26ef 100644 --- a/app/models/poll/shift.rb +++ b/app/models/poll/shift.rb @@ -5,28 +5,37 @@ class Poll validates :booth_id, presence: true validates :officer_id, presence: true - validates :date, presence: true - validates :date, uniqueness: { scope: [:officer_id, :booth_id] } + validates :date, presence: true, uniqueness: { scope: [:officer_id, :booth_id, :task] } validates :task, presence: true enum task: { vote_collection: 0, recount_scrutiny: 1 } before_create :persist_data after_create :create_officer_assignments - - def create_officer_assignments - booth.booth_assignments.each do |booth_assignment| - attrs = { officer_id: officer_id, - date: date, - booth_assignment_id: booth_assignment.id } - Poll::OfficerAssignment.create!(attrs) - end - end + before_destroy :destroy_officer_assignments def persist_data self.officer_name = officer.name self.officer_email = officer.email end + def create_officer_assignments + booth.booth_assignments.each do |booth_assignment| + attrs = { + officer_id: officer_id, + date: date, + booth_assignment_id: booth_assignment.id, + final: recount_scrutiny? + } + Poll::OfficerAssignment.create!(attrs) + end + end + + def destroy_officer_assignments + Poll::OfficerAssignment.where(booth_assignment: booth.booth_assignments, + officer: officer, + date: date, + final: recount_scrutiny?).destroy_all + end end end diff --git a/app/models/poll/voter.rb b/app/models/poll/voter.rb index 760096206..abcae7d25 100644 --- a/app/models/poll/voter.rb +++ b/app/models/poll/voter.rb @@ -1,5 +1,8 @@ class Poll class Voter < ActiveRecord::Base + + VALID_ORIGINS = %w{ web booth } + belongs_to :poll belongs_to :user belongs_to :geozone @@ -10,9 +13,13 @@ class Poll validates :user_id, presence: true validates :document_number, presence: true, uniqueness: { scope: [:poll_id, :document_type], message: :has_voted } + validates :origin, inclusion: { in: VALID_ORIGINS } before_validation :set_demographic_info, :set_document_info + scope :web, -> { where(origin: 'web') } + scope :booth, -> { where(origin: 'booth') } + def set_demographic_info return if user.blank? diff --git a/app/views/admin/poll/questions/edit.html.erb b/app/views/admin/poll/questions/edit.html.erb index 48998081a..fffc4a399 100644 --- a/app/views/admin/poll/questions/edit.html.erb +++ b/app/views/admin/poll/questions/edit.html.erb @@ -2,4 +2,6 @@

<%= t("admin.questions.edit.title") %>

-<%= render "form", form_url: admin_question_path(@question) %> \ No newline at end of file +
+ <%= render "form", form_url: admin_question_path(@question) %> +
diff --git a/app/views/admin/poll/questions/new.html.erb b/app/views/admin/poll/questions/new.html.erb index 844195920..c91b78016 100644 --- a/app/views/admin/poll/questions/new.html.erb +++ b/app/views/admin/poll/questions/new.html.erb @@ -2,4 +2,6 @@

<%= t("admin.questions.new.title") %>

-<%= render "form", form_url: admin_questions_path %> \ No newline at end of file +
+ <%= render "form", form_url: admin_questions_path %> +
diff --git a/app/views/budgets/investments/_investment.html.erb b/app/views/budgets/investments/_investment.html.erb index e663229ad..2c6269a3f 100644 --- a/app/views/budgets/investments/_investment.html.erb +++ b/app/views/budgets/investments/_investment.html.erb @@ -1,13 +1,15 @@
-
+
- <% if investment.image.present? %> - <%= image_tag investment.image_url(:thumb), alt: investment.image.title %> - <% else %> -
- <% end %> +
+ <% if investment.image.present? %> + <%= image_tag investment.image_url(:thumb), alt: investment.image.title %> + <% else %> +
+ <% end %> +
@@ -53,7 +55,7 @@ <% if investment.should_show_votes? %>
+ class="small-12 medium-3 column text-center" data-equalizer-watch> <%= render partial: '/budgets/investments/votes', locals: { investment: investment, investment_votes: investment_votes, @@ -62,7 +64,7 @@
<% elsif investment.should_show_vote_count? %>
+ class="small-12 medium-3 column text-center" data-equalizer-watch>
<%= t("budgets.investments.investment.supports", @@ -72,7 +74,7 @@
<% elsif investment.should_show_ballots? %>
+ class="small-12 medium-3 column text-center" data-equalizer-watch> <%= render partial: '/budgets/investments/ballot', locals: { investment: investment, investment_ids: investment_ids, @@ -81,11 +83,13 @@
<% elsif investment.should_show_price? %>
+ class="supports small-12 medium-3 column text-center" data-equalizer-watch>

<%= investment.formatted_price %>

+ <% else %> +
<% end %> <% end %> diff --git a/app/views/budgets/show.html.erb b/app/views/budgets/show.html.erb index 814488d9d..99e9cae3c 100644 --- a/app/views/budgets/show.html.erb +++ b/app/views/budgets/show.html.erb @@ -36,7 +36,7 @@ <% end %> <% end %> - <% if @budget.finished? %> + <% if @budget.finished? || (@budget.balloting? && can?(:read_results, @budget)) %> <%= link_to t("budgets.show.see_results"), budget_results_path(@budget, heading_id: @budget.headings.first), class: "button margin-top expanded" %> diff --git a/app/views/documents/_nested_documents.html.erb b/app/views/documents/_nested_documents.html.erb index ecbe46ce7..04fd4e8a2 100644 --- a/app/views/documents/_nested_documents.html.erb +++ b/app/views/documents/_nested_documents.html.erb @@ -20,7 +20,4 @@
"> <%= t "documents.max_documents_allowed_reached_html" %>
- -
- -
\ No newline at end of file +
diff --git a/app/views/images/_image_fields.html.erb b/app/views/images/_image_fields.html.erb index 10a4cf898..092402706 100644 --- a/app/views/images/_image_fields.html.erb +++ b/app/views/images/_image_fields.html.erb @@ -28,4 +28,5 @@
+
diff --git a/app/views/images/_nested_image.html.erb b/app/views/images/_nested_image.html.erb index a981dd190..2f65b8760 100644 --- a/app/views/images/_nested_image.html.erb +++ b/app/views/images/_nested_image.html.erb @@ -1,12 +1,10 @@ -
- <%= f.label :image, t("images.form.title") %> -

<%= imageables_note(imageable) %>

+<%= f.label :image, t("images.form.title") %> +

<%= imageables_note(imageable) %>

-
- <%= f.fields_for :image do |image_builder| %> - <%= render 'images/image_fields', f: image_builder, imageable: imageable %> - <% end %> -
+
+ <%= f.fields_for :image do |image_builder| %> + <%= render 'images/image_fields', f: image_builder, imageable: imageable %> + <% end %>
<%= link_to_add_association t('images.form.add_new_image'), f, :image, @@ -21,5 +19,3 @@ association_insertion_node: "#nested-image", association_insertion_method: "append" } %> - -
diff --git a/app/views/polls/_poll_group.html.erb b/app/views/polls/_poll_group.html.erb index e3e5c5976..9372aee17 100644 --- a/app/views/polls/_poll_group.html.erb +++ b/app/views/polls/_poll_group.html.erb @@ -33,23 +33,30 @@ <%= t("polls.index.cant_answer") %>
<% end %> -
-
-
<%= poll_dates(poll) %>
+
+
+ +
 
+ +
+
+
<% if poll.questions.count == 1 %> <% poll.questions.each do |question| %> -

<%= link_to question.title, poll %>

+

<%= link_to question.title, poll %>

+ <%= poll_dates(poll) %> <% end %> <% else %> -

<%= link_to poll.name, poll %>

-
    +

    <%= link_to poll.name, poll %>

    + <%= poll_dates(poll) %> +
      <% poll.questions.each do |question| %>
    • <%= link_to question.title, question_path(question) %>
    • <% end %>
    <% end %> <% if poll.geozones.any? %> -

    +

    <%= t("polls.index.geozone_info") %>

    <% end %> @@ -59,16 +66,18 @@ <% end %>
-
- <%= link_to poll, class: "button expanded" do %> - <% if poll.expired? %> - <%= t("polls.index.participate_button_expired") %> - <% elsif poll.incoming? %> - <%= t("polls.index.participate_button_incoming") %> - <% else %> - <%= t("polls.index.participate_button") %> +
+
+ <%= link_to poll, class: "button hollow expanded" do %> + <% if poll.expired? %> + <%= t("polls.index.participate_button_expired") %> + <% elsif poll.incoming? %> + <%= t("polls.index.participate_button_incoming") %> + <% else %> + <%= t("polls.index.participate_button") %> + <% end %> <% end %> - <% end %> +
diff --git a/app/views/polls/index.html.erb b/app/views/polls/index.html.erb index 871b04953..6877cd31d 100644 --- a/app/views/polls/index.html.erb +++ b/app/views/polls/index.html.erb @@ -6,18 +6,22 @@ <%= render "shared/section_header", i18n_namespace: "polls.index.section_header", image: "polls" %>
-
+
<%= render 'shared/filter_subnav', i18n_namespace: "polls.index" %> <% polls_by_geozone_restriction = @polls.group_by(&:geozone_restricted) %> <% if polls_by_geozone_restriction[false].present? %> -

<%= t("polls.index.no_geozone_restricted") %>

+

+ <%= t("polls.index.no_geozone_restricted") %> +

<%= render partial: 'poll_group', locals: {poll_group: polls_by_geozone_restriction[false]} %> <% end %> <% if polls_by_geozone_restriction[true].present? %> -

<%= t("polls.index.geozone_restricted") %>

+

+ <%= t("polls.index.geozone_restricted") %> +

<%= render partial: 'poll_group', locals: {poll_group: polls_by_geozone_restriction[true]} %> <% end %> diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb index 02ab70525..6895397de 100644 --- a/app/views/polls/show.html.erb +++ b/app/views/polls/show.html.erb @@ -31,8 +31,14 @@
- <% @questions.each do |question| %> - <%= render 'polls/questions/question', question: question %> + <% if @poll.voted_in_booth?(current_user) %> +
+ <%= t("polls.show.already_voted_in_booth") %> +
+ <% else %> + <% @questions.each do |question| %> + <%= render 'polls/questions/question', question: question %> + <% end %> <% end %>
diff --git a/app/views/proposals/_proposal.html.erb b/app/views/proposals/_proposal.html.erb index eb02ea316..be5ac8e17 100644 --- a/app/views/proposals/_proposal.html.erb +++ b/app/views/proposals/_proposal.html.erb @@ -3,14 +3,16 @@ data-type="proposal">
-
+
- <% if proposal.image.present? %> - <%= image_tag proposal.image_url(:thumb), alt: proposal.image.title %> - <% else %> -
- <% end %> +
+ <% if proposal.image.present? %> + <%= image_tag proposal.image_url(:thumb), alt: proposal.image.title %> + <% else %> +
+ <% end %> +
@@ -58,7 +60,7 @@
-
+
<% if proposal.successful? %>
diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb index e25bdf9f7..ade0972d8 100644 --- a/app/views/proposals/show.html.erb +++ b/app/views/proposals/show.html.erb @@ -117,6 +117,13 @@

<%= t("proposals.show.author") %>

+ <% if current_editable?(@proposal) %> + <%= link_to edit_proposal_path(@proposal), class: 'button hollow expanded' do %> + + <%= t("proposals.show.edit_proposal_link") %> + <% end %> + <% end %> + <% if author_of_proposal?(@proposal) %> <%= link_to new_proposal_notification_path(proposal_id: @proposal.id), class: 'button hollow expanded' do %> @@ -128,20 +135,13 @@ <% if can_destroy_image?(@proposal) %> <%= link_to image_path(@proposal.image, from: request.url), method: :delete, - class: 'button hollow expanded', + class: 'button hollow alert expanded', data: { confirm: t('images.actions.destroy.confirm') } do %> - + <%= t("images.remove_image") %> <% end %> <% end %> - <% if current_editable?(@proposal) %> - <%= link_to edit_proposal_path(@proposal), class: 'edit-proposal button hollow expanded' do %> - - <%= t("proposals.show.edit_proposal_link") %> - <% end %> - <% end %> -
<% end %> diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 29c81c074..67d8eef2c 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -481,6 +481,7 @@ en: help_text_1: "Voting takes place when a citizen proposal supports reaches 1% of the census with voting rights. Voting can also include questions that the City Council ask to the citizens decision." help_text_2: "To participate in the next vote you have to sign up on %{org} and verify your account. All registered voters in the city over 16 years old can vote. The results of all votes are binding on the government." show: + already_voted_in_booth: "You have already participated in a booth for this poll." dates_title: "Participation dates" cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate." signin: Sign in diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 1b1ceca21..aaa59a900 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -481,6 +481,7 @@ es: help_text_1: "Las votaciones se convocan cuando una propuesta ciudadana alcanza el 1% de apoyos del censo con derecho a voto. En las votaciones también se pueden incluir cuestiones que el Ayuntamiento somete a decisión directa de la ciudadanía." help_text_2: "Para participar en la próxima votación tienes que registrarte en %{org} y verificar tu cuenta. Pueden votar todas las personas empadronadas en la ciudad mayores de 16 años. Los resultados de todas las votaciones serán vinculantes para el gobierno." show: + already_voted_in_booth: "Ya has participado en esta votación en una urna." dates_title: "Fechas de participación" cant_answer_not_logged_in: "Necesitas %{signin} o %{signup} para participar." signin: iniciar sesión diff --git a/db/migrate/20171002103314_add_poll_shift_task_index.rb b/db/migrate/20171002103314_add_poll_shift_task_index.rb new file mode 100644 index 000000000..d15275556 --- /dev/null +++ b/db/migrate/20171002103314_add_poll_shift_task_index.rb @@ -0,0 +1,7 @@ +class AddPollShiftTaskIndex < ActiveRecord::Migration + def change + remove_index "poll_shifts", name: "index_poll_shifts_on_booth_id_and_officer_id" + add_index :poll_shifts, :task + add_index :poll_shifts, [:booth_id, :officer_id, :task], unique: true + end +end diff --git a/db/migrate/20171002121658_add_origin_to_poll_voters.rb b/db/migrate/20171002121658_add_origin_to_poll_voters.rb new file mode 100644 index 000000000..845c1b774 --- /dev/null +++ b/db/migrate/20171002121658_add_origin_to_poll_voters.rb @@ -0,0 +1,5 @@ +class AddOriginToPollVoters < ActiveRecord::Migration + def change + add_column :poll_voters, :origin, :string + end +end diff --git a/db/migrate/20171002191347_add_default_to_recount_amounts.rb b/db/migrate/20171002191347_add_default_to_recount_amounts.rb new file mode 100644 index 000000000..b90e86aae --- /dev/null +++ b/db/migrate/20171002191347_add_default_to_recount_amounts.rb @@ -0,0 +1,7 @@ +class AddDefaultToRecountAmounts < ActiveRecord::Migration + def change + change_column_default :poll_recounts, :white_amount, 0 + change_column_default :poll_recounts, :null_amount, 0 + change_column_default :poll_recounts, :total_amount, 0 + end +end diff --git a/db/migrate/20171003095936_remove_officer_assigment_composed_index.rb b/db/migrate/20171003095936_remove_officer_assigment_composed_index.rb new file mode 100644 index 000000000..874672f84 --- /dev/null +++ b/db/migrate/20171003095936_remove_officer_assigment_composed_index.rb @@ -0,0 +1,5 @@ +class RemoveOfficerAssigmentComposedIndex < ActiveRecord::Migration + def change + remove_index "poll_officer_assignments", name: "index_poll_officer_assignments_on_officer_id_and_date" + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 30159f85a..c21c35b4e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -52,6 +52,12 @@ FactoryGirl.define do trait :verified do verified_at Time.current end + + trait :in_census do + document_number "12345678Z" + document_type "1" + verified_at Time.current + end end factory :identity do @@ -525,6 +531,7 @@ FactoryGirl.define do factory :poll_voter, class: 'Poll::Voter' do poll association :user, :level_two + origin "web" trait :from_booth do association :booth_assignment, factory: :poll_booth_assignment diff --git a/spec/features/admin/poll/shifts_spec.rb b/spec/features/admin/poll/shifts_spec.rb index ceccc1867..1804735de 100644 --- a/spec/features/admin/poll/shifts_spec.rb +++ b/spec/features/admin/poll/shifts_spec.rb @@ -30,13 +30,13 @@ feature 'Admin shifts' do expect(page).to have_content officer.name end - scenario "Create Vote Collection Shift", :js do + scenario "Create Vote Collection Shift and Recount & Scrutiny Shift on same date", :js do poll = create(:poll, :current) - vote_collection_dates = (poll.starts_at.to_date..poll.ends_at.to_date).to_a.map { |date| I18n.l(date, format: :long) } - booth = create(:poll_booth) assignment = create(:poll_booth_assignment, poll: poll, booth: booth) officer = create(:poll_officer) + vote_collection_dates = (poll.starts_at.to_date..poll.ends_at.to_date).to_a.map { |date| I18n.l(date, format: :long) } + recount_scrutiny_dates = (poll.ends_at.to_date..poll.ends_at.to_date + 1.week).to_a.map { |date| I18n.l(date, format: :long) } visit available_admin_booths_path @@ -61,15 +61,6 @@ feature 'Admin shifts' do expect(page).to have_content("Collect Votes") expect(page).to have_content(officer.name) end - end - - scenario "Create Recount & Scrutiny Shift", :js do - poll = create(:poll, :current) - recount_scrutiny_dates = (poll.ends_at.to_date..poll.ends_at.to_date + 1.week).to_a.map { |date| I18n.l(date, format: :long) } - - booth = create(:poll_booth) - assignment = create(:poll_booth_assignment, poll: poll, booth: booth) - officer = create(:poll_officer) visit available_admin_booths_path @@ -91,7 +82,7 @@ feature 'Admin shifts' do expect(page).to have_content "Shift added" within("#shifts") do - expect(page).to have_css(".shift", count: 1) + expect(page).to have_css(".shift", count: 2) expect(page).to have_content(I18n.l(poll.ends_at.to_date + 4.days, format: :long)) expect(page).to have_content("Recount & Scrutiny") expect(page).to have_content(officer.name) diff --git a/spec/features/home_spec.rb b/spec/features/home_spec.rb index 8b9cf3e4e..cb5349814 100644 --- a/spec/features/home_spec.rb +++ b/spec/features/home_spec.rb @@ -22,11 +22,6 @@ feature "Home" do feature "For signed in users" do - before do - # user = create(:user) - # login_as(user) - end - feature "Recommended" do background do diff --git a/spec/features/polls/polls_spec.rb b/spec/features/polls/polls_spec.rb index 047c71146..8a90fb4a8 100644 --- a/spec/features/polls/polls_spec.rb +++ b/spec/features/polls/polls_spec.rb @@ -184,6 +184,7 @@ feature 'Polls' do poll.geozones << geozone create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca') user = create(:user, :level_two, geozone: geozone) + login_as user visit poll_path(poll) @@ -193,5 +194,25 @@ feature 'Polls' do expect(page).to have_link('Chewbacca') end + scenario 'Level 2 users changing answer', :js do + poll.update(geozone_restricted: true) + poll.geozones << geozone + create(:poll_question, poll: poll, 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') + + click_link 'Chewbacca' + + expect(page).to_not have_link('Chewbacca') + expect(page).to have_link('Han Solo') + end + end end diff --git a/spec/features/polls/voter_spec.rb b/spec/features/polls/voter_spec.rb new file mode 100644 index 000000000..08bc6f963 --- /dev/null +++ b/spec/features/polls/voter_spec.rb @@ -0,0 +1,94 @@ +require 'rails_helper' + +feature "Voter" do + + context "Origin" do + + scenario "Voting via web", :js do + poll = create(:poll) + question = create(:poll_question, poll: poll, valid_answers: 'Yes, No') + user = create(:user, :level_two) + + login_as user + visit question_path(question) + + click_link 'Answer this question' + click_link 'Yes' + + expect(page).to_not have_link('Yes') + expect(Poll::Voter.count).to eq(1) + expect(Poll::Voter.first.origin).to eq("web") + end + + scenario "Voting in booth", :js do + user = create(:user, :in_census) + create(:geozone, :in_census) + + poll = create(:poll) + officer = create(:poll_officer) + + ba = create(:poll_booth_assignment, poll: poll) + create(:poll_officer_assignment, officer: officer, booth_assignment: ba) + + login_through_form_as_officer(officer.user) + + visit new_officing_residence_path + officing_verify_residence + + expect(page).to have_content poll.name + + first(:button, "Confirm vote").click + expect(page).to have_content "Vote introduced!" + + expect(Poll::Voter.count).to eq(1) + expect(Poll::Voter.first.origin).to eq("booth") + end + + context "Trying to vote the same poll in booth and web" do + + let(:poll) { create(:poll) } + let(:question) { create(:poll_question, poll: poll, valid_answers: 'Yes, No') } + let!(:user) { create(:user, :in_census) } + + let(:officer) { create(:poll_officer) } + let(:ba) { create(:poll_booth_assignment, poll: poll) } + let!(:oa) { create(:poll_officer_assignment, officer: officer, booth_assignment: ba) } + + scenario "Trying to vote in web and then in booth", :js do + login_as user + vote_for_poll_via_web + + click_link "Sign out" + + login_through_form_as_officer(officer.user) + + visit new_officing_residence_path + officing_verify_residence + + expect(page).to have_content poll.name + expect(page).to_not have_button "Confirm vote" + expect(page).to have_content "Has already participated in this poll" + end + + scenario "Trying to vote in booth and then in web", :js do + login_through_form_as_officer(officer.user) + + vote_for_poll_via_booth + + visit root_path + click_link "Sign out" + + login_as user + visit question_path(question) + + click_link 'Answer this question' + + expect(page).to_not have_link('Yes') + expect(page).to have_content "You have already participated in a booth for this poll." + expect(Poll::Voter.count).to eq(1) + end + end + + end + +end \ No newline at end of file diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb index 5a6c4b734..0934c7cfe 100644 --- a/spec/models/abilities/administrator_spec.rb +++ b/spec/models/abilities/administrator_spec.rb @@ -72,6 +72,7 @@ describe "Abilities::Administrator" do it { should be_able_to(:create, Budget) } it { should be_able_to(:update, Budget) } + it { should be_able_to(:read_results, Budget) } it { should be_able_to(:create, Budget::ValuatorAssignment) } diff --git a/spec/models/abilities/everyone_spec.rb b/spec/models/abilities/everyone_spec.rb index fcfff4e42..21568c999 100644 --- a/spec/models/abilities/everyone_spec.rb +++ b/spec/models/abilities/everyone_spec.rb @@ -8,6 +8,9 @@ describe "Abilities::Everyone" do let(:debate) { create(:debate) } let(:proposal) { create(:proposal) } + let(:reviewing_ballot_budget) { create(:budget, phase: 'reviewing_ballots') } + let(:finished_budget) { create(:budget, phase: 'finished') } + it { should be_able_to(:index, Debate) } it { should be_able_to(:show, debate) } it { should_not be_able_to(:edit, Debate) } @@ -28,4 +31,7 @@ describe "Abilities::Everyone" do it { should_not be_able_to(:create, SpendingProposal) } it { should be_able_to(:index, Budget) } -end \ No newline at end of file + + it { should be_able_to(:read_results, finished_budget) } + it { should_not be_able_to(:read_results, reviewing_ballot_budget) } +end diff --git a/spec/models/poll/answer_spec.rb b/spec/models/poll/answer_spec.rb index 10ccafbb6..d66cdc18c 100644 --- a/spec/models/poll/answer_spec.rb +++ b/spec/models/poll/answer_spec.rb @@ -3,28 +3,71 @@ require 'rails_helper' describe Poll::Answer do describe "validations" do - it "validates that the answers are included in the Poll::Question's list" do - q = create(:poll_question, valid_answers: 'One, Two, Three') - expect(build(:poll_answer, question: q, answer: 'One')).to be_valid - expect(build(:poll_answer, question: q, answer: 'Two')).to be_valid - expect(build(:poll_answer, question: q, answer: 'Three')).to be_valid - expect(build(:poll_answer, question: q, answer: 'Four')).to_not be_valid + let(:answer) { build(:poll_answer) } + + it "should be valid" do + expect(answer).to be_valid + end + + it "should not be valid wihout a question" do + answer.question = nil + expect(answer).to_not be_valid + end + + it "should not be valid without an author" do + answer.author = nil + expect(answer).to_not be_valid + end + + it "should not be valid without an answer" do + answer.answer = nil + expect(answer).to_not be_valid + end + + it "should be valid for answers included in the Poll::Question's list" do + question = create(:poll_question, valid_answers: 'One, Two, Three') + expect(build(:poll_answer, question: question, answer: 'One')).to be_valid + expect(build(:poll_answer, question: question, answer: 'Two')).to be_valid + expect(build(:poll_answer, question: question, answer: 'Three')).to be_valid + + expect(build(:poll_answer, question: question, answer: 'Four')).to_not be_valid end end describe "#record_voter_participation" do + + let(:author) { create(:user, :level_two) } + let(:poll) { create(:poll) } + let(:question) { create(:poll_question, poll: poll, valid_answers: "Yes, No") } + it "creates a poll_voter with user and poll data" do - answer = create(:poll_answer) + answer = create(:poll_answer, question: question, author: author, answer: "Yes") expect(answer.poll.voters).to be_blank answer.record_voter_participation - expect(answer.poll.reload.voters.size).to eq(1) - voter = answer.poll.voters.first + expect(poll.reload.voters.size).to eq(1) + voter = poll.voters.first expect(voter.document_number).to eq(answer.author.document_number) expect(voter.poll_id).to eq(answer.poll.id) end + + it "updates a poll_voter with user and poll data" do + answer = create(:poll_answer, question: question, author: author, answer: "Yes") + answer.record_voter_participation + + expect(poll.reload.voters.size).to eq(1) + + answer = create(:poll_answer, question: question, author: author, answer: "No") + answer.record_voter_participation + + expect(poll.reload.voters.size).to eq(1) + + voter = poll.voters.first + expect(voter.document_number).to eq(answer.author.document_number) + expect(voter.poll_id).to eq(answer.poll.id) + end end end diff --git a/spec/models/poll/poll_spec.rb b/spec/models/poll/poll_spec.rb index ac0cc44ea..5083d7439 100644 --- a/spec/models/poll/poll_spec.rb +++ b/spec/models/poll/poll_spec.rb @@ -138,4 +138,33 @@ describe :poll do end end end + + describe "#voted_in_booth?" do + + it "returns true if the user has already voted in booth" do + user = create(:user, :level_two) + poll = create(:poll) + + create(:poll_voter, poll: poll, user: user, origin: "booth") + + expect(poll.voted_in_booth?(user)).to be + end + + it "returns false if the user has not already voted in a booth" do + user = create(:user, :level_two) + poll = create(:poll) + + expect(poll.voted_in_booth?(user)).to_not be + end + + it "returns false if the user has voted in web" do + user = create(:user, :level_two) + poll = create(:poll) + + create(:poll_voter, poll: poll, user: user, origin: "web") + + expect(poll.voted_in_booth?(user)).to_not be + end + + end end diff --git a/spec/models/poll/recount_spec.rb b/spec/models/poll/recount_spec.rb index 1ca0b41eb..2a5ed16a7 100644 --- a/spec/models/poll/recount_spec.rb +++ b/spec/models/poll/recount_spec.rb @@ -3,7 +3,9 @@ require 'rails_helper' describe Poll::Recount do describe "logging changes" do - let(:poll_recount) { create(:poll_recount) } + let(:author) { create(:user) } + let(:officer_assignment) { create(:poll_officer_assignment) } + let(:poll_recount) { create(:poll_recount, author: author, officer_assignment: officer_assignment) } it "should update white_amount_log if white_amount changes" do poll_recount.white_amount = 33 @@ -17,7 +19,7 @@ describe Poll::Recount do poll_recount.white_amount = 34 poll_recount.save - expect(poll_recount.white_amount_log).to eq(":33:32") + expect(poll_recount.white_amount_log).to eq(":0:33:32") end it "should update null_amount_log if null_amount changes" do @@ -32,7 +34,7 @@ describe Poll::Recount do poll_recount.null_amount = 34 poll_recount.save - expect(poll_recount.null_amount_log).to eq(":33:32") + expect(poll_recount.null_amount_log).to eq(":0:33:32") end it "should update total_amount_log if total_amount changes" do @@ -47,7 +49,7 @@ describe Poll::Recount do poll_recount.total_amount = 34 poll_recount.save - expect(poll_recount.total_amount_log).to eq(":33:32") + expect(poll_recount.total_amount_log).to eq(":0:33:32") end it "should update officer_assignment_id_log if amount changes" do @@ -68,8 +70,8 @@ describe Poll::Recount do poll_recount.officer_assignment = create(:poll_officer_assignment, id: 103) poll_recount.save - expect(poll_recount.white_amount_log).to eq(":33:32") - expect(poll_recount.officer_assignment_id_log).to eq(":101:102") + expect(poll_recount.white_amount_log).to eq(":0:33:32") + expect(poll_recount.officer_assignment_id_log).to eq(":#{officer_assignment.id}:101:102") end it "should update author_id if amount changes" do @@ -78,24 +80,24 @@ describe Poll::Recount do expect(poll_recount.white_amount_log).to eq("") expect(poll_recount.author_id_log).to eq("") - author_A = create(:poll_officer).user - author_B = create(:poll_officer).user - author_C = create(:poll_officer).user + first_author = create(:poll_officer).user + second_author = create(:poll_officer).user + third_author = create(:poll_officer).user poll_recount.white_amount = 33 - poll_recount.author_id = author_A.id + poll_recount.author_id = first_author.id poll_recount.save! poll_recount.white_amount = 32 - poll_recount.author_id = author_B.id + poll_recount.author_id = second_author.id poll_recount.save! poll_recount.white_amount = 34 - poll_recount.author_id = author_C.id + poll_recount.author_id = third_author.id poll_recount.save! - expect(poll_recount.white_amount_log).to eq(":33:32") - expect(poll_recount.author_id_log).to eq(":#{author_A.id}:#{author_B.id}") + expect(poll_recount.white_amount_log).to eq(":0:33:32") + expect(poll_recount.author_id_log).to eq(":#{author.id}:#{first_author.id}:#{second_author.id}") end end diff --git a/spec/models/poll/shift_spec.rb b/spec/models/poll/shift_spec.rb index b7918b95c..b62f220da 100644 --- a/spec/models/poll/shift_spec.rb +++ b/spec/models/poll/shift_spec.rb @@ -28,7 +28,7 @@ describe :shift do describe "officer_assignments" do - it "should create corresponding officer_assignments" do + it "should create and destroy corresponding officer_assignments" do poll1 = create(:poll) poll2 = create(:poll) poll3 = create(:poll) @@ -39,21 +39,45 @@ describe :shift do booth_assignment1 = create(:poll_booth_assignment, poll: poll1, booth: booth) booth_assignment2 = create(:poll_booth_assignment, poll: poll2, booth: booth) - shift = create(:poll_shift, booth: booth, officer: officer, date: Date.current) + expect { create(:poll_shift, booth: booth, officer: officer, date: Date.current) }.to change {Poll::OfficerAssignment.all.count}.by(2) officer_assignments = Poll::OfficerAssignment.all - expect(officer_assignments.count).to eq(2) - oa1 = officer_assignments.first oa2 = officer_assignments.second expect(oa1.officer).to eq(officer) expect(oa1.date).to eq(Date.current) expect(oa1.booth_assignment).to eq(booth_assignment1) + expect(oa1.final).to be_falsey expect(oa2.officer).to eq(officer) expect(oa2.date).to eq(Date.current) expect(oa2.booth_assignment).to eq(booth_assignment2) + expect(oa2.final).to be_falsey + + create(:poll_officer_assignment, officer: officer, booth_assignment: booth_assignment1, date: Date.tomorrow) + + expect { Poll::Shift.last.destroy }.to change {Poll::OfficerAssignment.all.count}.by(-2) + end + + it "should create final officer_assignments" do + poll = create(:poll) + booth = create(:poll_booth) + officer = create(:poll_officer) + + booth_assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + + shift = create(:poll_shift, booth: booth, officer: officer, date: Date.current, task: :recount_scrutiny) + + officer_assignments = Poll::OfficerAssignment.all + expect(officer_assignments.count).to eq(1) + + officer_assignment = officer_assignments.first + + expect(officer_assignment.officer).to eq(officer) + expect(officer_assignment.date).to eq(Date.current) + expect(officer_assignment.booth_assignment).to eq(booth_assignment) + expect(officer_assignment.final).to be_truthy end end diff --git a/spec/models/poll/voter_spec.rb b/spec/models/poll/voter_spec.rb index c1c248550..f306248dc 100644 --- a/spec/models/poll/voter_spec.rb +++ b/spec/models/poll/voter_spec.rb @@ -83,6 +83,64 @@ describe :voter do expect(voter.errors.messages[:document_number]).to eq(["User has already voted"]) end + context "origin" do + + it "should not be valid without an origin" do + voter.origin = nil + expect(voter).to_not be_valid + end + + it "should not be valid without a valid origin" do + voter.origin = "invalid_origin" + expect(voter).to_not be_valid + end + + it "should be valid with a booth origin" do + voter.origin = "booth" + expect(voter).to be_valid + end + + it "should be valid with a web origin" do + voter.origin = "web" + expect(voter).to be_valid + end + + end + + end + + describe "scopes" do + + describe "#web" do + it "returns voters with a web origin" do + voter1 = create(:poll_voter, origin: "web") + voter2 = create(:poll_voter, origin: "web") + voter3 = create(:poll_voter, origin: "booth") + + web_voters = Poll::Voter.web + + expect(web_voters.count).to eq(2) + expect(web_voters).to include(voter1) + expect(web_voters).to include(voter2) + expect(web_voters).to_not include(voter3) + end + end + + describe "#booth" do + it "returns voters with a booth origin" do + voter1 = create(:poll_voter, origin: "booth") + voter2 = create(:poll_voter, origin: "booth") + voter3 = create(:poll_voter, origin: "web") + + booth_voters = Poll::Voter.booth + + expect(booth_voters.count).to eq(2) + expect(booth_voters).to include(voter1) + expect(booth_voters).to include(voter2) + expect(booth_voters).to_not include(voter3) + end + end + end describe "save" do diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 8b8470293..97f05294c 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -24,6 +24,17 @@ module CommonActions click_button 'Enter' end + def login_through_form_as_officer(user) + visit root_path + click_link 'Sign in' + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: user.password + + click_button 'Enter' + visit new_officing_residence_path + end + def login_as_authenticated_manager expected_response = {login: login, user_key: user_key, date: date}.with_indifferent_access login, user_key, date = "JJB042", "31415926", Time.current.strftime("%Y%m%d%H%M%S") @@ -287,4 +298,26 @@ module CommonActions end end + def vote_for_poll_via_web + visit question_path(question) + + click_link 'Answer this question' + click_link 'Yes' + + expect(page).to_not have_link('Yes') + expect(Poll::Voter.count).to eq(1) + end + + def vote_for_poll_via_booth + visit new_officing_residence_path + officing_verify_residence + + expect(page).to have_content poll.name + + first(:button, "Confirm vote").click + expect(page).to have_content "Vote introduced!" + + expect(Poll::Voter.count).to eq(1) + end + end