require 'sinatra' # Set environment before requiring 'sinatra/activerecord' to make `whenever` uses the database. if ENV['RAILS_ENV'] set :environment, ENV['RAILS_ENV'] end require 'sinatra/activerecord' require 'bcrypt' require 'gettext' require 'securerandom' require 'chartkick' require 'mail' require 'ostruct' require 'tzinfo' require 'redcarpet' include GetText require_relative 'mj' require_relative 'config/environments/defaults.rb' require_relative "config/environments/#{settings.environment}" class Vote < ActiveRecord::Base has_many :candidates, dependent: :destroy has_many :ratings, dependent: :destroy has_many :organizers, dependent: :destroy has_many :users, through: :organizers validates :state, inclusion: { in: ['draft', 'open', 'closed'] } end class Candidate < ActiveRecord::Base belongs_to :vote has_many :ratings, dependent: :destroy def mj return MajorityJudgment.new(self.ratings.collect {|r| r.value }) end end class User < ActiveRecord::Base has_many :ratings has_many :organizers has_many :votes, through: :organizers validates :email, uniqueness: true validates :email, format: URI::MailTo::EMAIL_REGEXP end class Organizer < ActiveRecord::Base belongs_to :vote belongs_to :user validates :vote_id, uniqueness: { scope: :user_id } validates :user_id, uniqueness: { scope: :vote_id } end class Rating < ActiveRecord::Base belongs_to :vote belongs_to :user belongs_to :candidate end def hash_password(password) BCrypt::Password.create(password).to_s end def verify_password(password, hash) BCrypt::Password.new(hash) == password end set_output_charset('UTF-8') bindtextdomain('vedia', 'locale') set_locale('ca') enable :sessions MajorityJudgment.values = settings.values get '/' do redirect '/votes' end get '/style.css' do content_type 'text/css' erb :style, :layout => false end get '/signup' do erb :signup end post '/signup' do @user = User.create(email: params[:email]) @errors = [] if params[:password].empty? @errors << OpenStruct.new(:attribute => :password, :type => :blank) else @user.password = hash_password(params[:password]) end if @errors.empty? and @user.valid? @user.admin = true if @user.id == 1 @user.save session.clear session[:user_id] = @user.id redirect '/' else erb :signup end end get '/login' do erb :login end post '/login' do user = User.find_by(email: params[:email]) if user && verify_password(params[:password], user.password) if not user.reset.nil? user.reset = nil user.save end session.clear session[:user_id] = user.id redirect '/' else @error = _("Incorrect email or password.") erb :login end end post '/timezone' do session[:timezone] = JSON.parse(request.body.read)['timezone'] end get '/reset' do erb :reset end post '/reset' do @user = User.find_by(email: params[:email]) if @user @reset = SecureRandom.uuid @user.reset = hash_password(@reset) @user.save mail = Mail.new mail.from = settings.admin_email mail.to = @user.email mail.subject = _("Reset your password") mail.body = erb :reset_email, :layout => false mail.deliver end erb :reset_sent end get '/reset/:uuid' do @user = nil User.where.not(reset: nil).each do |user| @user = user if verify_password(params[:uuid], user.reset) end if @user erb :reset_change else erb :reset_invalid end end post '/reset/:uuid' do @user = nil User.where.not(reset: nil).each do |user| @user = user if verify_password(params[:uuid], user.reset) end if @user @errors = [] if params[:password].empty? @errors << OpenStruct.new(:attribute => :password, :type => :blank) else @user.password = hash_password(params[:password]) end if @errors.empty? and @user.valid? @user.reset = nil @user.save session.clear session[:user_id] = @user.id redirect '/' else erb :reset_change end else erb :reset_invalid end end get '/logout' do session.clear redirect '/login' end post '/logout' do session.clear redirect '/login' end get '/admin' do require_admin @users = User.all @votes = Vote.all 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| organizer.destroy end redirect "/admin/users/#{params[:id]}" end get '/admin/users/:id/ratings/:vote/delete' do require_admin rating = Rating.where(user: params[:id]).where(vote: params[:vote]).each do |rating| rating.destroy end redirect "/admin/users/#{params[:id]}" end post '/admin/users/:id/delete' do require_admin @user = User.find(params[:id]) @user.destroy redirect '/admin' end get '/admin/votes/:id' do require_admin @vote = Vote.find(params[:id]) erb :admin_votes end get '/admin/votes/:id/organizers/:user/delete' do require_admin rating = Organizer.where(vote: params[:id]).where(user: params[:user]).each do |organizer| organizer.destroy end redirect "/admin/votes/#{params[:id]}" end get '/admin/votes/:id/ratings/:user/delete' do require_admin rating = Rating.where(vote: params[:id]).where(user: params[:user]).each do |rating| rating.destroy end redirect "/admin/votes/#{params[:id]}" end post '/admin/votes/:id/delete' do require_admin @vote = Vote.find(params[:id]) @vote.destroy redirect '/admin' end get '/votes' do require_login @votes = Vote.all erb :votes end get '/votes/new' do require_login erb :votes_new end post '/votes/new' do require_login @vote = Vote.create(secure_id: SecureRandom.hex(8), title: params[:title], description: params[:description], state: 'draft') @vote.users << current_user redirect '/votes/' + @vote.secure_id end get '/votes/:id' do require_login find_vote case @vote.state when 'draft' if @vote.users.exists?(current_user.id) erb :votes_edit else erb :votes_show_draft end when 'open' erb :votes_show_open when 'closed' erb :votes_show_closed end end get '/votes/:id/edit' do require_login find_vote require_organizer require_draft_vote erb :votes_edit_description end post '/votes/:id/edit' do require_login find_vote require_organizer require_draft_vote @vote.title = params[:title] @vote.description = params[:description] @vote.save redirect '/votes/' + @vote.secure_id end post '/votes/:id/candidates' do require_login find_vote require_organizer require_draft_vote @candidate = Candidate.new(name: params[:name], description: params[:description]) @candidate.vote = @vote @candidate.save redirect '/votes/' + @vote.secure_id end post '/votes/:id/candidates/:cid/delete' do require_login find_vote require_organizer require_draft_vote @candidate = Candidate.find(params[:cid]) @candidate.destroy redirect '/votes/' + @vote.secure_id end get '/votes/:id/open' do require_login find_vote require_organizer require_draft_vote @expire_on = Time.now.utc + settings.expire_after erb :votes_open end post '/votes/:id/open' do require_login find_vote require_organizer require_draft_vote if not @vote.candidates.length < 2 @vote.state = 'open' @vote.expire_on = Time.now.utc + settings.expire_after @vote.save end redirect '/votes/' + @vote.secure_id end post '/votes/:id/draft' do require_login find_vote require_organizer require_open_vote require_no_expire_on @vote.ratings.each {|r| r.destroy} @vote.state = 'draft' @vote.save redirect '/votes/' + @vote.secure_id end post '/votes/:id/close' do require_login find_vote require_organizer require_open_vote require_no_expire_on @vote.state = 'closed' @vote.save redirect '/votes/' + @vote.secure_id end post '/votes/:id/reopen' do require_login find_vote require_organizer require_closed_vote require_no_expire_on @vote.state = 'open' @vote.save redirect '/votes/' + @vote.secure_id end get '/votes/:id/ratings' do redirect '/votes/' + params[:id] end post '/votes/:id/ratings' do require_login find_vote require_open_vote @errors = [] @vote.candidates.each do |candidate| if not params[candidate.id.to_s] @errors << OpenStruct.new(:attribute => :rating, :type => :blank, :candidate => candidate) end end if not @errors.empty? @params = params erb :votes_show_open else @vote.candidates.each do |candidate| rating = Rating.find_or_initialize_by(vote: @vote, user: current_user, candidate: candidate) rating.value = params[candidate.id.to_s] rating.save end redirect '/votes/' + @vote.secure_id end end post '/votes/:id/organizers' do require_login find_vote require_organizer user = User.find_by(email: params[:email]) @vote.users << user redirect '/votes/' + @vote.secure_id end post '/votes/:id/delete' do require_login find_vote require_organizer require_draft_vote @vote.destroy redirect '/' end def close_expired_votes Vote.where(state: 'open').where("expire_on < :now", { now: Time.now.utc }).each do |vote| puts "#{Time.now.utc} Closing vote \"#{vote.title}\" because it expired on #{vote.expire_on}..." vote.state = 'closed' vote.save User.all.each do |user| puts "#{Time.now.utc} Sending results by email to #{user.email}..." mail = Mail.new mail.from = settings.admin_email mail.to = user.email mail.subject = _("Results of the vote: #{vote.title}") template = ERB.new(File.read("views/votes_close_email.erb")) mail.body = template.result(binding) mail.deliver end end end helpers do def current_user if session[:user_id] User.find(session[:user_id]) elsif settings.spoof_admin User.find(1) else nil end end def is_admin current_user and current_user.admin end def require_login redirect '/login' unless current_user end def require_admin redirect '/' unless is_admin end def find_vote @vote = Vote.find_by(secure_id: params[:id]) end def require_organizer redirect '/votes/' + @vote.secure_id unless @vote.users.exists?(current_user.id) end def require_draft_vote redirect '/votes/' + @vote.secure_id unless @vote.state == 'draft' end def require_open_vote redirect '/votes/' + @vote.secure_id unless @vote.state == 'open' end def require_closed_vote redirect '/votes/' + @vote.secure_id unless @vote.state == 'closed' end def require_no_expire_on redirect '/votes/' + @vote.secure_id unless @vote.expire_on.nil? end def format_date(timestamp) "#{TZInfo::Timezone.get(session[:timezone]).to_local(timestamp).strftime('%F')}" end def format_date_and_time(timestamp) "#{TZInfo::Timezone.get(session[:timezone]).to_local(timestamp).strftime('%F %R')} (#{session[:timezone].gsub('_', ' ')})" end def markdown(markdown) renderer = Redcarpet::Render::HTML.new(filter_html: true, no_styles: true, safe_links_only: true) parser = Redcarpet::Markdown.new(renderer, tables: true, autolink: true, strikethrough: true, space_after_headers: true, superscript: true, underline: true, highlight: true, quote: true, footnotes: true) parser.render(markdown) end end