From 9d67656f44d1b35c296d443ff9c1727fc4b68cf8 Mon Sep 17 00:00:00 2001 From: kiara Date: Fri, 27 Jan 2023 14:47:10 +0000 Subject: [PATCH] add voting portal (#7) de huidige opzet hier houdt in dat de tokens van leden voor een vergadering, voorheen alleen gebruikt voor hun persoonlijke links tot de digitale vergadering (die hun naam instelt in BigBlueButton en ze als aanwezig markeerde voor stemmingen), nu ook gebruikt worden voor de link naar de stem portaal. implicatie hiervan is dat Ingang admins zo ook toegang krijgen tot de stem credentials, in plaats van enkel de systeembeheerders met toegang tot de Helios database. waar die kennis iemand wel in staat stelt om namens een ander te stemmen, wordt dit risico in check gehouden doordat dit niet ongemerkt gaat: na het uitbrengen van een stem wordt er naar het bijbehorende email adres een bevestiging gestuurd. het risico bij anderszins kwaadwillenden die de info verkrijgen is hetzelfde; gebruik ervan gaat niet ongemerkt. waar het mogelijk zou zijn om van de bestaande token af te wijken voor dit doeleinde naar een hash niet zichtbaar voor Ingang admins, zou dit ook nadelen hebben. af en toe hebben we leden voor wie de mailtjes uit Ingang niet makkelijk aankomen, in welk geval we nu handmatig een inlog link kunnen doorgeven. mochten de nodige info hiervoor niet meer beschikbaar zijn voor degene die een ALV technisch runt, dan worden juist dit soort situaties weer lastiger. Co-authored-by: Kiara Grouwstra Reviewed-on: https://code.bij1.org/bij1/ingang/pulls/7 --- app/controllers/main_controller.rb | 13 ++- app/controllers/users_controller.rb | 13 ++- app/controllers/votes_controller.rb | 81 +++++++++++++++++++ app/models/user.rb | 3 + app/models/vote.rb | 6 ++ app/views/main/votes.html.erb | 18 +++++ app/views/rooms/show.html.erb | 2 + app/views/users/bulk.html.erb | 2 +- app/views/users/index.html.erb | 2 + app/views/votes/bulk.html.erb | 46 +++++++++++ app/views/votes/index.html.erb | 29 +++++++ config/application.yml.bck | 4 + config/database.yml | 34 ++++---- config/routes.rb | 12 ++- db/migrate/20230123193409_create_votes.rb | 16 ++++ db/migrate/20230127133700_upsertable_users.rb | 8 ++ public/votes.csv | 1 + test/fixtures/votes.yml | 17 ++++ test/models/vote_test.rb | 7 ++ 19 files changed, 285 insertions(+), 29 deletions(-) create mode 100644 app/controllers/votes_controller.rb create mode 100644 app/models/vote.rb create mode 100644 app/views/main/votes.html.erb create mode 100644 app/views/votes/bulk.html.erb create mode 100644 app/views/votes/index.html.erb create mode 100644 db/migrate/20230123193409_create_votes.rb create mode 100644 db/migrate/20230127133700_upsertable_users.rb create mode 100644 public/votes.csv create mode 100644 test/fixtures/votes.yml create mode 100644 test/models/vote_test.rb diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 916eb55..8409009 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,5 +1,5 @@ class MainController < ApplicationController - before_action :set_user_room, only: [:join, :users, :stream] + before_action :set_user_room, only: [:join, :users, :stream, :stemmen] def index end @@ -32,10 +32,19 @@ class MainController < ApplicationController end end + def stemmen + @votes = Vote.where(room_id: @room.id, user_id: @user.id).order(created_at: :desc) + render :votes + end + private def set_user_room @user = User.find_by token: params[:token] - @room = Room.find(@user.room_id) + if @user.nil? + redirect_to 'https://bij1.org/', status: :unauthorized + else + @room = Room.find(@user.room_id) + end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 47a43fe..b937c1f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -130,6 +130,9 @@ class UsersController < ApplicationController # DELETE /users/1 # DELETE /users/1.json def destroy + Vote.where(user_id: @user.id).each do |vote| + vote.destroy + end @user.destroy respond_to do |format| format.html { redirect_to room_users_url(@user.room_id), notice: 'User was successfully destroyed.' } @@ -141,6 +144,9 @@ class UsersController < ApplicationController # DELETE /rooms/1/users.json def destroy_all @users.each do |user| + Vote.where(user_id: user.id).each do |vote| + vote.destroy + end user.destroy end respond_to do |format| @@ -162,11 +168,12 @@ class UsersController < ApplicationController require 'csv' - CSV.parse(users_csv, :headers => true) do |row| + users = CSV.parse(users_csv, :headers => true).map { |row| fields = row.to_hash fields[:room_id] = room_id - User.create!(fields) - end + fields + } + User.upsert_all(users, unique_by: [:room_id, :email]) respond_to do |format| format.html { redirect_to room_users_url(room_id), notice: 'Users were successfully created.' } diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb new file mode 100644 index 0000000..ef34536 --- /dev/null +++ b/app/controllers/votes_controller.rb @@ -0,0 +1,81 @@ +class VotesController < ApplicationController + http_basic_authenticate_with name: Rails.application.config.admin_name, + password: Rails.application.config.admin_password + + before_action :set_room, only: [:index, :bulk, :destroy_for_room] + before_action :set_votes, only: [:index, :bulk, :destroy_for_room] + + # GET /rooms/:room_id/votes + # GET /rooms/:room_id/votes.json + def index + respond_to do |format| + format.html { render :index } + # let's protect voter credentials + format.json { render json: [], status: :unauthorized } + end + end + + # GET /rooms/:room_id/votes/bulk + def bulk + @sample = "info@bij1.org,mijn-stemming,1,abcdABCD1234" + end + + # POST /rooms/:room_id/votes/bulk + # POST /rooms/:room_id/votes/bulk.json + def create_bulk + room_id = params[:room_id] + votes_csv = params[:votes_csv] + + require 'csv' + + headers = %i[ + voter_email + short_name + voter_login_id + voter_password + ] + votes = CSV.parse(votes_csv, headers: headers).map { |row| + csv_fields = row.to_hash + email = csv_fields[:voter_email] + user = User.find_by(room_id: room_id, email: email) + { + :room_id => room_id, + :user_id => user.id, + :election_slug => csv_fields[:short_name], + :voter_login_id => csv_fields[:voter_login_id], + :voter_password => csv_fields[:voter_password], + } + } + Vote.upsert_all(votes, unique_by: [:user_id, :election_slug]) + + respond_to do |format| + format.html { redirect_to room_users_url(room_id), notice: 'Votes were successfully created.' } + format.json { render :show, status: :created, location: @room } + end + end + + # DELETE /rooms/:room_id/votes + # DELETE /rooms/:room_id/votes.json + def destroy_for_room + @votes.each do |vote| + vote.destroy + end + + respond_to do |format| + format.html { redirect_to room_users_url(@room.id), notice: 'Votes were successfully destroyed.' } + format.json { render :show, status: :created, location: @room } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + + def set_room + @room = Room.find(params[:room_id]) + end + + def set_votes + @votes = Vote.where(room_id: @room.id) + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index 47e474e..a95f14f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,4 +6,7 @@ class User < ApplicationRecord attribute :invited, :boolean, default: false attribute :vote, :boolean, default: true attribute :proxy, :boolean, default: false + + validates :email, uniqueness: { scope: :room_id } + validates :room_id, uniqueness: { scope: :email } end diff --git a/app/models/vote.rb b/app/models/vote.rb new file mode 100644 index 0000000..bafd553 --- /dev/null +++ b/app/models/vote.rb @@ -0,0 +1,6 @@ +class Vote < ApplicationRecord + belongs_to :user + + validates :election_slug, uniqueness: { scope: :user_id } + validates :user_id, uniqueness: { scope: :election_slug } +end diff --git a/app/views/main/votes.html.erb b/app/views/main/votes.html.erb new file mode 100644 index 0000000..fdd44c3 --- /dev/null +++ b/app/views/main/votes.html.erb @@ -0,0 +1,18 @@ +

<%= notice %>

+ +

Stemmingen

+ + + +
+ +<%= link_to 'Stream', user_stream_path %> | +<%= link_to 'Inbellen', join_room_path %> via BigBlueButton (voor toelichtingen) diff --git a/app/views/rooms/show.html.erb b/app/views/rooms/show.html.erb index 3b1cbc1..f155440 100644 --- a/app/views/rooms/show.html.erb +++ b/app/views/rooms/show.html.erb @@ -32,5 +32,7 @@ <%= link_to 'Show Users', room_users_path(@room) %> | <%= link_to 'Edit', edit_room_path(@room) %> | +<%= link_to 'Import Users', bulk_new_room_users_path(@room), method: :get %> | +<%= link_to 'Import Votes', bulk_new_room_votes_path(@room), method: :get %> | <%= link_to 'Present Users', room_present_users_path(@room) %> | <%= link_to 'Back', rooms_path %> diff --git a/app/views/users/bulk.html.erb b/app/views/users/bulk.html.erb index 18efb11..85c5e17 100644 --- a/app/views/users/bulk.html.erb +++ b/app/views/users/bulk.html.erb @@ -1,7 +1,7 @@

Bulk Users: <%= @room.name %>

-csv formaat voor bulk import: email,name,moderator,vote,proxy,invited,presence. kolom volgorde maakt niet uit. komma-separated met header. +csv formaat voor bulk import: email,name,moderator,vote,proxy,invited,presence. kolom volgorde maakt niet uit. komma-separated met header. <% if false %> download een sample <%= link_to "hier", "/bbb.csv" %>. <% end %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 280828e..44a8eab 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -50,4 +50,6 @@ <%= link_to 'Present Users', room_present_users_path(id: params[:room_id], v: 1) %> | <%= link_to 'Absent Users', room_present_users_path(id: params[:room_id], v: 0) %> | <%= link_to 'Destroy all', destroy_room_users_path(params[:room_id]), method: :delete %> | +<%= link_to 'Import Votes', bulk_new_room_votes_path(params[:room_id]), method: :get %> | +<%= link_to 'Destroy Votes', room_votes_path(params[:room_id]), method: :delete %> | <%= link_to 'Back', room_path(params[:room_id]) %> diff --git a/app/views/votes/bulk.html.erb b/app/views/votes/bulk.html.erb new file mode 100644 index 0000000..17f7718 --- /dev/null +++ b/app/views/votes/bulk.html.erb @@ -0,0 +1,46 @@ +

Bulk Votes: <%= @room.name %>

+ +

+csv formaat voor bulk import, in die volgorde, komma-separated en zonder header: voter_email,short_name,voter_login_id,voter_password +<% if false %> +download een sample <%= link_to "hier", "/votes.csv" %>. +<% end %> +

+ + + +

+De gegevens hiervoor zijn te verkrijgen via commando, gegeven SSH toegang tot de database server: +

+ +

+... waar 'UUID1', 'UUID2', ... vervangen dient te worden door de UUIDs van de stemmingen van deze kamer, in de URLs te vinden in Helios. +

+

+Gebruikers vinden vervolgens hun stem links op de volgende URL, waar TOKEN dient te worden vervangen door hun Ingang token: +

+ + +<%= form_with(local: true) do |form| %> +
+ <%= form.text_area :votes_csv, value: @sample %> +
+ +
+ <%= form.submit %> +
+ <%= link_to 'Back', room_users_path %> +<% end %> diff --git a/app/views/votes/index.html.erb b/app/views/votes/index.html.erb new file mode 100644 index 0000000..332155c --- /dev/null +++ b/app/views/votes/index.html.erb @@ -0,0 +1,29 @@ +

<%= notice %>

+ +

Votes: <%= @room.name %>

+ + + + + + + + + + + + + <% @votes.each do |vote| %> + + + + + + + <% end %> + +
IDUser IDElection SlugLogin ID
<%= vote.id %><%= vote.user_id %><%= vote.election_slug %><%= vote.voter_login_id %>
+ +
+ +<%= link_to 'Back', room_path(@room) %> diff --git a/config/application.yml.bck b/config/application.yml.bck index 0454273..c6cb762 100644 --- a/config/application.yml.bck +++ b/config/application.yml.bck @@ -1,4 +1,8 @@ +RAILS_LOG_TO_STDOUT: 'any value will do, it just cares that this env var is present' SMTP_USERNAME: "" SMTP_PASSWORD: "" INGANG_ADMIN_PASSWORD: "" BIGBLUEBUTTON_SECRET: "" +DATABASE_PASSWORD_PRODUCTION: "" +DATABASE_PASSWORD_TEST: "" +DATABASE_PASSWORD_DEV: "" diff --git a/config/database.yml b/config/database.yml index 316f61e..857c61c 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,35 +1,29 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -# default: &default pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 + # upsert does not work with sqlite + adapter: postgresql + encoding: unicode + host: db.internal.bij1.net + port: 5432 development: <<: *default - adapter: sqlite3 - database: db/development.sqlite3 + database: ingang_dev + username: ingang_dev + password: <%= ENV.fetch("DATABASE_PASSWORD_DEV") { } %> # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default - adapter: sqlite3 - database: db/test.sqlite3 + database: ingang_test + username: ingang_test + password: <%= ENV.fetch("DATABASE_PASSWORD_TEST") { } %> production: <<: *default - adapter: sqlite3 - database: db/production.sqlite3 - # adapter: postgresql - # encoding: unicode - # database: ingang - # username: ingang - # password: <%= ENV.fetch("DATABASE_PASSWORD") { } %> - # host: db.internal.bij1.net - # port: 5432 - + database: ingang + username: ingang + password: <%= ENV.fetch("DATABASE_PASSWORD_PRODUCTION") { } %> diff --git a/config/routes.rb b/config/routes.rb index ab0be58..483ce86 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,18 +1,24 @@ Rails.application.routes.draw do - resources :rooms do - resources :users, shallow: true + resources :rooms, shallow: true do + resources :users do + # resources :votes + end end get 'rooms/:room_id/users/bulk', to: 'users#bulk', as: 'bulk_new_room_users' post 'rooms/:room_id/users/bulk', to: 'users#create_bulk', as: 'bulk_create_room_users' post 'rooms/:room_id/users/:id/test_invite', to: 'users#test_invite', as: 'test_invite_user' post 'rooms/:room_id/users/invite', to: 'users#invite', as: 'invite_room_users' delete 'rooms/:room_id/users', to: 'users#destroy_all', as: 'destroy_room_users' + delete 'rooms/:room_id/votes', to: 'votes#destroy_for_room', as: 'room_votes' delete 'rooms/:room_id/users/invite', to: 'users#uninvite', as: 'uninvite_room_users' post 'rooms/:room_id/users/mark_invited', to: 'users#mark_invited', as: 'mark_invited_room_users' post 'rooms/:room_id/users/mark_presence', to: 'users#mark_presence', as: 'mark_presence_room_users' + get 'rooms/:room_id/votes/bulk', to: 'votes#bulk', as: 'bulk_new_room_votes' + post 'rooms/:room_id/votes/bulk', to: 'votes#create_bulk', as: 'bulk_create_room_votes' get 'rooms/:id/voters.csv', to: 'rooms#voters', as: 'room_export_voters' get 'rooms/:id/aanwezig.csv', to: 'rooms#present', as: 'room_present_users' - get ':token/stream', to: 'main#stream' + get ':token/stemmen', to: 'main#stemmen', as: 'user_elections' + get ':token/stream', to: 'main#stream', as: 'user_stream' get ':token', to: 'main#join', as: 'join_room' root 'main#index' end diff --git a/db/migrate/20230123193409_create_votes.rb b/db/migrate/20230123193409_create_votes.rb new file mode 100644 index 0000000..ca195d1 --- /dev/null +++ b/db/migrate/20230123193409_create_votes.rb @@ -0,0 +1,16 @@ +class CreateVotes < ActiveRecord::Migration[6.0] + def change + create_table :votes do |t| + t.references :room, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + t.string :election_slug + t.string :voter_login_id + t.string :voter_password + + t.timestamps + end + change_column_default :votes, :created_at, from: nil, to: ->{ 'now()' } + change_column_default :votes, :updated_at, from: nil, to: ->{ 'now()' } + add_index :votes, [:user_id, :election_slug], unique: true + end +end diff --git a/db/migrate/20230127133700_upsertable_users.rb b/db/migrate/20230127133700_upsertable_users.rb new file mode 100644 index 0000000..01286f7 --- /dev/null +++ b/db/migrate/20230127133700_upsertable_users.rb @@ -0,0 +1,8 @@ +class UpsertableUsers < ActiveRecord::Migration[6.0] + def change + change_column_default :users, :created_at, from: nil, to: ->{ 'now()' } + change_column_default :users, :updated_at, from: nil, to: ->{ 'now()' } + add_index :users, [:room_id, :email], unique: true + end + end + \ No newline at end of file diff --git a/public/votes.csv b/public/votes.csv new file mode 100644 index 0000000..9038b0f --- /dev/null +++ b/public/votes.csv @@ -0,0 +1 @@ +info@bij1.org,mijn-stemming,1,abcdABCD1234 \ No newline at end of file diff --git a/test/fixtures/votes.yml b/test/fixtures/votes.yml new file mode 100644 index 0000000..7062be5 --- /dev/null +++ b/test/fixtures/votes.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + id: + room_id: 1 + user_id: 1 + election_slug: MyString + voter_login_id: MyString + voter_password: MyString + +two: + id: + room_id: 1 + user_id: 1 + election_slug: MyString + voter_login_id: MyString + voter_password: MyString diff --git a/test/models/vote_test.rb b/test/models/vote_test.rb new file mode 100644 index 0000000..f31f992 --- /dev/null +++ b/test/models/vote_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class VoteTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end