invitation system, user settings

This commit is contained in:
joshua stein 2012-07-01 13:31:31 -05:00
parent 22ae6bc1b2
commit ada1571a53
30 changed files with 380 additions and 73 deletions

View file

@ -90,7 +90,7 @@ textarea {
border: 1px solid #ccc;
}
input[type="checkbox"] {
margin-top: 10px;
margin-top: 0.5em;
}
select {
border: 1px solid #ccc;

View file

@ -0,0 +1,19 @@
class InvitationsController < ApplicationController
before_filter :require_logged_in_user
def create
i = Invitation.new
i.user_id = @user.id
i.email = params[:email]
i.memo = params[:memo]
if i.save
flash[:success] = "Successfully e-mailed invitation to " <<
params[:email]
else
flash[:error] = "Could not send invitation, verify the e-mail " <<
"address is valid."
end
return redirect_to "/settings"
end
end

View file

@ -21,7 +21,7 @@ class LoginController < ApplicationController
return redirect_to "/"
end
flash[:error] = "Invalid e-mail address and/or password."
flash.now[:error] = "Invalid e-mail address and/or password."
index
end
@ -35,13 +35,14 @@ class LoginController < ApplicationController
params[:email]).first
if !@found_user
flash[:error] = "Invalid e-mail address or username."
flash.now[:error] = "Invalid e-mail address or username."
return forgot_password
end
@found_user.initiate_password_reset_for_ip(request.remote_ip)
flash[:success] = "Password reset instructions have been e-mailed to you."
flash.now[:success] = "Password reset instructions have been e-mailed " <<
"to you."
return index
end
@ -56,9 +57,11 @@ class LoginController < ApplicationController
if !params[:password].blank?
@reset_user.password = params[:password]
@reset_user.password_confirmation = params[:password_confirmation]
@reset_user.session_token = nil
@reset_user.password_reset_token = nil
# this will get reset upon save
@reset_user.session_token = nil
if @reset_user.save
session[:u] = @reset_user.session_token
return redirect_to "/"

View file

@ -0,0 +1,18 @@
class SettingsController < ApplicationController
before_filter :require_logged_in_user
def index
@edit_user = @user.dup
end
def update
@edit_user = @user.clone
if @edit_user.update_attributes(params[:user])
flash.now[:success] = "Successfully updated settings"
@user = @edit_user
end
render :action => "index"
end
end

View file

@ -1,17 +1,48 @@
class SignupController < ApplicationController
def index
@page_title = "Signup"
if @user
flash[:error] = "You are already signed up."
return redirect_to "/"
end
@title = "Signup"
@page_title = "Create an Account"
end
def invited
if @user
flash[:error] = "You are already signed up."
return redirect_to "/"
end
@title = "Signup"
@page_title = "Create an Account"
if !(@invitation = Invitation.find_by_code(params[:invitation_code]))
flash[:error] = "Invalid or expired invitation"
return redirect_to "/signup"
end
@new_user = User.new
render :action => "invited"
end
def signup
if !(@invitation = Invitation.find_by_code(params[:invitation_code]))
flash[:error] = "Invalid or expired invitation"
return redirect_to "/signup"
end
@new_user = User.new(params[:user])
@new_user.invited_by_user_id = @invitation.user_id
if @new_user.save
@invitation.destroy
session[:u] = @new_user.session_token
flash[:success] = "Welcome to Lobsters, #{@new_user.username}"
return redirect_to "/"
else
render :action => "index"
render :action => "invited"
end
end
end

View file

@ -162,8 +162,8 @@ private
end
if !@story
flash[:error] = "Could not find story or you are not authorized to " <<
"manage it."
flash[:error] = "Could not find story or you are not authorized " <<
"to manage it."
redirect_to "/"
return false
end

View file

@ -0,0 +1,2 @@
module InvitationsHelper
end

View file

@ -0,0 +1,2 @@
module SettingsHelper
end

View file

@ -24,7 +24,7 @@ class Comment < ActiveRecord::Base
self.story_id.blank? &&
errors.add(:story_id, "cannot be blank.")
(m = self.comment.to_s.strip.match(/\A(t)his([\.!])?$\z/i)) &&
errors.add(:base, (m[1] == "T" ? "N" : "n") + "ope" + m[2].to_s)
end
@ -80,14 +80,14 @@ class Comment < ActiveRecord::Base
Markdowner.markdown(self.comment)
end
def upvote!(amount = 1)
Story.update_counters self.id, :upvotes => amount
end
def flag!
Story.update_counters self.id, :flaggings => 1
end
def has_been_edited?
self.updated_at && (self.updated_at - self.created_at > 1.minute)
end
def self.ordered_for_story_or_thread_for_user(story_id, thread_id, user_id)
parents = {}

28
app/models/invitation.rb Normal file
View file

@ -0,0 +1,28 @@
class Invitation < ActiveRecord::Base
belongs_to :user
validate do
if !email.to_s.match(/\A[^@]+@[^@]+\.[^@]+\z/)
errors.add(:email, "is not valid")
end
end
before_create :create_code
after_create :send_email
def create_code
(1...10).each do |tries|
if tries == 10
raise "too many hash collisions"
end
if !Invitation.find_by_code(self.code = Utils.random_str(15))
break
end
end
end
def send_email
InvitationMailer.invitation(self).deliver
end
end

View file

@ -0,0 +1,10 @@
class InvitationMailer < ActionMailer::Base
default from: "nobody@lobste.rs"
def invitation(invitation)
@invitation = invitation
mail(to: invitation.email, from: "Lobsters Invitation <nobody@lobste.rs>",
subject: "[Lobsters] Welcome to Lobsters")
end
end

View file

@ -80,7 +80,8 @@ class Story < ActiveRecord::Base
end
def comments_url
"/s/#{self.short_id}/#{self.title_as_url}"
Rails.application.routes.url_helpers.root_url +
"s/#{self.short_id}/#{self.title_as_url}"
end
@_comment_count = nil

View file

@ -12,7 +12,8 @@ class User < ActiveRecord::Base
validates_presence_of :password, :on => :create
attr_accessible :username, :email, :password, :password_confirmation,
:email_notifications
:about, :email_replies, :pushover_replies, :pushover_user_key,
:pushover_device
before_save :check_session_token
@ -40,7 +41,7 @@ class User < ActiveRecord::Base
end
def initiate_password_reset_for_ip(ip)
self.password_reset_token = Utils.random_str(60)
self.password_reset_token = Utils.random_str(45)
self.save!
PasswordReset.password_reset_link(self, ip).deliver

View file

@ -22,8 +22,9 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
<a href="/u/<%= comment.user.username %>"><%= comment.user.username
%></a>
<%= comment.updated_at ? "edited" : "" %>
<%= time_ago_in_words(comment.created_at).gsub(/^about /, "") %> ago
<%= comment.has_been_edited?? "edited" : "" %>
<%= time_ago_in_words(comment.has_been_edited?? comment.updated_at :
comment.created_at).gsub(/^about /, "") %> ago
<span class="reason"><%= comment.current_vote &&
comment.current_vote[:vote] == -1 ?

View file

@ -1,8 +1,7 @@
<div id="header">
<div id="headerright" class="<%= @user ? "loggedin" : "" %>">
<% if @user %>
<a href="/u/<%= @user.username %>"><%= @user.username
%> (<%= @user.karma %>)</a>
<a href="/settings"><%= @user.username %> (<%= @user.karma %>)</a>
<% if false %>
<% if (count = @user.unread_message_count) > 0 %>
@ -16,8 +15,6 @@
{ :confirm => "Are you sure you want to logout?",
"method" => "post" } %>
<% else %>
<a href="/signup">Signup</a>
or
<a href="/login">Login</a>
<% end %>
</div>

View file

@ -4,7 +4,7 @@
<channel>
<title>lobste.rs<%= @page_title ? ": " + h(@page_title) : "" %></title>
<description><%= @page_title %></description>
<link>http://lobste.rs/<%= @newest ? "newest" : "" %></link>
<link><%= root_url + (@newest ? "newest" : "") %></link>
<% @stories.each do |story| %>
<item>

View file

@ -0,0 +1,9 @@
Hello <%= @invitation.email %>,
The user <%= @invitation.user.username %> has invited you to the website Lobsters:
<%= @invitation.memo %>
To create an account, visit the URL below:
<%= root_url %>invitations/<%= @invitation.code %>

View file

@ -24,7 +24,7 @@
</p>
<p>
Not signed up yet? <%= link_to "Signup", signup_url %>.
Not a user yet? Lobsters is currently invitation-only.
</p>
<% end %>
</div>

View file

@ -1,7 +1,7 @@
Hello <%= @user.email %>,
Someone at <%= @ip %> requested to reset your account password.
If you submitted this request, visit the link below to set a new password.
If not, you can disregard this e-mail.
Someone at <%= @ip %> requested to reset your account password
on lobste.rs. If you submitted this request, visit the link below to
set a new password. If not, you can disregard this e-mail.
http://lobste.rs/login/set_new_password?token=<%= @user.password_reset_token %>
<%= root_url %>login/set_new_password?token=<%= @user.password_reset_token %>

View file

@ -0,0 +1,108 @@
<div class="box wide">
<div style="float: right;">
<a href="/u/<%= @user.username %>">View Profile</a>
</div>
<div class="legend">
Invite a New User
</div>
<%= form_tag "/invitations", :method => :post do |f| %>
<div class="boxline">
<%= label_tag :email, "E-mail Address:", :class => "required" %>
<%= text_field_tag :email, "", :size => 30 %>
</div>
<div class="boxline">
<%= label_tag :memo, "Memo to User:", :class => "required" %>
<%= text_field_tag :memo, "", :size => 60 %>
</div>
<div class="boxline">
<p></p>
<%= submit_tag "Send Invitation" %>
</div>
<% end %>
<br>
<div class="legend">
Account Settings
</div>
<%= form_for @edit_user, :url => settings_url, :method => :post do |f| %>
<%= error_messages_for f.object %>
<div class="boxline">
<%= f.label :username, "Username:", :class => "required" %>
<%= f.text_field :username %>
<span class="hint">
<tt>[A-Za-z0-9][A-Za-z0-9_-]*</tt>
</span>
</div>
<div class="boxline">
<%= f.label :email, "E-mail Address:", :class => "required" %>
<%= f.text_field :email %>
</div>
<div class="boxline">
<%= f.label :password, "New Password:", :class => "required" %>
<%= f.password_field :password %>
</div>
<div class="boxline">
<%= f.label :password_confirmation, "Confirm Password:",
:class => "required" %>
<%= f.password_field :password_confirmation %>
</div>
<div class="boxline">
<%= f.label :about, "About:", :class => "required" %>
<%= f.text_area :about, :size => "100x5" %>
</div>
<div class="box">
<div class="boxline markdown_help_toggler" style="margin-left: 9em;">
<div class="markdown_help_label">
<span class="fakea">Limited Markdown formatting available</span>
</div>
<div style="clear: both;"></div>
<%= render :partial => "global/markdownhelp" %>
</div>
</div>
<div class="legend">
Reply Notification Settings
</div>
<div class="boxline">
<%= f.label :email_replies, "E-mail:", :class => "required" %>
<%= f.check_box :email_replies %>
</div>
<div class="boxline">
<%= f.label :pushover_replies, raw("<a href=\"https://pushover.net/\">" +
"Pushover</a>:"), :class => "required" %>
<%= f.check_box :pushover_replies %>
</div>
<div class="boxline">
<%= f.label :pushover_user_key, "Pushover User Key:",
:class => "required" %>
<%= f.text_field :pushover_user_key, :size => 40 %>
</div>
<div class="boxline">
<%= f.label :pushover_device, "Pushover Device:",
:class => "required" %>
<%= f.text_field :pushover_device, :placeholder => "optional",
:size => 15 %>
</div>
<br>
<%= f.submit "Save All Settings" %>
<% end %>
</div>

View file

@ -3,40 +3,5 @@
Create an Account
</div>
<%= form_for @new_user, { :url => signup_url,
:autocomplete => "off" } do |f| %>
<p>
To create a new account, enter your e-mail address and a password.
Your e-mail address will never be shown to users and will only be used
if you need to reset your password, or to receive optional new-message
alerts.
</p>
<%= error_messages_for(@new_user) %>
<p>
<%= f.label :username, "Username:" %>
<%= f.text_field :username, :size => 30 %>
<span class="hint">
<tt>[A-Za-z0-9][A-Za-z0-9_-]*</tt>
</span>
<br />
<%= f.label :email, "E-mail Address:" %>
<%= f.email_field :email, :size => 30 %>
<br />
<%= f.label :password, "Password:" %>
<%= f.password_field :password, :size => 30 %>
<br />
<%= f.label :password_confirmation, "Password (again):" %>
<%= f.password_field :password_confirmation, :size => 30 %>
<br />
</p>
<p>
<%= submit_tag "Signup" %>
</p>
<% end %>
</fieldset>
Signup is currently by invitation only.
</div>

View file

@ -0,0 +1,51 @@
<div class="box wide">
<div class="legend">
Create an Account
</div>
<%= form_for @new_user, { :url => signup_url,
:autocomplete => "off" } do |f| %>
<%= hidden_field_tag "invitation_code", @invitation.code %>
<p>
To create a new account, enter your e-mail address and a password.
Your e-mail address will never be shown to users and will only be used
if you need to reset your password, or to receive optional new-message
alerts.
</p>
<%= error_messages_for(@new_user) %>
<p>
<%= f.label :invitation, "Invited By:" %>
<span class="d">
<%= @invitation.user.username %>
</span>
</p>
<p>
<%= f.label :username, "Username:" %>
<%= f.text_field :username, :size => 30 %>
<span class="hint">
<tt>[A-Za-z0-9][A-Za-z0-9_-]*</tt>
</span>
<br />
<%= f.label :email, "E-mail Address:" %>
<%= f.email_field :email, :size => 30 %>
<br />
<%= f.label :password, "Password:" %>
<%= f.password_field :password, :size => 30 %>
<br />
<%= f.label :password_confirmation, "Password (again):" %>
<%= f.password_field :password_confirmation, :size => 30 %>
<br />
</p>
<p>
<%= submit_tag "Signup" %>
</p>
<% end %>
</div>

View file

@ -1,4 +1,10 @@
<div class="box wide">
<% if @user && @user.id == @showing_user.id %>
<div style="float: right;">
<a href="/settings">Edit Settings</a>
</div>
<% end %>
<div class="legend">
<%= @showing_user.username %>
</div>

View file

@ -57,3 +57,5 @@ module Lobsters
config.assets.version = '1.0'
end
end
Rails.application.routes.default_url_options[:host] = "lobste.rs"

View file

@ -35,3 +35,5 @@ Lobsters::Application.configure do
# Expands the lines which load the assets
config.assets.debug = true
end
Rails.application.routes.default_url_options[:host] = "lobsters.localhost:3000"

View file

@ -41,4 +41,10 @@ Lobsters::Application.routes.draw do
get "/u/:id" => "users#show"
get "/rss" => "home#index", :format => "rss"
get "/settings" => "settings#index"
post "/settings" => "settings#update"
post "/invitations" => "invitations#create"
get "/invitations/:invitation_code" => "signup#invited"
end

View file

@ -0,0 +1,12 @@
class CreateInvitations < ActiveRecord::Migration
def change
create_table :invitations do |t|
t.integer :user_id
t.string :email
t.string :code
t.timestamps
end
add_column :users, :invited_by_user_id, :integer
end
end

View file

@ -0,0 +1,11 @@
class ReplyNotifications < ActiveRecord::Migration
def up
add_column :users, :email_replies, :boolean, :default => false
add_column :users, :pushover_replies, :boolean, :default => false
add_column :users, :pushover_user_key, :string
add_column :users, :pushover_device, :string
end
def down
end
end

View file

@ -0,0 +1,8 @@
class InvitationMemo < ActiveRecord::Migration
def up
add_column :invitations, :memo, :text
end
def down
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 0) do
ActiveRecord::Schema.define(:version => 20120701181319) do
create_table "comments", :force => true do |t|
t.datetime "created_at", :null => false
@ -27,9 +27,18 @@ ActiveRecord::Schema.define(:version => 0) do
end
add_index "comments", ["short_id"], :name => "short_id", :unique => true
add_index "comments", ["story_id", "short_id"], :name => "story_id_short_id"
add_index "comments", ["story_id", "short_id"], :name => "story_id"
add_index "comments", ["thread_id"], :name => "thread_id"
create_table "invitations", :force => true do |t|
t.integer "user_id"
t.string "email"
t.string "code"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.text "memo"
end
create_table "keystores", :primary_key => "key", :force => true do |t|
t.integer "value", :null => false
end
@ -66,7 +75,7 @@ ActiveRecord::Schema.define(:version => 0) do
t.integer "tag_id", :null => false
end
add_index "taggings", ["story_id", "tag_id"], :name => "story_id_tag_id", :unique => true
add_index "taggings", ["story_id", "tag_id"], :name => "story_id", :unique => true
create_table "tags", :force => true do |t|
t.string "tag", :limit => 25, :default => "", :null => false
@ -81,10 +90,15 @@ ActiveRecord::Schema.define(:version => 0) do
t.string "password_digest", :limit => 75
t.datetime "created_at"
t.integer "email_notifications", :limit => 1, :default => 0
t.integer "is_admin", :limit => 1, :default => 0, :null => false
t.integer "is_admin", :limit => 1, :default => 0, :null => false
t.string "password_reset_token", :limit => 75
t.string "session_token", :limit => 75, :default => "", :null => false
t.string "session_token", :limit => 75, :default => "", :null => false
t.text "about"
t.integer "invited_by_user_id"
t.boolean "email_replies", :default => false
t.boolean "pushover_replies", :default => false
t.string "pushover_user_key"
t.string "pushover_device"
end
add_index "users", ["session_token"], :name => "session_hash", :unique => true
@ -98,7 +112,7 @@ ActiveRecord::Schema.define(:version => 0) do
t.string "reason", :limit => 1
end
add_index "votes", ["user_id", "comment_id"], :name => "user_id_comment_id"
add_index "votes", ["user_id", "story_id"], :name => "user_id_story_id"
add_index "votes", ["user_id", "comment_id"], :name => "user_id_2"
add_index "votes", ["user_id", "story_id"], :name => "user_id"
end