From ada1571a533a718357307a2ba91407cdc326dace Mon Sep 17 00:00:00 2001 From: joshua stein Date: Sun, 1 Jul 2012 13:31:31 -0500 Subject: [PATCH] invitation system, user settings --- app/assets/stylesheets/application.css | 2 +- app/controllers/invitations_controller.rb | 19 +++ app/controllers/login_controller.rb | 11 +- app/controllers/settings_controller.rb | 18 +++ app/controllers/signup_controller.rb | 35 +++++- app/controllers/stories_controller.rb | 4 +- app/helpers/invitations_helper.rb | 2 + app/helpers/settings_helper.rb | 2 + app/models/comment.rb | 10 +- app/models/invitation.rb | 28 +++++ app/models/invitation_mailer.rb | 10 ++ app/models/story.rb | 3 +- app/models/user.rb | 5 +- app/views/comments/_comment.html.erb | 5 +- app/views/global/_header.html.erb | 5 +- app/views/home/rss.erb | 2 +- .../invitation_mailer/invitation.text.erb | 9 ++ app/views/login/index.html.erb | 2 +- .../password_reset_link.text.erb | 8 +- app/views/settings/index.html.erb | 108 ++++++++++++++++++ app/views/signup/index.html.erb | 39 +------ app/views/signup/invited.html.erb | 51 +++++++++ app/views/users/show.html.erb | 6 + config/application.rb | 2 + config/environments/development.rb | 2 + config/routes.rb | 6 + .../20120701154453_create_invitations.rb | 12 ++ .../20120701160006_reply_notifications.rb | 11 ++ db/migrate/20120701181319_invitation_memo.rb | 8 ++ db/schema.rb | 28 +++-- 30 files changed, 380 insertions(+), 73 deletions(-) create mode 100644 app/controllers/invitations_controller.rb create mode 100644 app/controllers/settings_controller.rb create mode 100644 app/helpers/invitations_helper.rb create mode 100644 app/helpers/settings_helper.rb create mode 100644 app/models/invitation.rb create mode 100644 app/models/invitation_mailer.rb create mode 100644 app/views/invitation_mailer/invitation.text.erb create mode 100644 app/views/settings/index.html.erb create mode 100644 app/views/signup/invited.html.erb create mode 100644 db/migrate/20120701154453_create_invitations.rb create mode 100644 db/migrate/20120701160006_reply_notifications.rb create mode 100644 db/migrate/20120701181319_invitation_memo.rb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 2ffa044..a2cffbf 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -90,7 +90,7 @@ textarea { border: 1px solid #ccc; } input[type="checkbox"] { - margin-top: 10px; + margin-top: 0.5em; } select { border: 1px solid #ccc; diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb new file mode 100644 index 0000000..f2996c4 --- /dev/null +++ b/app/controllers/invitations_controller.rb @@ -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 diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index e4e44da..96a7bd9 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -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 "/" diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb new file mode 100644 index 0000000..4d0a0df --- /dev/null +++ b/app/controllers/settings_controller.rb @@ -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 diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb index 81834a6..c2f2848 100644 --- a/app/controllers/signup_controller.rb +++ b/app/controllers/signup_controller.rb @@ -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 diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 0f33a20..04d5c50 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -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 diff --git a/app/helpers/invitations_helper.rb b/app/helpers/invitations_helper.rb new file mode 100644 index 0000000..1483b9e --- /dev/null +++ b/app/helpers/invitations_helper.rb @@ -0,0 +1,2 @@ +module InvitationsHelper +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb new file mode 100644 index 0000000..ffbedba --- /dev/null +++ b/app/helpers/settings_helper.rb @@ -0,0 +1,2 @@ +module SettingsHelper +end diff --git a/app/models/comment.rb b/app/models/comment.rb index 9d5b21d..5244e20 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -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 = {} diff --git a/app/models/invitation.rb b/app/models/invitation.rb new file mode 100644 index 0000000..28963e7 --- /dev/null +++ b/app/models/invitation.rb @@ -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 diff --git a/app/models/invitation_mailer.rb b/app/models/invitation_mailer.rb new file mode 100644 index 0000000..90108d3 --- /dev/null +++ b/app/models/invitation_mailer.rb @@ -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 ", + subject: "[Lobsters] Welcome to Lobsters") + end +end diff --git a/app/models/story.rb b/app/models/story.rb index a956af9..6db1bcd 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index a76d03b..409195d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index d5bddca..9a0776a 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -22,8 +22,9 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? <%= comment.user.username %> - <%= 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 <%= comment.current_vote && comment.current_vote[:vote] == -1 ? diff --git a/app/views/global/_header.html.erb b/app/views/global/_header.html.erb index 84bf6d1..1201140 100644 --- a/app/views/global/_header.html.erb +++ b/app/views/global/_header.html.erb @@ -1,8 +1,7 @@ diff --git a/app/views/password_reset/password_reset_link.text.erb b/app/views/password_reset/password_reset_link.text.erb index 42aaea8..985d829 100644 --- a/app/views/password_reset/password_reset_link.text.erb +++ b/app/views/password_reset/password_reset_link.text.erb @@ -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 %> diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb new file mode 100644 index 0000000..6a5c6d8 --- /dev/null +++ b/app/views/settings/index.html.erb @@ -0,0 +1,108 @@ +
+
+ View Profile +
+ +
+ Invite a New User +
+ + <%= form_tag "/invitations", :method => :post do |f| %> +
+ <%= label_tag :email, "E-mail Address:", :class => "required" %> + <%= text_field_tag :email, "", :size => 30 %> +
+ +
+ <%= label_tag :memo, "Memo to User:", :class => "required" %> + <%= text_field_tag :memo, "", :size => 60 %> +
+ +
+

+ <%= submit_tag "Send Invitation" %> +
+ <% end %> + +
+ +
+ Account Settings +
+ + <%= form_for @edit_user, :url => settings_url, :method => :post do |f| %> + <%= error_messages_for f.object %> + +
+ <%= f.label :username, "Username:", :class => "required" %> + <%= f.text_field :username %> + + [A-Za-z0-9][A-Za-z0-9_-]* + +
+ +
+ <%= f.label :email, "E-mail Address:", :class => "required" %> + <%= f.text_field :email %> +
+ +
+ <%= f.label :password, "New Password:", :class => "required" %> + <%= f.password_field :password %> +
+ +
+ <%= f.label :password_confirmation, "Confirm Password:", + :class => "required" %> + <%= f.password_field :password_confirmation %> +
+ +
+ <%= f.label :about, "About:", :class => "required" %> + <%= f.text_area :about, :size => "100x5" %> +
+ +
+
+
+ Limited Markdown formatting available +
+ +
+ + <%= render :partial => "global/markdownhelp" %> +
+
+ +
+ Reply Notification Settings +
+ +
+ <%= f.label :email_replies, "E-mail:", :class => "required" %> + <%= f.check_box :email_replies %> +
+ +
+ <%= f.label :pushover_replies, raw("" + + "Pushover:"), :class => "required" %> + <%= f.check_box :pushover_replies %> +
+ +
+ <%= f.label :pushover_user_key, "Pushover User Key:", + :class => "required" %> + <%= f.text_field :pushover_user_key, :size => 40 %> +
+ +
+ <%= f.label :pushover_device, "Pushover Device:", + :class => "required" %> + <%= f.text_field :pushover_device, :placeholder => "optional", + :size => 15 %> +
+ +
+ <%= f.submit "Save All Settings" %> + <% end %> +
diff --git a/app/views/signup/index.html.erb b/app/views/signup/index.html.erb index 1ed7d3b..0d34777 100644 --- a/app/views/signup/index.html.erb +++ b/app/views/signup/index.html.erb @@ -3,40 +3,5 @@ Create an Account - <%= form_for @new_user, { :url => signup_url, - :autocomplete => "off" } do |f| %> -

- 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. -

- - <%= error_messages_for(@new_user) %> - -

- <%= f.label :username, "Username:" %> - <%= f.text_field :username, :size => 30 %> - - [A-Za-z0-9][A-Za-z0-9_-]* - -
- - <%= f.label :email, "E-mail Address:" %> - <%= f.email_field :email, :size => 30 %> -
- - <%= f.label :password, "Password:" %> - <%= f.password_field :password, :size => 30 %> -
- - <%= f.label :password_confirmation, "Password (again):" %> - <%= f.password_field :password_confirmation, :size => 30 %> -
-

- -

- <%= submit_tag "Signup" %> -

- <% end %> - + Signup is currently by invitation only. + diff --git a/app/views/signup/invited.html.erb b/app/views/signup/invited.html.erb new file mode 100644 index 0000000..ff8d933 --- /dev/null +++ b/app/views/signup/invited.html.erb @@ -0,0 +1,51 @@ +
+
+ Create an Account +
+ + <%= form_for @new_user, { :url => signup_url, + :autocomplete => "off" } do |f| %> + <%= hidden_field_tag "invitation_code", @invitation.code %> + +

+ 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. +

+ + <%= error_messages_for(@new_user) %> + +

+ <%= f.label :invitation, "Invited By:" %> + + <%= @invitation.user.username %> + +

+ +

+ <%= f.label :username, "Username:" %> + <%= f.text_field :username, :size => 30 %> + + [A-Za-z0-9][A-Za-z0-9_-]* + +
+ + <%= f.label :email, "E-mail Address:" %> + <%= f.email_field :email, :size => 30 %> +
+ + <%= f.label :password, "Password:" %> + <%= f.password_field :password, :size => 30 %> +
+ + <%= f.label :password_confirmation, "Password (again):" %> + <%= f.password_field :password_confirmation, :size => 30 %> +
+

+ +

+ <%= submit_tag "Signup" %> +

+ <% end %> +
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index af39f24..9980cb6 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,4 +1,10 @@
+ <% if @user && @user.id == @showing_user.id %> +
+ Edit Settings +
+ <% end %> +
<%= @showing_user.username %>
diff --git a/config/application.rb b/config/application.rb index a00b355..9856295 100644 --- a/config/application.rb +++ b/config/application.rb @@ -57,3 +57,5 @@ module Lobsters config.assets.version = '1.0' end end + +Rails.application.routes.default_url_options[:host] = "lobste.rs" diff --git a/config/environments/development.rb b/config/environments/development.rb index 430e967..9733c26 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 40e8841..4101765 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20120701154453_create_invitations.rb b/db/migrate/20120701154453_create_invitations.rb new file mode 100644 index 0000000..f571af0 --- /dev/null +++ b/db/migrate/20120701154453_create_invitations.rb @@ -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 diff --git a/db/migrate/20120701160006_reply_notifications.rb b/db/migrate/20120701160006_reply_notifications.rb new file mode 100644 index 0000000..811bf3e --- /dev/null +++ b/db/migrate/20120701160006_reply_notifications.rb @@ -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 diff --git a/db/migrate/20120701181319_invitation_memo.rb b/db/migrate/20120701181319_invitation_memo.rb new file mode 100644 index 0000000..8ddb4f0 --- /dev/null +++ b/db/migrate/20120701181319_invitation_memo.rb @@ -0,0 +1,8 @@ +class InvitationMemo < ActiveRecord::Migration + def up + add_column :invitations, :memo, :text + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index bddb094..16c6ab0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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