Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

finished beta invitation system

  • Loading branch information...
commit 25c338e61088cc0867bd647fee0de8df4744f2d5 1 parent d15de95
activefx authored
Showing with 254 additions and 26 deletions.
  1. +6 −2 README
  2. +22 −0 app/controllers/admin/invite_actions_controller.rb
  3. +37 −0 app/controllers/admin/invites_controller.rb
  4. +16 −0 app/controllers/admin/mailings_controller.rb
  5. +1 −1  app/controllers/sessions_controller.rb
  6. +3 −3 app/controllers/user/activations_controller.rb
  7. +4 −0 app/helpers/application_helper.rb
  8. +31 −2 app/models/invitation.rb
  9. +2 −2 app/models/user_observer.rb
  10. +3 −0  app/views/admin/controls/index.html.erb
  11. +19 −0 app/views/admin/invite_actions/index.html.erb
  12. +9 −0 app/views/admin/invites/edit.html.erb
  13. +25 −0 app/views/admin/invites/index.html.erb
  14. +2 −1  app/views/admin/roles/index.html.erb
  15. +3 −0  app/views/admin/users/_user.html.erb
  16. +7 −0 app/views/admin/users/index.html.erb
  17. +1 −1  app/views/openid_sessions/new.html.erb
  18. +1 −0  app/views/root/index.html.erb
  19. +2 −2 app/views/sessions/new.html.erb
  20. +3 −0  app/views/shared/_user_bar.html.erb
  21. +1 −1  app/views/user/activations/{edit.html.erb → new.html.erb}
  22. +5 −1 app/views/user/profiles/show.html.erb
  23. +2 −1  config/config.yml.sample
  24. +2 −2 config/environment.rb
  25. +4 −1 config/routes.rb
  26. +1 −0  db/migrate/20080911154835_create_invitations.rb
  27. +2 −0  db/schema.rb
  28. +40 −6 lib/authentication/user_abstraction.rb
View
8 README
@@ -25,7 +25,8 @@ CURRENT FEATURES
- Set roles, activate, enable / disable users
- Member list and public profiles for logged in users
- Activation, with option to resend activation code
- - Beta invitation system with easy on/off functionality
+ - Beta invitation system
+ - easy on/off functionality, add/remove invites, send emails to pending users
- Forgot Password / Reset Password
- Change Password
- Failed login attempts database logging
@@ -45,6 +46,8 @@ CURRENT FEATURES
- auto_migrations
- Testing
- rspec, rspec_rails
+ - Gems
+ - ruby-openid-2.1.2
KNOWN ISSUES
- View and Layout notes for the rails-footnotes plugin do not work due to changes to ActionView::Base
@@ -56,7 +59,6 @@ KNOWN ISSUES
TODO
- Fix known issues
- Full rSpec test suite
- - Set up admin interface on a subdomain and require a second password
- Better access and permission denied redirects
- Make the ActivationsController "activate" action restful
- Integrate user interface plugins / dry form builders
@@ -113,6 +115,8 @@ Exception Logger:
- http://railscasts.com/episodes/104
Beta Invitations:
- http://railscasts.com/episodes/124-beta-invitations
+ar_mailer gem:
+ - http://www.ameravant.com/posts/sending-tons-of-emails-in-ruby-on-rails-with-ar_mailer
Configuration File:
- http://railscasts.com/episodes/85-yaml-configuration-file
- https://peepcode.com/products/draft-rails-code-review-pdf
View
22 app/controllers/admin/invite_actions_controller.rb
@@ -0,0 +1,22 @@
+class Admin::InviteActionsController < ApplicationController
+ before_filter :login_required
+ require_role :admin
+
+ # Show users waiting for an invitation code
+ def index
+ @users = Invitation.pending_users(params[:page])
+ end
+
+ # Add invitations to all users
+ def create
+ if User.add_to_invitation_limit(params[:add_invites].to_i)
+ flash[:notice] = "Invitation limit updated."
+ redirect_to admin_users_path
+ else
+ flash.now[:error] = "There was a problem updating the invitation limit."
+ render :action => 'index'
+ end
+ end
+
+end
+
View
37 app/controllers/admin/invites_controller.rb
@@ -0,0 +1,37 @@
+class Admin::InvitesController < ApplicationController
+ before_filter :login_required
+ require_role :admin
+
+ def index
+ end
+
+ # Remove invitations from all current users
+ def create
+ if params[:remove_invites]
+ User.remove_all_invitations
+ flash[:notice] = "Invitation limit updated."
+ redirect_to admin_users_path
+ else
+ flash.now[:error] = "There was a problem updating the invitation limit."
+ render :action => 'index'
+ end
+ end
+
+ # Edit a user's invitation limit
+ def edit
+ @user = User.find_by_login(params[:id])
+ end
+
+ def update
+ @user = User.find_by_login(params[:id])
+ @user.invitation_limit = params[:user][:invitation_limit]
+ if @user.save
+ flash[:notice] = "Invitation limit updated."
+ redirect_to :action => 'edit', :id => params[:id]
+ else
+ flash.now[:error] = "There was a problem updating the invitation limit."
+ render :action => 'edit'
+ end
+ end
+
+end
View
16 app/controllers/admin/mailings_controller.rb
@@ -0,0 +1,16 @@
+class Admin::MailingsController < ApplicationController
+ before_filter :login_required
+ require_role :admin
+
+ # Send invite emails
+ def create
+ if Invitation.send_to_pending_users(params[:limit].to_i)
+ flash[:notice] = "Sending invitations."
+ redirect_to admin_invites_path
+ else
+ flash[:error] = "There was a problem sending the invitations."
+ redirect_to admin_invites_path
+ end
+ end
+
+end
View
2  app/controllers/sessions_controller.rb
@@ -60,7 +60,7 @@ def open_id_authentication(identity_url_params)
authenticate_with_open_id(identity_url_params,
:optional => [ :nickname, :email, :fullname],
:invitation_token => params[:invitation_token],
- :remember_me => params[:remember_me]) do |result, identity_url, registration, extensions|
+ :remember_me => params[:remember_me]) do |result, identity_url, registration|
case result.status
when :missing
failed_login("Sorry, the OpenID server couldn't be found.", identity_url, true)
View
6 app/controllers/user/activations_controller.rb
@@ -7,7 +7,7 @@ def activate
if user = User.find_with_activation_code(params[:activation_code])
user.activate!
flash[:notice] = "Signup complete! Please sign in to continue."
- redirect_to login_path
+ (user.user_type == "SiteUser") ? (redirect_to login_path) : (redirect_to login_with_openid_path)
else
logger.warn "Invalid activation code from #{request.remote_ip} at #{Time.now.utc}"
flash[:error] = "We couldn't find a user with that activation code, please check your email and try again, or %s."
@@ -24,11 +24,11 @@ def activate
end
# Enter email address to resend activation
- def edit
+ def new
end
# Resend activation action
- def update
+ def create
begin
if !params[:email].blank? && User.send_new_activation_code(params[:email])
flash[:notice] = "A new activation code has been sent to your email address."
View
4 app/helpers/application_helper.rb
@@ -55,4 +55,8 @@ def unless_in_beta?
yield unless in_beta?
end
+ def if_invites_available?
+ yield if in_beta? and logged_in? and (current_user.invitation_limit > 0)
+ end
+
end
View
33 app/models/invitation.rb
@@ -2,7 +2,12 @@ class Invitation < ActiveRecord::Base
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'
- validates_presence_of :email
+ validates_presence_of :email
+ validates_uniqueness_of :email, :message => '^This email address has already been submitted'
+ validates_length_of :email, :within => 6..100
+ validates_format_of :email, :with => Authentication.email_regex,
+ :message => Authentication.bad_email_message
+
validate :recipient_is_not_registered
validate :sender_has_invitations, :if => :sender
@@ -11,6 +16,30 @@ class Invitation < ActiveRecord::Base
attr_accessible :email
+ def self.pending_users(page)
+ paginate :all,
+ :per_page => 50, :page => page,
+ :conditions => ['sent_at IS NULL'],
+ :order => 'created_at ASC'
+ end
+
+ def self.users_for_mailing(limit)
+ return nil if ((limit > 20) || (limit < 1))
+ find :all,
+ :conditions => ['sent_at IS NULL'],
+ :order => 'created_at ASC',
+ :limit => limit
+ end
+
+ def self.send_to_pending_users(limit)
+ if users = Invitation.users_for_mailing(limit)
+ users.each do |user|
+ UserMailer.deliver_invitation(user)
+ end
+ return true
+ end
+ end
+
private
def recipient_is_not_registered
@@ -24,7 +53,7 @@ def sender_has_invitations
end
def generate_token
- self.token = User.make_token #Digest::SHA1.hexdigest([Time.now, rand].join)
+ self.token = User.make_token
end
def decrement_sender_count
View
4 app/models/user_observer.rb
@@ -3,11 +3,11 @@ class UserObserver < ActiveRecord::Observer
#def after_create(user)
#end
- def after_save(user)
+ def after_save(user)
UserMailer.deliver_activation(user) if user.recently_activated?
UserMailer.deliver_forgot_password(user) if user.recently_forgot_password?
UserMailer.deliver_reset_password(user) if user.recently_reset_password?
- UserMailer.deliver_signup_notification(user) if user.lost_activation_code?
+ UserMailer.deliver_signup_notification(user) if (user.recently_created? || user.lost_activation_code?)
end
end
View
3  app/views/admin/controls/index.html.erb
@@ -2,5 +2,8 @@
<ul>
<li><%= link_to "Administer Users (#{@users})", admin_users_path %></li>
+ <% if_in_beta? do -%>
+ <li><%= link_to 'Beta Invitations', admin_invites_path %></li>
+ <% end -%>
<li><%= link_to "Logged Exceptions (#{@exceptions})", logged_exceptions_path %></li>
</ul>
View
19 app/views/admin/invite_actions/index.html.erb
@@ -0,0 +1,19 @@
+<h1>Users waiting to sign up (<%=h @users.total_entries %>)</h1>
+ <p><%= link_to 'User Administration', admin_users_path %> |
+<%= link_to 'Manage Beta Invitations for All Users', admin_invites_path %></p>
+
+<p>
+<% form_tag url_for(:controller => 'mailings', :action => 'create') do -%>
+Send invitations to users who have signed up on the front page for the private beta. Recommended maximum of 20 (enforced in the find) sent emails at a time, if you consistantly use that many or need more you should implement the ar_mailer gem or a background process.<br/>
+ Number of invitations to send (max 20): <%= text_field_tag 'limit' %><br/>
+ <%= submit_tag 'Send invitations' %>
+<% end -%>
+</p>
+
+<ul>
+ <% for user in @users -%>
+ <li><%=h user.email %></li>
+ <% end -%>
+</ul>
+
+<%= will_paginate @users %>
View
9 app/views/admin/invites/edit.html.erb
@@ -0,0 +1,9 @@
+<h1>Edit invitations for <%= @user.login %></h1>
+<p><%= link_to 'Back to user administration', admin_users_path %>.</p>
+<%= error_messages_for :user %>
+<% form_for :user, :url => admin_invite_path(@user),
+ :html => { :method => :put },
+ :builder => Uberkit::Forms::Builder do |f| -%>
+ <%= f.text_field :invitation_limit %>
+ <%= f.submit "Update" %>
+<% end -%>
View
25 app/views/admin/invites/index.html.erb
@@ -0,0 +1,25 @@
+<h1>Invitations</h1>
+<p><%= link_to 'User Administration', admin_users_path %> |
+<%= link_to 'Pending Beta Users', admin_invite_actions_path %></p>
+<h2>Add invitations to all users</h2>
+
+<% form_tag url_for(:controller => 'invite_actions', :action => 'create') do -%>
+Add number of invites to users: <%= text_field_tag 'add_invites' %>
+ <%= submit_tag 'Update invite limits' %>
+<% end -%>
+
+<h2>Remove invitations from all users</h2>
+
+<% form_tag url_for(:action => 'create') do -%>
+Reset all invitation limits to zero. Users with invitation codes will still be able to sign up and have the default number of invitations availabe, unless you also set new_user_invite_limit to zero in the config file.<br/>
+ Check for confirmation: <%= check_box_tag 'remove_invites' %> Remove all invites?<br/>
+ <%= submit_tag 'Reset invite limits' %>
+<% end -%>
+
+<h2>Send invitations to pending beta users</h2>
+
+<% form_tag url_for(:controller => 'mailings', :action => 'create') do -%>
+Send invitations to users who have signed up on the front page for the private beta. Recommended maximum of 20 (enforced in the find) sent emails at a time, if you consistantly use that many or need more you should implement the ar_mailer gem or a background process.<br/>
+ Number of invitations to send (max 20): <%= text_field_tag 'limit' %><br/>
+ <%= submit_tag 'Send invitations' %>
+<% end -%>
View
3  app/views/admin/roles/index.html.erb
@@ -1,6 +1,7 @@
<h1>Roles for <%=h @user.login %></h1>
<% form_for @user, :url => { :action => "update", :id => @user } do |f| -%>
-<%= f.hidden_field :created_at %>
+<%= hidden_field_tag "user[created_at]", @user.created_at %>
+
<ul>
<% for role in @roles %>
<li><%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role) %> <%= role.name %></li>
View
3  app/views/admin/users/_user.html.erb
@@ -16,6 +16,9 @@
<% end %>
</td>
<td><%= link_to 'edit roles', admin_user_roles_path(user) %></td>
+ <% if_in_beta? do -%>
+ <td><%=h user.invitation_limit %> <%= link_to 'edit', edit_admin_invite_path(user) %></td>
+ <% end -%>
</tr>
View
7 app/views/admin/users/index.html.erb
@@ -1,4 +1,8 @@
<h1>All Users</h1>
+<% if_in_beta? do -%>
+ <p><%= link_to 'Manage Beta Invitations for All Users', admin_invites_path %> |
+<%= link_to 'Pending Beta Users', admin_invite_actions_path %></p>
+<% end -%>
<table>
<tr>
<th>Login</th>
@@ -6,6 +10,9 @@
<th>Active?</th>
<th>Enabled?</th>
<th>Roles</th>
+ <% if_in_beta? do -%>
+ <th>Invitations</th>
+ <% end -%>
</tr>
<%= render :partial => 'user', :collection => @users %>
</table>
View
2  app/views/openid_sessions/new.html.erb
@@ -1,5 +1,5 @@
<h1>Log in with OpenID</h1>
-
+<p><%= link_to "Login with Username and Password", login_path %></p>
<% form_tag session_path do -%>
<%= hidden_field_tag 'openid', true %>
View
1  app/views/root/index.html.erb
@@ -1,5 +1,6 @@
<% content_for :side do -%>
<% unless logged_in? -%><% if_in_beta? do -%>
+ <%= error_messages_for :invitation %>
<p>We are currently in private beta. Please enter your email address below and we will let know when an invitation becomes available.</p>
<% uberform_for [:user, Invitation.new] do |f| -%>
<%= f.text_field :email, :label => "Your Email:" %>
View
4 app/views/sessions/new.html.erb
@@ -1,7 +1,7 @@
<h1>Log in with your account</h1>
-
+<p><%= link_to "Login with OpenID", login_with_openid_path %></p>
<% form_tag session_path do -%>
-<p><%= label_tag 'login' %><br />
+<p><%= label_tag 'login', "Username" %><br />
<%= text_field_tag 'login', @login %></p>
<p><%= label_tag 'password' %><br/>
View
3  app/views/shared/_user_bar.html.erb
@@ -1,6 +1,9 @@
<% if logged_in? -%>
<div id="user-bar-greeting">Logged in as: <%= link_to_current_user :content_method => :login %> |
<%= link_to 'My Account', user_profile_path(current_user), { :title => "My account" } %> |
+ <% if_invites_available? do -%>
+ <%= link_to "Invite Friends (#{current_user.invitation_limit})", new_user_invitation_path %> |
+ <% end -%>
<%= link_to "Log Out", logout_path, { :title => "Log out" } %> </div>
<% else -%>
<div id="user-bar-greeting">Log in with:
View
2  app/views/user/activations/edit.html.erb → app/views/user/activations/new.html.erb
@@ -1,5 +1,5 @@
<h1>Resend activation</h1>
-<% form_tag url_for(:action => 'update') do %>
+<% form_tag user_activations_path do %>
What is the email address used to create your account?<br />
<%= text_field_tag :email, "", :size => 50 %><br />
<%= submit_tag 'Resend activation code' %>
View
6 app/views/user/profiles/show.html.erb
@@ -1,5 +1,9 @@
<h1>My Profile: <%=h @user.login %> <%= link_to '(edit)', edit_user_profile_path(current_user) %></h1>
-<p>Joined on: <%= @user.created_at.to_s(:long) %></p>
+<% if_invites_available? do -%>
+ <p>You have <%=h @user.invitation_limit %> invitations left. <%= link_to "Invite friends to join",
+ new_user_invitation_path %>.</p>
+<% end -%>
+<p>Joined on: <%=h @user.created_at.to_s(:long) %></p>
<p>Name: <%=h @user.name %></p>
<p>Username: <%=h @user.login %></p>
<p>Email: <%=h @user.email %></p>
View
3  config/config.yml.sample
@@ -8,6 +8,7 @@ development:
admin_email: yourpersonalemail@example.com
in_beta: true
new_user_invite_limit: 5
+ max_user_invite_limit: 100
rest_auth:
site_key: e587f9d09baa59c920b9ee97ac70f58b3c51356c
stretches: 10
@@ -16,7 +17,7 @@ development:
port: 25
domain: yourapplication.com
authentication: :login
- user_name: emailaccount@yourapplication.com
+ user_name: email_account_username_or_address@yourapplication.com
password: emailaccountpassword
sender: donotreply@yourapplication.com
recaptcha:
View
4 config/environment.rb
@@ -1,5 +1,6 @@
# Be sure to restart your server when you modify this file
require 'yaml'
+
# Uncomment below to force Rails into production mode when
# you don't control web/app server and can't set it the proper way
# ENV['RAILS_ENV'] ||= 'production'
@@ -19,7 +20,7 @@
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
# See Rails::Configuration for more options.
-
+ #require 'action_mailer/ar_mailer'
# Skip frameworks you're not going to use. To use Rails without a database
# you must remove the Active Record framework.
# config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
@@ -30,7 +31,6 @@
# config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
# config.gem "aws-s3", :lib => "aws/s3"
-
# Only load the plugins named here, in the order given. By default, all plugins
# in vendor/plugins are loaded in alphabetical order.
# :all can be used as a placeholder for all plugins not explicitly named
View
5 config/routes.rb
@@ -11,10 +11,13 @@
:action => 'activate', :activation_code => nil
map.forgot_password '/forgot_password', :controller => 'user/passwords', :action => 'new'
map.reset_password '/reset_password/:id', :controller => 'user/passwords', :action => 'edit', :id => nil
- map.resend_activation '/resend_activation', :controller => 'user/activations', :action => 'edit'
+ map.resend_activation '/resend_activation', :controller => 'user/activations', :action => 'new'
map.namespace :admin do |admin|
admin.resources :controls
+ admin.resources :invite_actions
+ admin.resources :invites
+ admin.resources :mailings
admin.resources :users, :member => { :enable => :put } do |users|
users.resources :roles
end
View
1  db/migrate/20080911154835_create_invitations.rb
@@ -4,6 +4,7 @@ def self.up
t.integer :sender_id
t.string :email, :token
t.datetime :sent_at
+ t.timestamps
end
add_index :invitations, :token, :unique => true
View
2  db/schema.rb
@@ -27,6 +27,8 @@
t.string "email"
t.string "token"
t.datetime "sent_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
end
add_index "invitations", ["token"], :name => "index_invitations_on_token", :unique => true
View
46 lib/authentication/user_abstraction.rb
@@ -33,8 +33,14 @@ def self.included( recipient )
validates_format_of :email, :with => Authentication.email_regex,
:message => Authentication.bad_email_message
- validates_presence_of :invitation_id, :message => 'is required', :if => :site_in_beta?
- validates_uniqueness_of :invitation_id, :if => :site_in_beta?
+ validates_presence_of :invitation_id, :message => 'is required',
+ :on => :create,
+ :if => :site_in_beta?
+ validates_uniqueness_of :invitation_id, :on => :create, :if => :site_in_beta?
+
+ validates_numericality_of :invitation_limit,
+ :less_than_or_equal_to => APP_CONFIG['settings']['max_user_invite_limit'],
+ :on => :update
before_create :set_invitation_limit
before_create :make_activation_code
@@ -98,6 +104,22 @@ def find_for_forget(email)
return false if (email.blank? || u.nil? || (!u.identity_url.blank? && u.password.blank?))
(u.forgot_password && u.save) ? true : false
end
+
+ def add_to_invitation_limit(number)
+ users = find :all, :conditions => ['enabled = ? and activated_at IS NOT NULL', true]
+ users.each do |u|
+ u.add_invites(number)
+ u.save(false)
+ end
+ end
+
+ def remove_all_invitations
+ users = find :all
+ users.each do |u|
+ u.invitation_limit = 0
+ u.save(false)
+ end
+ end
end # class methods
@@ -185,13 +207,27 @@ def recently_reset_password?
@reset_password
end
+ def recently_created?
+ @created
+ end
+
def site_in_beta?
APP_CONFIG['settings']['in_beta']
end
def emails_match?
+ return false if self.invitation.nil?
self.email == self.invitation.email
end
+
+ def add_invites(number)
+ self.invitation_limit || (self.invitation_limit = 0)
+ if ((self.invitation_limit + number) > APP_CONFIG['settings']['max_user_invite_limit'])
+ self.invitation_limit = APP_CONFIG['settings']['max_user_invite_limit']
+ else
+ self.invitation_limit += number
+ end
+ end
protected
@@ -199,11 +235,9 @@ def make_activation_code
self.activation_code = self.class.make_token
if site_in_beta? && emails_match?
self.activated_at = Time.now
- # Uncomment if you'd like UserMailer to deliver
- # the activation.erb email
- # @activated = true
+ @activated = true
else
- UserMailer.deliver_signup_notification(self)
+ @created = true
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.