Compare commits
5 commits
50ad689b28
...
e70279a1b2
| Author | SHA1 | Date | |
|---|---|---|---|
| e70279a1b2 | |||
| 29bd105072 | |||
| 1acc369774 | |||
| 267dc439f1 | |||
| b4e56645f6 |
11 changed files with 158 additions and 36 deletions
5
db/migrate/20260420181729_add_closed_at_to_user.rb
Normal file
5
db/migrate/20260420181729_add_closed_at_to_user.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddClosedAtToUser < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :users, :closed_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20260420210736_add_opened_at_to_vote.rb
Normal file
5
db/migrate/20260420210736_add_opened_at_to_vote.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddOpenedAtToVote < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :votes, :opened_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table "candidates", force: :cascade do |t|
|
||||||
t.integer "vote_id"
|
t.integer "vote_id"
|
||||||
t.string "name"
|
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.datetime "updated_at", null: false
|
||||||
t.string "reset"
|
t.string "reset"
|
||||||
t.boolean "admin"
|
t.boolean "admin"
|
||||||
|
t.datetime "closed_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "votes", force: :cascade do |t|
|
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.datetime "updated_at", null: false
|
||||||
t.string "state"
|
t.string "state"
|
||||||
t.integer "reminders"
|
t.integer "reminders"
|
||||||
|
t.datetime "opened_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "ratings", "votes"
|
add_foreign_key "ratings", "votes"
|
||||||
|
|
|
||||||
108
vedia.rb
108
vedia.rb
|
|
@ -120,7 +120,7 @@ end
|
||||||
|
|
||||||
post '/login' do
|
post '/login' do
|
||||||
user = User.find_by(email: params[:email].downcase.strip)
|
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?
|
if not user.reset.nil?
|
||||||
user.reset = nil
|
user.reset = nil
|
||||||
user.save
|
user.save
|
||||||
|
|
@ -134,7 +134,7 @@ post '/login' do
|
||||||
redirect '/'
|
redirect '/'
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@error = _("Incorrect email or password.")
|
@error = true
|
||||||
erb :login
|
erb :login
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -154,7 +154,7 @@ post '/reset' do
|
||||||
erb :reset
|
erb :reset
|
||||||
else
|
else
|
||||||
@user = User.find_by(email: params[:email])
|
@user = User.find_by(email: params[:email])
|
||||||
if @user
|
if @user && active(@user)
|
||||||
@reset = SecureRandom.uuid
|
@reset = SecureRandom.uuid
|
||||||
@user.reset = hash_password(@reset)
|
@user.reset = hash_password(@reset)
|
||||||
@user.save
|
@user.save
|
||||||
|
|
@ -174,7 +174,7 @@ get '/reset/:uuid' do
|
||||||
User.where.not(reset: nil).each do |user|
|
User.where.not(reset: nil).each do |user|
|
||||||
@user = user if verify_password(params[:uuid], user.reset)
|
@user = user if verify_password(params[:uuid], user.reset)
|
||||||
end
|
end
|
||||||
if @user
|
if @user && active(@user)
|
||||||
erb :reset_change
|
erb :reset_change
|
||||||
else
|
else
|
||||||
erb :reset_invalid
|
erb :reset_invalid
|
||||||
|
|
@ -186,7 +186,7 @@ post '/reset/:uuid' do
|
||||||
User.where.not(reset: nil).each do |user|
|
User.where.not(reset: nil).each do |user|
|
||||||
@user = user if verify_password(params[:uuid], user.reset)
|
@user = user if verify_password(params[:uuid], user.reset)
|
||||||
end
|
end
|
||||||
if @user
|
if @user && active(@user)
|
||||||
@errors = []
|
@errors = []
|
||||||
if params[:password].empty?
|
if params[:password].empty?
|
||||||
@errors << OpenStruct.new(:attribute => :password, :type => :blank)
|
@errors << OpenStruct.new(:attribute => :password, :type => :blank)
|
||||||
|
|
@ -218,17 +218,35 @@ post '/logout' do
|
||||||
redirect '/login'
|
redirect '/login'
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/admin' do
|
get '/users/:id' do
|
||||||
require_admin
|
require_admin_or_self
|
||||||
@users = User.all
|
erb :users_show
|
||||||
@votes = Vote.all
|
|
||||||
erb :admin
|
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/admin/users/:id' do
|
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
|
require_admin
|
||||||
@user = User.find(params[:id])
|
@votes = Vote.all
|
||||||
erb :admin_users
|
erb :admin
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/admin/users/:id/organizers/:vote/delete' do
|
get '/admin/users/:id/organizers/:vote/delete' do
|
||||||
|
|
@ -557,7 +575,7 @@ post '/votes/:id/organizers' do
|
||||||
@errors << OpenStruct.new(:attribute => :email, :type => :invalid)
|
@errors << OpenStruct.new(:attribute => :email, :type => :invalid)
|
||||||
else
|
else
|
||||||
user = User.find_by(email: params[:email])
|
user = User.find_by(email: params[:email])
|
||||||
if not user
|
if not user or not active(user)
|
||||||
@errors << OpenStruct.new(:attribute => :email, :type => :unknown)
|
@errors << OpenStruct.new(:attribute => :email, :type => :unknown)
|
||||||
@params = params
|
@params = params
|
||||||
elsif @vote.users.exists?(user.id)
|
elsif @vote.users.exists?(user.id)
|
||||||
|
|
@ -585,7 +603,7 @@ def close_expired_votes
|
||||||
puts "#{Time.now.utc} Closing vote \"#{vote.title}\" because it expired on #{vote.expire_on}..."
|
puts "#{Time.now.utc} Closing vote \"#{vote.title}\" because it expired on #{vote.expire_on}..."
|
||||||
vote.state = 'closed'
|
vote.state = 'closed'
|
||||||
vote.save
|
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}..."
|
puts "#{Time.now.utc} Sending results by email to #{user.email}..."
|
||||||
mail = Mail.new
|
mail = Mail.new
|
||||||
mail.from = settings.admin_email
|
mail.from = settings.admin_email
|
||||||
|
|
@ -603,7 +621,7 @@ def send_reminders
|
||||||
settings.reminders.slice(vote.reminders..settings.reminders.length-1).each do |reminder|
|
settings.reminders.slice(vote.reminders..settings.reminders.length-1).each do |reminder|
|
||||||
minutes_to_expiry = ( vote.expire_on - Time.now.utc ) / 60
|
minutes_to_expiry = ( vote.expire_on - Time.now.utc ) / 60
|
||||||
if minutes_to_expiry < reminder[:timeout] / 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)
|
if not vote.ratings.collect { |rating| rating.user }.include?(user)
|
||||||
puts "#{Time.now.utc} Sending reminder #{reminder[:template]} for '#{vote.title}' to #{user.email}..."
|
puts "#{Time.now.utc} Sending reminder #{reminder[:template]} for '#{vote.title}' to #{user.email}..."
|
||||||
mail = Mail.new
|
mail = Mail.new
|
||||||
|
|
@ -629,7 +647,13 @@ end
|
||||||
helpers do
|
helpers do
|
||||||
def current_user
|
def current_user
|
||||||
if session[:user_id]
|
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
|
elsif settings.spoof_admin
|
||||||
session.clear
|
session.clear
|
||||||
session[:timezone] = 'UTC'
|
session[:timezone] = 'UTC'
|
||||||
|
|
@ -652,6 +676,21 @@ helpers do
|
||||||
redirect '/' unless is_admin
|
redirect '/' unless is_admin
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_admin_or_self
|
||||||
|
require_login
|
||||||
|
find_user
|
||||||
|
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
|
def find_vote
|
||||||
@vote = Vote.find_by(secure_id: params[:id])
|
@vote = Vote.find_by(secure_id: params[:id])
|
||||||
end
|
end
|
||||||
|
|
@ -660,10 +699,43 @@ helpers do
|
||||||
@candidate = Candidate.find(params[:cid])
|
@candidate = Candidate.find(params[:cid])
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_users_sorted
|
def find_user
|
||||||
|
@user = User.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_users
|
||||||
User.all.each.sort_by { |user| user.email }
|
User.all.each.sort_by { |user| user.email }
|
||||||
end
|
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
|
def require_candidate_in_vote
|
||||||
redirect '/votes/' + @vote.secure_id unless @candidate.vote == @vote
|
redirect '/votes/' + @vote.secure_id unless @candidate.vote == @vote
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,44 @@
|
||||||
|
|
||||||
<h2 class="mb-4"><%= _("Users") %></h2>
|
<h2 class="mb-4"><%= _("Users") %></h2>
|
||||||
|
|
||||||
<ul class="mb-5">
|
<table class="table table-striped mb-5">
|
||||||
<% @users.sort_by { |user| user.email }.each do |user| %>
|
<thead>
|
||||||
<li>
|
<tr>
|
||||||
<a href="/admin/users/<%= user.id %>"><%= user.email %></a>
|
<th><%= _("Email") %></th>
|
||||||
</li>
|
<th><%= _("Created") %></th>
|
||||||
|
<th><%= _("Closed") %></th>
|
||||||
|
<th><%= _("Admin") %></th>
|
||||||
|
<th><%= _("Votes") %></th>
|
||||||
|
<th><%= _("Ratings") %></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<% all_users.each do |user| %>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/users/<%= user.id %>"><%= user.email %></a></td>
|
||||||
|
<td><%= format_date(user.created_at) %></td>
|
||||||
|
<td><%= active(user) ? nil : format_date(user.closed_at) %></td>
|
||||||
|
<td><%= user.admin %></td>
|
||||||
|
<td><%= user.votes.length %></td>
|
||||||
|
<td><%= user.ratings.length %></td>
|
||||||
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</table>
|
||||||
|
|
||||||
<h2 class="mb-4"><%= _("Votes") %></h2>
|
<h2 class="mb-4"><%= _("Votes") %></h2>
|
||||||
|
|
||||||
<table class="table table-striped mb-5">
|
<table class="table table-striped mb-5">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<th><%= _("Created") %></th>
|
||||||
<td></td>
|
<th><%= _("Expired") %></th>
|
||||||
|
<th><%= _("State") %></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @votes.reverse.each do |vote| %>
|
<% @votes.reverse.each do |vote| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= format_date(vote.created_at) %></td>
|
<td><%= format_date(vote.created_at) %></td>
|
||||||
|
<td><%= format_date(vote.expire_on) if vote.expire_on %></td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
<% case vote.state
|
<% case vote.state
|
||||||
when 'draft' %>
|
when 'draft' %>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
<div class="d-flex flex-wrap justify-content-end pe-3 py-3 mb-4">
|
<div class="d-flex flex-wrap justify-content-end pe-3 py-3 mb-4">
|
||||||
<% if current_user %>
|
<% if current_user %>
|
||||||
<%= current_user.email %>
|
<a href="/users/<%= current_user.id %>"><%= current_user.email %></a>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
<h1 class="mb-5"><%= _("Login") %></h1>
|
<h1 class="mb-5"><%= _("Login") %></h1>
|
||||||
|
|
||||||
|
<% if @closed %>
|
||||||
|
<p class="alert alert-success mb-4"><%= _("Your account was closed successfully.") %></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if @error %>
|
<% if @error %>
|
||||||
<div class="alert alert-warning mb-4">
|
<div class="alert alert-warning mb-4">
|
||||||
<p><%= @error %></p>
|
<p><%= _("Incorrect email or password.") %></p>
|
||||||
<p class="mb-0"><a href="/reset"><%= _("Reset password") %></a></p>
|
<p class="mb-0"><a href="/reset"><%= _("Reset password") %></a></p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
11
views/users_close.erb
Normal file
11
views/users_close.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<h1 class="mb-5"><%= _("Closing account %{email}") % { email: @user.email } %></h1>
|
||||||
|
|
||||||
|
<p><%= _("Do you really want to close the account for %{email}?") % { email: @user.email } %></p>
|
||||||
|
|
||||||
|
<p><%= _("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.") %></p>
|
||||||
|
|
||||||
|
<p><%= _("This action cannot be undone.") %></p>
|
||||||
|
|
||||||
|
<form action="/users/<%= @user.id %>/close" method="post" class="mb-5">
|
||||||
|
<button type="submit" class="btn btn-danger"><%= _("Close account") %></button>
|
||||||
|
</form>
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
<h1 class="mb-5"><%= _("Admin") %></h1>
|
<h1 class="mb-5"><%= @user.email %></h1>
|
||||||
|
|
||||||
<h2 class="mb-4"><%= @user.email %></h2>
|
|
||||||
|
|
||||||
<p><%= _("Created: %{date}") % { date: format_date(@user.created_at) } %></p>
|
<p><%= _("Created: %{date}") % { date: format_date(@user.created_at) } %></p>
|
||||||
|
|
||||||
<p><%= _("Updated: %{date}") % { date: format_date(@user.updated_at) } %></p>
|
<p><%= _("Updated: %{date}") % { date: format_date(@user.updated_at) } %></p>
|
||||||
|
|
||||||
<p class="mb-5"><%= _("Admin: %{admin}") % { admin: @user.admin ? _("Yes") : _("No") } %></p>
|
<p><%= _("Admin: %{admin}") % { admin: @user.admin ? _("Yes") : _("No") } %></p>
|
||||||
|
|
||||||
|
<% if @user.closed_at %>
|
||||||
|
<p class="mb-5"><%= _("Closed: %{date}") % { date: format_date(@user.closed_at) } %></p>
|
||||||
|
<% else %>
|
||||||
|
<form action="/users/<%= @user.id %>/close" method="get" class="mb-5">
|
||||||
|
<button type="submit" class="btn btn-outline-danger"><%= _("Close account") %></button>
|
||||||
|
</form>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<h2 class="mb-4"><%= _("Organized votes") %></h2>
|
<h2 class="mb-4"><%= _("Organized votes") %></h2>
|
||||||
|
|
||||||
|
|
@ -175,7 +175,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<% all_users_sorted.each do |user| %>
|
<% users_for_vote(@vote).each do |user| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= user.email %></td>
|
<td><%= user.email %></td>
|
||||||
<% @vote.candidates.each do |candidate| %>
|
<% @vote.candidates.each do |candidate| %>
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
<h2 class="mb-4"><%= _("Participants") + " (#{@vote.ratings.collect { |rating| rating.user }.uniq.count})" %></h2>
|
<h2 class="mb-4"><%= _("Participants") + " (#{@vote.ratings.collect { |rating| rating.user }.uniq.count})" %></h2>
|
||||||
|
|
||||||
<ul class="mb-5">
|
<ul class="mb-5">
|
||||||
<% all_users_sorted.each do |user| %>
|
<% users_for_vote(@vote).each do |user| %>
|
||||||
<li>
|
<li>
|
||||||
<% if @vote.ratings.collect { |rating| rating.user }.include?(user) %>
|
<% if @vote.ratings.collect { |rating| rating.user }.include?(user) %>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue