From b4e56645f6bd08a85b575c8aa9f4698dbf971dbf Mon Sep 17 00:00:00 2001 From: ricola Date: Mon, 20 Apr 2026 12:45:21 -0600 Subject: [PATCH 1/5] Display users as table in admin panel --- views/admin.erb | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/views/admin.erb b/views/admin.erb index 9857d75..3f7607d 100644 --- a/views/admin.erb +++ b/views/admin.erb @@ -2,13 +2,26 @@

<%= _("Users") %>

- + + + + + + + + + + + <% @users.sort_by { |user| user.email }.each do |user| %> + + + + + + + + <% end %> +
<%= _("Email") %><%= _("Created") %><%= _("Admin") %><%= _("Votes") %><%= _("Ratings") %>
<%= user.email %><%= format_date(user.created_at) %><%= user.admin %><%= user.votes.length %><%= user.ratings.length %>

<%= _("Votes") %>

From 267dc439f1d5dc5ce5bce571f1d9d6d426aee15e Mon Sep 17 00:00:00 2001 From: ricola Date: Mon, 20 Apr 2026 13:09:14 -0600 Subject: [PATCH 2/5] Allow user to see their profile page --- vedia.rb | 21 +++++++++++++++------ views/admin.erb | 2 +- views/layout.erb | 2 +- views/{admin_users.erb => users_show.erb} | 4 +--- 4 files changed, 18 insertions(+), 11 deletions(-) rename views/{admin_users.erb => users_show.erb} (93%) diff --git a/vedia.rb b/vedia.rb index 080d054..b59f999 100644 --- a/vedia.rb +++ b/vedia.rb @@ -218,6 +218,11 @@ post '/logout' do redirect '/login' end +get '/users/:id' do + require_admin_or_self + erb :users_show +end + get '/admin' do require_admin @users = User.all @@ -225,12 +230,6 @@ get '/admin' do erb :admin end -get '/admin/users/:id' do - require_admin - @user = User.find(params[:id]) - erb :admin_users -end - get '/admin/users/:id/organizers/:vote/delete' do require_admin rating = Organizer.where(user: params[:id]).where(vote: params[:vote]).each do |organizer| @@ -652,6 +651,12 @@ helpers do redirect '/' unless is_admin end + def require_admin_or_self + require_login + find_user + redirect '/' unless is_admin or current_user == @user + end + def find_vote @vote = Vote.find_by(secure_id: params[:id]) end @@ -660,6 +665,10 @@ helpers do @candidate = Candidate.find(params[:cid]) end + def find_user + @user = User.find(params[:id]) + end + def all_users_sorted User.all.each.sort_by { |user| user.email } end diff --git a/views/admin.erb b/views/admin.erb index 3f7607d..267cdb2 100644 --- a/views/admin.erb +++ b/views/admin.erb @@ -14,7 +14,7 @@ <% @users.sort_by { |user| user.email }.each do |user| %> - <%= user.email %> + <%= user.email %> <%= format_date(user.created_at) %> <%= user.admin %> <%= user.votes.length %> diff --git a/views/layout.erb b/views/layout.erb index e9e1e9f..26de47b 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -40,7 +40,7 @@
<% if current_user %> - <%= current_user.email %> + <%= current_user.email %> <% else %>   <% end %> diff --git a/views/admin_users.erb b/views/users_show.erb similarity index 93% rename from views/admin_users.erb rename to views/users_show.erb index 8868df4..0101801 100644 --- a/views/admin_users.erb +++ b/views/users_show.erb @@ -1,6 +1,4 @@ -

<%= _("Admin") %>

- -

<%= @user.email %>

+

<%= @user.email %>

<%= _("Created: %{date}") % { date: format_date(@user.created_at) } %>

From 1acc3697748620f59708061e45e44f818300bbc2 Mon Sep 17 00:00:00 2001 From: ricola Date: Mon, 20 Apr 2026 14:10:38 -0600 Subject: [PATCH 3/5] Move error message to view --- vedia.rb | 2 +- views/login.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vedia.rb b/vedia.rb index b59f999..42ff172 100644 --- a/vedia.rb +++ b/vedia.rb @@ -134,7 +134,7 @@ post '/login' do redirect '/' end else - @error = _("Incorrect email or password.") + @error = true erb :login end end diff --git a/views/login.erb b/views/login.erb index e7fed26..1ea317e 100644 --- a/views/login.erb +++ b/views/login.erb @@ -2,7 +2,7 @@ <% if @error %>
-

<%= @error %>

+

<%= _("Incorrect email or password.") %>

<%= _("Reset password") %>

<% end %> From 29bd10507222e04c247f7bd03a1e102363e863bf Mon Sep 17 00:00:00 2001 From: ricola Date: Mon, 20 Apr 2026 14:17:52 -0600 Subject: [PATCH 4/5] Allow closing accounts Closes #37 --- .../20260420181729_add_closed_at_to_user.rb | 5 ++ .../20260420210736_add_opened_at_to_vote.rb | 5 ++ db/schema.rb | 4 +- vedia.rb | 83 ++++++++++++++++--- views/admin.erb | 4 +- views/login.erb | 4 + views/users_close.erb | 11 +++ views/users_show.erb | 10 ++- views/votes_show_closed.erb | 2 +- views/votes_show_open.erb | 2 +- 10 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20260420181729_add_closed_at_to_user.rb create mode 100644 db/migrate/20260420210736_add_opened_at_to_vote.rb create mode 100644 views/users_close.erb diff --git a/db/migrate/20260420181729_add_closed_at_to_user.rb b/db/migrate/20260420181729_add_closed_at_to_user.rb new file mode 100644 index 0000000..1487103 --- /dev/null +++ b/db/migrate/20260420181729_add_closed_at_to_user.rb @@ -0,0 +1,5 @@ +class AddClosedAtToUser < ActiveRecord::Migration[7.2] + def change + add_column :users, :closed_at, :datetime + end +end diff --git a/db/migrate/20260420210736_add_opened_at_to_vote.rb b/db/migrate/20260420210736_add_opened_at_to_vote.rb new file mode 100644 index 0000000..2629227 --- /dev/null +++ b/db/migrate/20260420210736_add_opened_at_to_vote.rb @@ -0,0 +1,5 @@ +class AddOpenedAtToVote < ActiveRecord::Migration[7.2] + def change + add_column :votes, :opened_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 5675bc4..c5aee86 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_23_215246) do +ActiveRecord::Schema[7.2].define(version: 2026_04_20_210736) do create_table "candidates", force: :cascade do |t| t.integer "vote_id" t.string "name" @@ -46,6 +46,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_03_23_215246) do t.datetime "updated_at", null: false t.string "reset" t.boolean "admin" + t.datetime "closed_at" end create_table "votes", force: :cascade do |t| @@ -57,6 +58,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_03_23_215246) do t.datetime "updated_at", null: false t.string "state" t.integer "reminders" + t.datetime "opened_at" end add_foreign_key "ratings", "votes" diff --git a/vedia.rb b/vedia.rb index 42ff172..48bc673 100644 --- a/vedia.rb +++ b/vedia.rb @@ -120,7 +120,7 @@ end post '/login' do user = User.find_by(email: params[:email].downcase.strip) - if user && verify_password(params[:password], user.password) + if user && active(user) && verify_password(params[:password], user.password) if not user.reset.nil? user.reset = nil user.save @@ -154,7 +154,7 @@ post '/reset' do erb :reset else @user = User.find_by(email: params[:email]) - if @user + if @user && active(@user) @reset = SecureRandom.uuid @user.reset = hash_password(@reset) @user.save @@ -174,7 +174,7 @@ get '/reset/:uuid' do User.where.not(reset: nil).each do |user| @user = user if verify_password(params[:uuid], user.reset) end - if @user + if @user && active(@user) erb :reset_change else erb :reset_invalid @@ -186,7 +186,7 @@ post '/reset/:uuid' do User.where.not(reset: nil).each do |user| @user = user if verify_password(params[:uuid], user.reset) end - if @user + if @user && active(@user) @errors = [] if params[:password].empty? @errors << OpenStruct.new(:attribute => :password, :type => :blank) @@ -223,9 +223,28 @@ get '/users/:id' do erb :users_show end +get '/users/:id/close' do + require_admin_or_self + require_active + erb :users_close +end + +post '/users/:id/close' do + require_admin_or_self + require_active + @user.closed_at = Time.now.utc + @user.save + if is_admin + redirect "/users/#{@user.id}" + else + @closed = true + session.clear + erb :login + end +end + get '/admin' do require_admin - @users = User.all @votes = Vote.all erb :admin end @@ -556,7 +575,7 @@ post '/votes/:id/organizers' do @errors << OpenStruct.new(:attribute => :email, :type => :invalid) else user = User.find_by(email: params[:email]) - if not user + if not user or not active(user) @errors << OpenStruct.new(:attribute => :email, :type => :unknown) @params = params elsif @vote.users.exists?(user.id) @@ -584,7 +603,7 @@ def close_expired_votes puts "#{Time.now.utc} Closing vote \"#{vote.title}\" because it expired on #{vote.expire_on}..." vote.state = 'closed' vote.save - all_users_sorted.each do |user| + users_for_vote(vote).each do |user| puts "#{Time.now.utc} Sending results by email to #{user.email}..." mail = Mail.new mail.from = settings.admin_email @@ -602,7 +621,7 @@ def send_reminders settings.reminders.slice(vote.reminders..settings.reminders.length-1).each do |reminder| minutes_to_expiry = ( vote.expire_on - Time.now.utc ) / 60 if minutes_to_expiry < reminder[:timeout] / 60 - all_users_sorted.each do |user| + active_users.each do |user| if not vote.ratings.collect { |rating| rating.user }.include?(user) puts "#{Time.now.utc} Sending reminder #{reminder[:template]} for '#{vote.title}' to #{user.email}..." mail = Mail.new @@ -628,7 +647,13 @@ end helpers do def current_user if session[:user_id] - User.find(session[:user_id]) + user = User.find(session[:user_id]) + if active(user) + return user + else + session.clear + return nil + end elsif settings.spoof_admin session.clear session[:timezone] = 'UTC' @@ -657,6 +682,15 @@ helpers do redirect '/' unless is_admin or current_user == @user end + def active(user) + user.closed_at.nil? + end + + def require_active + find_user + redirect '/' unless active(@user) + end + def find_vote @vote = Vote.find_by(secure_id: params[:id]) end @@ -669,10 +703,39 @@ helpers do @user = User.find(params[:id]) end - def all_users_sorted + def all_users User.all.each.sort_by { |user| user.email } end + def active_users(timestamp = Time.now.utc) + users = [] + User.all.each do |user| + if user.created_at < timestamp and ( active(user) or user.closed_at > timestamp ) + users << user + end + end + users.sort_by { |user| user.email } + end + + def users_created_between(min, max) + users = [] + User.all.each do |user| + if user.created_at > min and user.created_at < max + users << user + end + end + users.sort_by { |user| user.email } + end + + def users_for_vote(vote) + # Users who were active when the vote was open (or created if not open yet) + # Users who were created between the vote was open and the vote was closed (or now if the vote is not closed yet) + min = vote.opened_at ? vote.opened_at : vote.created_at + max = vote.expire_on ? vote.expire_on : Time.now.utc + users = active_users(min) + users_created_between(min, max) + users.sort_by { |user| user.email } + end + def require_candidate_in_vote redirect '/votes/' + @vote.secure_id unless @candidate.vote == @vote end diff --git a/views/admin.erb b/views/admin.erb index 267cdb2..3b93fc1 100644 --- a/views/admin.erb +++ b/views/admin.erb @@ -7,15 +7,17 @@ <%= _("Email") %> <%= _("Created") %> + <%= _("Closed") %> <%= _("Admin") %> <%= _("Votes") %> <%= _("Ratings") %> - <% @users.sort_by { |user| user.email }.each do |user| %> + <% all_users.each do |user| %> <%= user.email %> <%= format_date(user.created_at) %> + <%= active(user) ? nil : format_date(user.closed_at) %> <%= user.admin %> <%= user.votes.length %> <%= user.ratings.length %> diff --git a/views/login.erb b/views/login.erb index 1ea317e..e16bb4c 100644 --- a/views/login.erb +++ b/views/login.erb @@ -1,5 +1,9 @@

<%= _("Login") %>

+<% if @closed %> +

<%= _("Your account was closed successfully.") %>

+<% end %> + <% if @error %>

<%= _("Incorrect email or password.") %>

diff --git a/views/users_close.erb b/views/users_close.erb new file mode 100644 index 0000000..4945b09 --- /dev/null +++ b/views/users_close.erb @@ -0,0 +1,11 @@ +

<%= _("Closing account %{email}") % { email: @user.email } %>

+ +

<%= _("Do you really want to close the account for %{email}?") % { email: @user.email } %>

+ +

<%= _("Past votes and ratings created by this account will remain available, but the account will no longer be able to log in, create new votes, or participate in open votes.") %>

+ +

<%= _("This action cannot be undone.") %>

+ +
+ +
diff --git a/views/users_show.erb b/views/users_show.erb index 0101801..61122b5 100644 --- a/views/users_show.erb +++ b/views/users_show.erb @@ -4,7 +4,15 @@

<%= _("Updated: %{date}") % { date: format_date(@user.updated_at) } %>

-

<%= _("Admin: %{admin}") % { admin: @user.admin ? _("Yes") : _("No") } %>

+

<%= _("Admin: %{admin}") % { admin: @user.admin ? _("Yes") : _("No") } %>

+ +<% if @user.closed_at %> +

<%= _("Closed: %{date}") % { date: format_date(@user.closed_at) } %>

+<% else %> +
+ +
+<% end %>

<%= _("Organized votes") %>

diff --git a/views/votes_show_closed.erb b/views/votes_show_closed.erb index b904577..a0f220d 100644 --- a/views/votes_show_closed.erb +++ b/views/votes_show_closed.erb @@ -175,7 +175,7 @@ <% end %> - <% all_users_sorted.each do |user| %> + <% users_for_vote(@vote).each do |user| %> <%= user.email %> <% @vote.candidates.each do |candidate| %> diff --git a/views/votes_show_open.erb b/views/votes_show_open.erb index ea19fb5..1eae220 100644 --- a/views/votes_show_open.erb +++ b/views/votes_show_open.erb @@ -66,7 +66,7 @@

<%= _("Participants") + " (#{@vote.ratings.collect { |rating| rating.user }.uniq.count})" %>

    - <% all_users_sorted.each do |user| %> + <% users_for_vote(@vote).each do |user| %>
  • <% if @vote.ratings.collect { |rating| rating.user }.include?(user) %> From e70279a1b2485a2ab35b21998164bcfa04f355b9 Mon Sep 17 00:00:00 2001 From: ricola Date: Mon, 20 Apr 2026 16:05:07 -0600 Subject: [PATCH 5/5] Add headings --- views/admin.erb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/views/admin.erb b/views/admin.erb index 3b93fc1..24f100c 100644 --- a/views/admin.erb +++ b/views/admin.erb @@ -30,14 +30,16 @@ - - + + + <% @votes.reverse.each do |vote| %> +
    <%= _("Created") %><%= _("Expired") %><%= _("State") %>
    <%= format_date(vote.created_at) %><%= format_date(vote.expire_on) if vote.expire_on %> <% case vote.state when 'draft' %>