diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,8 +1,9 @@ -require 'pony' +require 'tmail' +require 'net/smtp' class UsersController < ApplicationController - before_filter :authenticate, :except => [:new, :register] + before_filter :authenticate, :except => [:new, :register, :confirm] verify :method => :post, :only => [:chg_passwd], :redirect_to => { :action => :index } @@ -40,17 +41,76 @@ @user.password_confirmation = @user.password = User.random_password @user.activated = false if (@user.valid?) and (@user.save) - send_confirmation_email(@user) - render :action => 'new_splash', :layout => 'empty' + if send_confirmation_email(@user) + render :action => 'new_splash', :layout => 'empty' + else + render :action => 'email_error', :layout => 'empty' + end else @user.errors.add_to_base("Email cannot be blank") if @user.email=='' render :action => 'new', :layout => 'empty' end end + def confirm + login = params[:login] + key = params[:activation] + user = User.find_by_login(login) + if (user) and (user.verify_activation_key(key)) + if user.valid? # check uniquenss of email + user.activated = true + user.save + @result = :successful + else + @result = :email_used + end + else + @result = :failed + end + render :action => 'confirm', :layout => 'empty' + end + protected def send_confirmation_email(user) + contest_name = Configuration['contest.name'] + activation_url = url_for(:action => 'confirm', + :login => user.login, + :activation => user.activation_key) + home_url = url_for(:controller => 'main', :action => 'index') + mail = TMail::Mail.new + mail.to = user.email + mail.from = Configuration['system.online_registration.from'] + mail.subject = "[#{contest_name}] Confirmation" + mail.body = <<-EOF +Hello #{user.full_name}, + +You have registered for #{contest_name} (#{home_url}). + +Your login is: #{user.login} +Your password is: #{user.password} + +Please follow the link: +#{activation_url} +to activate your user account. + +If you did not register, please ignore this e-mail. + +Thanks! +EOF + + smtp_server = Configuration['system.online_registration.smtp'] + + begin + Net::SMTP.start(smtp_server) do |smtp| + smtp.send_message(mail.to_s, mail.from, mail.to) + end + result = true + rescue + result = false + end + + return result end end diff --git a/app/models/configuration.rb b/app/models/configuration.rb --- a/app/models/configuration.rb +++ b/app/models/configuration.rb @@ -36,6 +36,10 @@ @@configurations = nil end + def self.enable_caching + @@cache = true + end + # # View decision # diff --git a/app/models/user.rb b/app/models/user.rb --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,7 +19,7 @@ belongs_to :site belongs_to :country - named_scope :activated, :conditions => {:activated => true} + named_scope :activated_users, :conditions => {:activated => true} validates_presence_of :login validates_uniqueness_of :login @@ -36,6 +36,7 @@ validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :allow_blank => true validate :uniqueness_of_email_from_activated_users + validate :enough_time_interval_between_same_email_registrations attr_accessor :password @@ -87,6 +88,9 @@ end def activation_key + if self.hashed_password==nil + encrypt_new_password + end Digest::SHA1.hexdigest(self.hashed_password)[0..7] end @@ -117,8 +121,18 @@ end def uniqueness_of_email_from_activated_users - if User.activated.find_by_email(self.email)!=nil + user = User.activated_users.find_by_email(self.email) + if user and (user.login != self.login) self.errors.add_to_base("Email has already been taken") end end + + def enough_time_interval_between_same_email_registrations + open_user = User.find_by_email(self.email, + :order => 'created_at DESC') + if open_user and open_user.created_at and + (open_user.created_at > Time.now.gmtime - 5.minutes) + self.errors.add_to_base("There are already unactivated registrations with this e-mail address (please wait for 5 minutes)") + end + end end diff --git a/app/views/users/confirm.html.haml b/app/views/users/confirm.html.haml new file mode 100644 --- /dev/null +++ b/app/views/users/confirm.html.haml @@ -0,0 +1,15 @@ +- if @result == :successful + %h1 User activated + Your account has been activated. + %br/ + Please go ahead to + = link_to 'login.', :controller => 'main', :action => 'login' +- else + %h1 Activation failed + - if @result == :email_used + A user with this E-mail exists. + - else + Your activation code is invalid. Please check again. + %br/ + Get back to + = link_to 'home.', :controller => 'main', :action => 'login' diff --git a/app/views/users/email_error.html.haml b/app/views/users/email_error.html.haml new file mode 100644 --- /dev/null +++ b/app/views/users/email_error.html.haml @@ -0,0 +1,9 @@ +%h1 Errors in sending registration confirmation + +.errorExplanation + %h2 + Your user account has been created, but the system cannot send you the + confirmation e-mail. + + Maybe there's a problem in the configuration. Please report the + admin. Thank you! diff --git a/config/environment.rb.SAMPLE b/config/environment.rb.SAMPLE --- a/config/environment.rb.SAMPLE +++ b/config/environment.rb.SAMPLE @@ -50,7 +50,7 @@ # This is for rspec config.gem "rspec-rails", :lib => "spec" config.gem "haml" - config.gem "pony" + config.gem "tmail" #config.gem "BlueCloth", :lig => "bluecloth" end @@ -72,3 +72,6 @@ # These are where inputs and outputs of test requests are stored TEST_REQUEST_INPUT_FILE_DIR = RAILS_ROOT + '/data/test_request/input' TEST_REQUEST_OUTPUT_FILE_DIR = RAILS_ROOT + '/data/test_request/output' + +# Uncomment so that configuration is read only once when the server is loaded +# Configuration.enable_caching diff --git a/db/migrate/20081205172258_add_emailing_info_on_config_option.rb b/db/migrate/20081205172258_add_emailing_info_on_config_option.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20081205172258_add_emailing_info_on_config_option.rb @@ -0,0 +1,18 @@ +class AddEmailingInfoOnConfigOption < ActiveRecord::Migration + def self.up + # If Configuration['system.online_registration'] is true, the + # system allows online registration, and will use these + # information for sending confirmation emails. + Configuration.create(:key => 'system.online_registration.smtp', + :value_type => 'string', + :value => 'smtp.somehost.com') + Configuration.create(:key => 'system.online_registration.from', + :value_type => 'string', + :value => 'your.email@address') + end + + def self.down + Configuration.find_by_key("system.online_registration.smtp").destroy + Configuration.find_by_key("system.online_registration.from").destroy + end +end diff --git a/db/migrate/20081210021333_add_timestamps_to_users.rb b/db/migrate/20081210021333_add_timestamps_to_users.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20081210021333_add_timestamps_to_users.rb @@ -0,0 +1,9 @@ +class AddTimestampsToUsers < ActiveRecord::Migration + def self.up + add_timestamps :users + end + + def self.down + remove_timestamps :users + end +end diff --git a/db/schema.rb b/db/schema.rb --- a/db/schema.rb +++ b/db/schema.rb @@ -9,7 +9,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20081204122651) do +ActiveRecord::Schema.define(:version => 20081210021333) do create_table "announcements", :force => true do |t| t.string "author" @@ -174,15 +174,17 @@ add_index "test_requests", ["user_id", "problem_id"], :name => "index_test_requests_on_user_id_and_problem_id" create_table "users", :force => true do |t| - t.string "login", :limit => 10 - t.string "full_name" - t.string "hashed_password" - t.string "salt", :limit => 5 - t.string "alias" - t.string "email" - t.integer "site_id" - t.integer "country_id" - t.boolean "activated", :default => false + t.string "login", :limit => 10 + t.string "full_name" + t.string "hashed_password" + t.string "salt", :limit => 5 + t.string "alias" + t.string "email" + t.integer "site_id" + t.integer "country_id" + t.boolean "activated", :default => false + t.datetime "created_at" + t.datetime "updated_at" end add_index "users", ["login"], :name => "index_users_on_login", :unique => true diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -82,6 +82,14 @@ font-size: 13px; } +div.errorExplanation { + border: 1px dashed black; + color: black; + padding: 5px; + margin-bottom: 5px; + background-color: white; +} + /******************************* [Settings] ********************************/ diff --git a/script/spec b/script/spec --- a/script/spec +++ b/script/spec @@ -1,4 +1,5 @@ #!/usr/bin/env ruby $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../vendor/plugins/rspec/lib")) +require 'rubygems' require 'spec' exit ::Spec::Runner::CommandLine.run(::Spec::Runner::OptionParser.parse(ARGV, STDERR, STDOUT)) diff --git a/spec/controllers/main_controller_spec.rb b/spec/controllers/main_controller_spec.rb --- a/spec/controllers/main_controller_spec.rb +++ b/spec/controllers/main_controller_spec.rb @@ -4,7 +4,7 @@ describe MainController do before(:each) do - @problem = mock(Problem, :name => 'test') + @problem = mock(Problem, :name => 'test', :output_only => false) @language = mock(Language, :name => 'cpp', :ext => 'cpp') @submission = mock(Submission, :id => 1, @@ -14,6 +14,7 @@ :source => 'sample source', :compiler_message => 'none') @user = mock(User, :id => 1, :login => 'john') + @another_user = mock(User, :id => 2, :login => 'mary') end it "should redirect user to login page when unlogged-in user try to access main/list" do @@ -23,18 +24,21 @@ it "should let user sees her own source" do Submission.should_receive(:find).with(@submission.id.to_s).and_return(@submission) + User.should_receive(:find).with(1).and_return(@user) get 'source', {:id => @submission.id}, {:user_id => 1} response.should be_success end it "should let user sees her own compiler message" do Submission.should_receive(:find).with(@submission.id.to_s).and_return(@submission) + User.should_receive(:find).with(1).and_return(@user) get 'compiler_msg', {:id => @submission.id}, {:user_id => 1} response.should be_success end it "should not let user sees other user's source" do Submission.should_receive(:find).with(@submission.id.to_s).and_return(@submission) + User.should_receive(:find).with(2).and_return(@another_user) get 'source', {:id => @submission.id}, {:user_id => 2} flash[:notice].should =~ /[Ee]rror/ response.should redirect_to(:action => 'list') @@ -42,6 +46,7 @@ it "should not let user sees other user's compiler message" do Submission.should_receive(:find).with(@submission.id.to_s).and_return(@submission) + User.should_receive(:find).with(2).and_return(@another_user) get 'compiler_msg', {:id => @submission.id}, {:user_id => 2} flash[:notice].should =~ /[Ee]rror/ response.should redirect_to(:action => 'list') diff --git a/spec/controllers/test_controller_spec.rb b/spec/controllers/test_controller_spec.rb --- a/spec/controllers/test_controller_spec.rb +++ b/spec/controllers/test_controller_spec.rb @@ -7,7 +7,7 @@ @john = mock(User, :id => "1", :login => 'john') @john_result = mock(TestRequest, :id => "1", :user_id => @john.id) @mary_result = mock(TestRequest, :id => "2", :user_id => @john.id + '1') - User.should_receive(:find).with(@john.id).and_return(@john) + User.should_receive(:find).at_least(:once).with(@john.id).and_return(@john) end it "should let user see her testing result" do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb new file mode 100644 --- /dev/null +++ b/spec/controllers/users_controller_spec.rb @@ -0,0 +1,95 @@ + +require File.dirname(__FILE__) + '/../spec_helper' + +describe UsersController, "when a new user registers" do + + before(:each) do + # create john + + @john_info = {:login => 'john', + :full_name => 'John John', + :email => 'john@space.com'} + @john = User.new(@john_info) + + @john_activation_key = "123456" + + @john.should_receive(:activation_key). + any_number_of_times. + and_return(@john_activation_key) + + get :new + response.should render_template('users/new') + end + + it "should show the new form again when user information is invalid" do + User.should_receive(:new).with(any_args()).and_return(@john) + @john.should_receive(:activated=).with(false) + @john.should_receive(:valid?).and_return(false) + @john.should_not_receive(:save) + + post :register, :login => @john_info[:login], + :full_name => @john_info[:full_name], + :email => @john_info[:email] + + response.should render_template('users/new') + end + + it "should create unactivated user and send e-mail with activation key" do + User.should_receive(:new).with(any_args()).and_return(@john) + @john.should_receive(:activated=).with(false) + @john.should_receive(:valid?).and_return(true) + @john.should_receive(:save).and_return(true) + + smtp_mock = mock("smtp") + smtp_mock.should_receive(:send_message) do |msg,fr,to| + to.should == [@john_info[:email]] + msg.index(@john_activation_key).should_not be_nil + end + + Net::SMTP.should_receive(:start). + with(any_args()). + and_yield(smtp_mock) + + post :register, :login => @john_info[:login], + :full_name => @john_info[:full_name], + :email => @john_info[:email] + + response.should render_template('users/new_splash') + end + + it "should create unactivated user and return error page when e-mail sending error" do + User.should_receive(:new).with(any_args()).and_return(@john) + @john.should_receive(:activated=).with(false) + @john.should_receive(:valid?).and_return(true) + @john.should_receive(:save).and_return(true) + + smtp_mock = mock("smtp") + smtp_mock.should_receive(:send_message). + and_throw(:error) + + Net::SMTP.should_receive(:start). + with(any_args()). + and_yield(smtp_mock) + + post :register, :login => @john_info[:login], + :full_name => @john_info[:full_name], + :email => @john_info[:email] + + response.should render_template('users/email_error') + end + + it "should activate user with valid activation key" do + login = @john_info[:login] + User.should_receive(:find_by_login). + with(login). + and_return(@john) + @john.should_receive(:valid?).and_return(true) + @john.should_receive(:activated=).with(true) + @john.should_receive(:save).and_return(true) + + get :confirm, :login => login, :activation => @john_activation_key + + response.should render_template('users/confirm') + end + +end diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb --- a/spec/models/site_spec.rb +++ b/spec/models/site_spec.rb @@ -4,39 +4,44 @@ describe Site do before(:each) do - start_time = Time.local(2008,5,10,9,00) + start_time = Time.local(2008,5,10,9,00).gmtime @site = Site.new({:name => 'Test site', :started => true, :start_time => start_time }) + @site.stub!(:start_time). + any_number_of_times. + and_return(start_time) + @site.stub!(:started).any_number_of_times.and_return(true) end it "should report that the contest is not finished when the contest time limit is not set" do Configuration.should_receive(:[]).with('contest.time_limit'). and_return('unlimit') - Time.should_not_receive(:now) @site.finished?.should == false end it "should report that the contest is finished when the contest is over" do Configuration.should_receive(:[]).with('contest.time_limit'). - and_return('5:00') - Time.should_receive(:now).and_return(Time.local(2008,5,10,14,01)) - @site.finished?.should == true - end + and_return('5:00') + Time.stub!(:now). + and_return(Time.local(2008,5,10,14,01).gmtime) + @site.finished?.should == true end it "should report if the contest is finished correctly, when the contest is over, and the contest time contains some minutes" do Configuration.should_receive(:[]).twice.with('contest.time_limit'). and_return('5:15') - Time.should_receive(:now). - and_return(Time.local(2008,5,10,14,14),Time.local(2008,5,10,14,16)) + Time.stub!(:now). + and_return(Time.local(2008,5,10,14,14)) @site.finished?.should == false + Time.stub!(:now). + and_return(Time.local(2008,5,10,14,16)) @site.finished?.should == true end it "should report that the contest is not finished, when the time is exactly at the finish time" do Configuration.should_receive(:[]).with('contest.time_limit'). and_return('5:00') - Time.should_receive(:now).and_return(Time.local(2008,5,10,14,00)) + Time.stub!(:now).and_return(Time.local(2008,5,10,14,00)) @site.finished?.should == false end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -48,7 +48,48 @@ it "should not accept invalid activation key" do @john.verify_activation_key("12345").should == false end - +end + +describe User, "when re-register with the same e-mail" do + + before(:each) do + @mary_email = 'mary@in.th' + + @time_first_register = Time.local(2008,5,10,9,00).gmtime + + @mary_first = mock_model(User, + :login => 'mary1', + :password => 'hello', + :email => @mary_email, + :created_at => @time_first_register) + @mary_second = User.new(:login => 'mary2', + :password => 'hello', + :email => @mary_email) + User.stub!(:find_by_email). + with(@mary_email, {:order => "created_at DESC"}). + and_return(@mary_first) + end + + class User + public :enough_time_interval_between_same_email_registrations + end + + it "should not be allowed if the time interval is less than 5 mins" do + time_now = @time_first_register + 4.minutes + Time.stub!(:now).and_return(time_now) + + @mary_second.enough_time_interval_between_same_email_registrations + @mary_second.errors.length.should_not be_zero + end + + it "should be allowed if the time interval is more than 5 mins" do + time_now = @time_first_register + 6.minutes + Time.stub!(:now).and_return(time_now) + + @mary_second.enough_time_interval_between_same_email_registrations + @mary_second.errors.length.should be_zero + end + end describe User, "as a class" do