From 46abddacad629e38e72f7b4d3ee9c3e83efcdf76 Mon Sep 17 00:00:00 2001
From: hungz23
Date: Fri, 8 Jul 2016 16:30:22 +0700
Subject: [PATCH 1/5] FInish user edit, update, index, and destroy actions
---
Gemfile | 3 ++
Gemfile.lock | 8 +++
app/assets/stylesheets/custom.scss | 11 ++++
app/controllers/sessions_controller.rb | 2 +-
app/controllers/users_controller.rb | 46 ++++++++++++++++
app/helpers/sessions_helper.rb | 14 +++++
app/helpers/users_helper.rb | 12 +++--
app/models/user.rb | 2 +-
app/views/layouts/_header.html.erb | 2 +-
app/views/users/_user.html.erb | 8 +++
app/views/users/edit.html.erb | 28 +++++++---
app/views/users/index.html.erb | 33 +++---------
.../20160708090312_add_admin_to_users.rb | 5 ++
db/schema.rb | 7 +--
db/seeds.rb | 14 +++++
test/controllers/users_controller_test.rb | 47 ++++++++++++++++
test/fixtures/users.yml | 21 +++++++-
test/integration/users_edit_test.rb | 54 +++++++++++++++++++
test/integration/users_index_test.rb | 33 ++++++++++++
test/models/user_test.rb | 8 +--
20 files changed, 310 insertions(+), 48 deletions(-)
create mode 100644 app/views/users/_user.html.erb
create mode 100644 db/migrate/20160708090312_add_admin_to_users.rb
create mode 100644 test/integration/users_edit_test.rb
create mode 100644 test/integration/users_index_test.rb
diff --git a/Gemfile b/Gemfile
index 2ab2238..06ba47e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,6 +5,9 @@ source 'https://rubygems.org'
gem 'rails', '4.2.6'
gem 'bootstrap-sass', '3.3.6'
gem 'bcrypt', '3.1.11'
+gem 'faker', '1.4.2'
+gem 'will_paginate', '3.0.7'
+gem 'bootstrap-will_paginate', '0.0.10'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
diff --git a/Gemfile.lock b/Gemfile.lock
index 457b8df..d46815b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -45,6 +45,8 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
+ bootstrap-will_paginate (0.0.10)
+ will_paginate
builder (3.2.2)
byebug (9.0.5)
coffee-rails (4.1.1)
@@ -58,6 +60,8 @@ GEM
debug_inspector (0.0.2)
erubis (2.7.0)
execjs (2.7.0)
+ faker (1.4.2)
+ i18n (~> 0.5)
globalid (0.3.6)
activesupport (>= 4.1.0)
i18n (0.7.0)
@@ -147,6 +151,7 @@ GEM
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
+ will_paginate (3.0.7)
PLATFORMS
ruby
@@ -154,8 +159,10 @@ PLATFORMS
DEPENDENCIES
bcrypt (= 3.1.11)
bootstrap-sass (= 3.3.6)
+ bootstrap-will_paginate (= 0.0.10)
byebug
coffee-rails (~> 4.1.0)
+ faker (= 1.4.2)
jbuilder (~> 2.0)
jquery-rails
rails (= 4.2.6)
@@ -166,6 +173,7 @@ DEPENDENCIES
turbolinks
uglifier (>= 1.3.0)
web-console (~> 2.0)
+ will_paginate (= 3.0.7)
BUNDLED WITH
1.12.5
diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss
index 4987e07..138fa8e 100644
--- a/app/assets/stylesheets/custom.scss
+++ b/app/assets/stylesheets/custom.scss
@@ -195,4 +195,15 @@ input {
#session_remember_me {
width: auto;
margin-left: 0;
+}
+
+/* Users index */
+.users {
+ list-style: none;
+ margin: 0;
+ li {
+ overflow: auto;
+ padding: 10px 0;
+ border-bottom: 1px solid $gray-lighter;
+ }
}
\ No newline at end of file
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 35efad5..1d2de59 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -9,7 +9,7 @@ def create
# Log the user in and redirect to the user's show page.
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
- redirect_to user
+ redirect_back_or user
else
# Create an error message.
flash.now[:danger] = 'Invalid email/password combination'
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e39a836..c5853d0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,6 +1,14 @@
class UsersController < ApplicationController
+ before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
+ before_action :correct_user, only: [:edit, :update]
+ before_action :admin_user, only: :destroy
def new
end
+
+ def index
+ @users = User.paginate(page: params[:page])
+ end
+
def show
@user = User.find(params[:id])
end
@@ -20,8 +28,46 @@ def create
def is_logged_in?
!session[:user_id].nil?
end
+ def edit
+ @user = User.find(params[:id])
+ end
+
+ def update
+ @user = User.find(params[:id])
+ if @user.update_attributes(user_params)
+ # Handle a successful update.
+ flash[:success] = "Profile updated"
+ redirect_to @user
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ User.find(params[:id]).destroy
+ flash[:success] = "User deleted"
+ redirect_to users_url
+ end
+
private
def user_params
params.require(:user).permit(:name, :email, :password,:password_confirmation)
end
+ # Confirms a logged-in user.
+ def logged_in_user
+ unless logged_in?
+ store_location
+ flash[:danger] = "Please log in."
+ redirect_to login_url
+ end
+ end
+ # Confirms the correct user.
+ def correct_user
+ @user = User.find(params[:id])
+ redirect_to(root_url) unless current_user?(@user)
+ end
+ # Confirms an admin user.
+ def admin_user
+ redirect_to(root_url) unless current_user.admin?
+ end
end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index 8c1946b..b168db8 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -11,6 +11,10 @@ def remember(user)
cookies.permanent[:remember_token] = user.remember_token
end
+ def current_user?(user)
+ user == current_user
+ end
+
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
@@ -41,4 +45,14 @@ def log_out
session.delete(:user_id)
@current_user = nil
end
+
+ # Redirects to stored location (or to the default).
+ def redirect_back_or(default)
+ redirect_to(session[:forwarding_url] || default)
+ session.delete(:forwarding_url)
+ end
+# Stores the URL trying to be accessed.
+ def store_location
+ session[:forwarding_url] = request.url if request.get?
+ end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 36c7e23..3f5b4df 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -1,8 +1,10 @@
module UsersHelper
# Returns the Gravatar for the given user.
- def gravatar_for(user)
- gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
- gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
- image_tag(gravatar_url, alt: user.name, class: "gravatar")
- end
+ # Returns the Gravatar (http://gravatar.com/) for the given user.
+ def gravatar_for(user, options = { size: 50 })
+ gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
+ size = options[:size]
+ gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
+ image_tag(gravatar_url, alt: user.name, class: "gravatar")
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 0649f65..125c287 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -7,7 +7,7 @@ class User < ActiveRecord::Base
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
- validates :password, presence: true, length: { minimum: 6 }
+ validates :password, length: { minimum: 6 }, allow_blank: true
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
index 5c15c0e..8cbd222 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -6,7 +6,7 @@
<%= link_to "Home", root_path %>
<%= link_to "Help", help_path %>
<% if logged_in? %>
- <%= link_to "Users", '#' %>
+ <%= link_to "Users", users_path %>
Account
diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb
new file mode 100644
index 0000000..7f9c1d2
--- /dev/null
+++ b/app/views/users/_user.html.erb
@@ -0,0 +1,8 @@
+
+ <%= gravatar_for user, size: 50 %>
+ <%= link_to user.name, user %>
+ <% if current_user.admin? && !current_user?(user) %>
+ | <%= link_to "delete", user, method: :delete,
+ data: { confirm: "You sure?" } %>
+ <% end %>
+
\ No newline at end of file
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index d87b2f5..69d2575 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -1,6 +1,22 @@
-Editing User
-
-<%= render 'form' %>
-
-<%= link_to 'Show', @user %> |
-<%= link_to 'Back', users_path %>
+<% provide(:title, "Edit user") %>
+Update your profile
+
+
+ <%= form_for(@user) do |f| %>
+ <%= render 'shared/error_messages' %>
+ <%= f.label :name %>
+ <%= f.text_field :name, class: 'form-control' %>
+ <%= f.label :email %>
+ <%= f.email_field :email, class: 'form-control' %>
+ <%= f.label :password %>
+ <%= f.password_field :password, class: 'form-control' %>
+ <%= f.label :password_confirmation, "Confirmation" %>
+ <%= f.password_field :password_confirmation, class: 'form-control' %>
+ <%= f.submit "Save changes", class: "btn btn-primary" %>
+ <% end %>
+
+ <%= gravatar_for @user %>
+
change
+
+
+
\ No newline at end of file
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
index 32c0d02..3c7738b 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -1,29 +1,10 @@
-<%= notice %>
+<% provide(:title, 'All users') %>
+All users
-Listing Users
+<%= will_paginate %>
-
-
-
- Name
- Email
-
-
-
+
-
- <% @users.each do |user| %>
-
- <%= user.name %>
- <%= user.email %>
- <%= link_to 'Show', user %>
- <%= link_to 'Edit', edit_user_path(user) %>
- <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %>
-
- <% end %>
-
-
-
-
-
-<%= link_to 'New User', new_user_path %>
+<%= will_paginate %>
\ No newline at end of file
diff --git a/db/migrate/20160708090312_add_admin_to_users.rb b/db/migrate/20160708090312_add_admin_to_users.rb
new file mode 100644
index 0000000..e386d33
--- /dev/null
+++ b/db/migrate/20160708090312_add_admin_to_users.rb
@@ -0,0 +1,5 @@
+class AddAdminToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :admin, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a428c08..0a39043 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160708032244) do
+ActiveRecord::Schema.define(version: 20160708090312) do
create_table "microposts", force: :cascade do |t|
t.text "content"
@@ -23,10 +23,11 @@
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.string "password_digest"
t.string "remember_digest"
+ t.boolean "admin", default: false
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
diff --git a/db/seeds.rb b/db/seeds.rb
index 4edb1e8..d89ca2a 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -5,3 +5,17 @@
#
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first)
+User.create!(name: "Example User",
+ email: "example@railstutorial.org",
+ password: "foobar",
+ password_confirmation: "foobar",
+ admin: true)
+99.times do |n|
+ name = Faker::Name.name
+ email = "example-#{n+1}@railstutorial.org"
+ password = "password"
+ User.create!(name: name,
+ email: email,
+ password: password,
+ password_confirmation: password)
+end
\ No newline at end of file
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 7d2d9b1..276b26f 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -1,9 +1,56 @@
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
+ def setup
+ @user = users(:michael)
+ @other_user = users(:archer)
+ end
+ test "should redirect index when not logged in" do
+ get :index
+ assert_redirected_to login_url
+ end
test "should get new" do
get :new
assert_response :success
end
+ test "should redirect edit when not logged in" do
+ get :edit, id: @user
+ assert_not flash.empty?
+ assert_redirected_to login_url
+ end
+
+ test "should redirect update when not logged in" do
+ patch :update, id: @user, user: { name: @user.name, email: @user.email }
+ assert_not flash.empty?
+ assert_redirected_to login_url
+ end
+
+ test "should redirect edit when logged in as wrong user" do
+ log_in_as(@other_user)
+ get :edit, id: @user
+ assert flash.empty?
+ assert_redirected_to root_url
+ end
+
+ test "should redirect update when logged in as wrong user" do
+ log_in_as(@other_user)
+ patch :update, id: @user, user: { name: @user.name, email: @user.email }
+ assert flash.empty?
+ assert_redirected_to root_url
+ end
+
+ test "should redirect destroy when not logged in" do
+ assert_no_difference 'User.count' do
+ delete :destroy, id: @user
+ end
+ assert_redirected_to login_url
+ end
+ test "should redirect destroy when logged in as a non-admin" do
+ log_in_as(@other_user)
+ assert_no_difference 'User.count' do
+ delete :destroy, id: @user
+ end
+ assert_redirected_to root_url
+ end
end
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index 1294a78..3edd50c 100644
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -1,4 +1,23 @@
michael:
name: Michael Example
email: michael@example.com
- password_digest: <%= User.digest('password') %>
\ No newline at end of file
+ password_digest: <%= User.digest('password') %>
+ admin: true
+archer:
+ name: Sterling Archer
+ email: duchess@example.gov
+ password_digest: <%= User.digest('password') %>
+lana:
+ name: Lana Kane
+ email: hands@example.gov
+ password_digest: <%= User.digest('password') %>
+mallory:
+ name: Mallory Archer
+ email: boss@example.gov
+ password_digest: <%= User.digest('password') %>
+<% 30.times do |n| %>
+user_<%= n %>:
+ name: <%= "User #{n}" %>
+ email: <%= "user-#{n}@example.com" %>
+ password_digest: <%= User.digest('password') %>
+<% end %>
\ No newline at end of file
diff --git a/test/integration/users_edit_test.rb b/test/integration/users_edit_test.rb
new file mode 100644
index 0000000..68b0115
--- /dev/null
+++ b/test/integration/users_edit_test.rb
@@ -0,0 +1,54 @@
+require 'test_helper'
+
+class UsersEditTest < ActionDispatch::IntegrationTest
+ # test "the truth" do
+ # assert true
+ # end
+ def setup
+ @user = users(:michael)
+ end
+ test "unsuccessful edit" do
+ log_in_as(@user)
+ get edit_user_path(@user)
+ assert_template 'users/edit'
+ patch user_path(@user), user: { name: "",
+ email: "foo@invalid",
+ password: "foo",
+ password_confirmation: "bar" }
+ assert_template 'users/edit'
+ end
+ test "successful edit" do
+ log_in_as(@user)
+ get edit_user_path(@user)
+ assert_template 'users/edit'
+ name = "Foo Bar"
+ email = "foo@bar.com"
+ patch user_path(@user), user: { name: name,
+ email: email,
+ password: "",
+ password_confirmation: "" }
+ assert_not flash.empty?
+ assert_redirected_to @user
+ @user.reload
+ assert_equal @user.name, name
+ assert_equal @user.email, email
+ end
+
+
+ test "successful edit with friendly forwarding" do
+ get edit_user_path(@user)
+ log_in_as(@user)
+ assert_redirected_to edit_user_path(@user)
+ name = "Foo Bar"
+ email = "foo@bar.com"
+ patch user_path(@user), user: { name: name,
+ email: email,
+ password: "foobar",
+ password_confirmation: "foobar" }
+ assert_not flash.empty?
+ assert_redirected_to @user
+ @user.reload
+ assert_equal @user.name, name
+ assert_equal @user.email, email
+ end
+end
diff --git a/test/integration/users_index_test.rb b/test/integration/users_index_test.rb
new file mode 100644
index 0000000..91b7058
--- /dev/null
+++ b/test/integration/users_index_test.rb
@@ -0,0 +1,33 @@
+require 'test_helper'
+
+class UsersIndexTest < ActionDispatch::IntegrationTest
+ # test "the truth" do
+ # assert true
+ # end
+ def setup
+ @admin = users(:michael)
+ @non_admin = users(:archer)
+ end
+ test "index as admin including pagination and delete links" do
+ log_in_as(@admin)
+ get users_path
+ assert_template 'users/index'
+ assert_select 'div.pagination'
+ first_page_of_users = User.paginate(page: 1)
+ first_page_of_users.each do |user|
+ assert_select 'a[href=?]', user_path(user), text: user.name
+ unless user == @admin
+ assert_select 'a[href=?]', user_path(user), text: 'delete',
+ method: :delete
+ end
+ end
+ assert_difference 'User.count', -1 do
+ delete user_path(@non_admin)
+ end
+ end
+ test "index as non-admin" do
+ log_in_as(@non_admin)
+ get users_path
+ assert_select 'a', text: 'delete', count: 0
+ end
+end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 7841939..ea2c37a 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -56,10 +56,10 @@ def setup
assert_equal mixed_case_email.downcase, @user.reload.email
end
- test "password should be present (nonblank)" do
- @user.password = @user.password_confirmation = " " * 6
- assert_not @user.valid?
- end
+ #test "password should be present (nonblank)" do
+ # @user.password = @user.password_confirmation = " " * 6
+ # assert_not @user.valid?
+ #end
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
From 091416becc336a94f065a8a81dd29a2c80a3e538 Mon Sep 17 00:00:00 2001
From: hungz23
Date: Tue, 12 Jul 2016 16:51:37 +0700
Subject: [PATCH 2/5] Add user microposts
---
Gemfile | 4 +
Gemfile.lock | 38 +++++++
.../javascripts/account_activation.coffee | 3 +
.../stylesheets/account_activation.scss | 3 +
app/assets/stylesheets/custom.scss | 44 +++++++++
.../account_activation_controller.rb | 14 +++
app/controllers/application_controller.rb | 12 ++-
app/controllers/microposts_controller.rb | 99 +++++--------------
app/controllers/sessions_controller.rb | 17 ++--
app/controllers/static_pages_controller.rb | 7 +-
app/controllers/users_controller.rb | 7 +-
app/helpers/account_activation_helper.rb | 2 +
app/helpers/sessions_helper.rb | 2 +-
app/mailers/application_mailer.rb | 4 +
app/mailers/user_mailer.rb | 22 +++++
app/models/micropost.rb | 15 ++-
app/models/user.rb | 43 ++++++--
app/uploaders/picture_uploader.rb | 62 ++++++++++++
app/views/layouts/mailer.html.erb | 5 +
app/views/layouts/mailer.text.erb | 1 +
app/views/microposts/_micropost.html.erb | 13 +++
app/views/microposts/show.html.erb | 34 ++++---
app/views/shared/_error_messages.html.erb | 10 +-
app/views/shared/_feed.html.erb | 6 ++
app/views/shared/_micropost_form.html.erb | 18 ++++
app/views/shared/_user_info.html.erb | 4 +
app/views/static_pages/home.html.erb | 42 +++++---
.../user_mailer/account_activation.html.erb | 7 ++
.../user_mailer/account_activation.text.erb | 3 +
app/views/user_mailer/password_reset.html.erb | 5 +
app/views/user_mailer/password_reset.text.erb | 3 +
app/views/users/edit.html.erb | 2 +-
app/views/users/new.html.erb | 14 +--
app/views/users/show.html.erb | 25 +++--
config/environments/development.rb | 6 ++
config/environments/test.rb | 2 +-
config/initializers/carrier_wave.rb | 11 +++
config/routes.rb | 3 +-
.../20160711062804_add_activation_to_users.rb | 7 ++
...rb => 20160712070613_create_microposts.rb} | 4 +-
...0160712093058_add_picture_to_microposts.rb | 5 +
db/schema.rb | 15 ++-
db/seeds.rb | 14 ++-
.../account_activation_controller_test.rb | 7 ++
.../controllers/microposts_controller_test.rb | 72 +++++---------
test/controllers/users_controller_test.rb | 4 -
test/fixtures/microposts.yml | 49 +++++++--
test/fixtures/users.yml | 10 ++
test/integration/microposts_interface_test.rb | 37 +++++++
test/integration/users_profile_test.rb | 19 ++++
test/integration/users_signup_test.rb | 58 ++++++++---
test/mailers/previews/user_mailer_preview.rb | 15 +++
test/mailers/user_mailer_test.rb | 14 +++
test/models/micropost_test.rb | 24 +++++
test/models/user_test.rb | 10 +-
55 files changed, 747 insertions(+), 229 deletions(-)
create mode 100644 app/assets/javascripts/account_activation.coffee
create mode 100644 app/assets/stylesheets/account_activation.scss
create mode 100644 app/controllers/account_activation_controller.rb
create mode 100644 app/helpers/account_activation_helper.rb
create mode 100644 app/mailers/application_mailer.rb
create mode 100644 app/mailers/user_mailer.rb
create mode 100644 app/uploaders/picture_uploader.rb
create mode 100644 app/views/layouts/mailer.html.erb
create mode 100644 app/views/layouts/mailer.text.erb
create mode 100644 app/views/microposts/_micropost.html.erb
create mode 100644 app/views/shared/_feed.html.erb
create mode 100644 app/views/shared/_micropost_form.html.erb
create mode 100644 app/views/shared/_user_info.html.erb
create mode 100644 app/views/user_mailer/account_activation.html.erb
create mode 100644 app/views/user_mailer/account_activation.text.erb
create mode 100644 app/views/user_mailer/password_reset.html.erb
create mode 100644 app/views/user_mailer/password_reset.text.erb
create mode 100644 config/initializers/carrier_wave.rb
create mode 100644 db/migrate/20160711062804_add_activation_to_users.rb
rename db/migrate/{20160707012303_create_microposts.rb => 20160712070613_create_microposts.rb} (53%)
create mode 100644 db/migrate/20160712093058_add_picture_to_microposts.rb
create mode 100644 test/controllers/account_activation_controller_test.rb
create mode 100644 test/integration/microposts_interface_test.rb
create mode 100644 test/integration/users_profile_test.rb
create mode 100644 test/mailers/previews/user_mailer_preview.rb
create mode 100644 test/mailers/user_mailer_test.rb
diff --git a/Gemfile b/Gemfile
index 06ba47e..f0760a6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,6 +6,10 @@ gem 'rails', '4.2.6'
gem 'bootstrap-sass', '3.3.6'
gem 'bcrypt', '3.1.11'
gem 'faker', '1.4.2'
+gem 'carrierwave', '0.10.0'
+gem 'mini_magick', '3.8.0'
+gem 'fog', '1.23.0'
+gem 'net-ssh'
gem 'will_paginate', '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
# Use sqlite3 as the database for Active Record
diff --git a/Gemfile.lock b/Gemfile.lock
index d46815b..36690ab 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,6 +49,11 @@ GEM
will_paginate
builder (3.2.2)
byebug (9.0.5)
+ carrierwave (0.10.0)
+ activemodel (>= 3.2.0)
+ activesupport (>= 3.2.0)
+ json (>= 1.7)
+ mime-types (>= 1.16)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.1.x)
@@ -59,12 +64,37 @@ GEM
concurrent-ruby (1.0.2)
debug_inspector (0.0.2)
erubis (2.7.0)
+ excon (0.51.0)
execjs (2.7.0)
faker (1.4.2)
i18n (~> 0.5)
+ fog (1.23.0)
+ fog-brightbox
+ fog-core (~> 1.23)
+ fog-json
+ fog-softlayer
+ ipaddress (~> 0.5)
+ nokogiri (~> 1.5, >= 1.5.11)
+ fog-brightbox (0.11.0)
+ fog-core (~> 1.22)
+ fog-json
+ inflecto (~> 0.0.2)
+ fog-core (1.42.0)
+ builder
+ excon (~> 0.49)
+ formatador (~> 0.2)
+ fog-json (1.0.2)
+ fog-core (~> 1.0)
+ multi_json (~> 1.10)
+ fog-softlayer (1.1.2)
+ fog-core
+ fog-json
+ formatador (0.2.5)
globalid (0.3.6)
activesupport (>= 4.1.0)
i18n (0.7.0)
+ inflecto (0.0.2)
+ ipaddress (0.8.3)
jbuilder (2.5.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
@@ -80,9 +110,12 @@ GEM
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
+ mini_magick (3.8.0)
+ subexec (~> 0.2.1)
mini_portile2 (2.1.0)
minitest (5.9.0)
multi_json (1.12.1)
+ net-ssh (3.2.0)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
@@ -136,6 +169,7 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.11)
+ subexec (0.2.3)
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.5)
@@ -161,10 +195,14 @@ DEPENDENCIES
bootstrap-sass (= 3.3.6)
bootstrap-will_paginate (= 0.0.10)
byebug
+ carrierwave (= 0.10.0)
coffee-rails (~> 4.1.0)
faker (= 1.4.2)
+ fog (= 1.23.0)
jbuilder (~> 2.0)
jquery-rails
+ mini_magick (= 3.8.0)
+ net-ssh
rails (= 4.2.6)
sass-rails (~> 5.0)
sdoc (~> 0.4.0)
diff --git a/app/assets/javascripts/account_activation.coffee b/app/assets/javascripts/account_activation.coffee
new file mode 100644
index 0000000..24f83d1
--- /dev/null
+++ b/app/assets/javascripts/account_activation.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/account_activation.scss b/app/assets/stylesheets/account_activation.scss
new file mode 100644
index 0000000..709c646
--- /dev/null
+++ b/app/assets/stylesheets/account_activation.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the AccountActivation controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss
index 138fa8e..ad065ad 100644
--- a/app/assets/stylesheets/custom.scss
+++ b/app/assets/stylesheets/custom.scss
@@ -206,4 +206,48 @@ input {
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
+}
+
+/* microposts */
+.microposts {
+ list-style: none;
+ padding: 0;
+ li {
+ padding: 10px 0;
+ border-top: 1px solid #e8e8e8;
+ }
+ .user {
+ margin-top: 5em;
+ padding-top: 0;
+ }
+ .content {
+ display: block;
+ margin-left: 60px;
+ img {
+ display: block;
+ padding: 5px 0;
+ }
+ }
+ .timestamp {
+ color: $gray-light;
+ display: block;
+ margin-left: 60px;
+ }
+ .gravatar {
+ float: left;
+ margin-right: 10px;
+ margin-top: 5px;
+ }
+}
+aside {
+ textarea {
+ height: 100px;
+ margin-bottom: 5px;
+ }
+}
+span.picture {
+ margin-top: 10px;
+ input {
+ border: 0;
+ }
}
\ No newline at end of file
diff --git a/app/controllers/account_activation_controller.rb b/app/controllers/account_activation_controller.rb
new file mode 100644
index 0000000..b79c99e
--- /dev/null
+++ b/app/controllers/account_activation_controller.rb
@@ -0,0 +1,14 @@
+class AccountActivationsController < ApplicationController
+ def edit
+ user = User.find_by(email: params[:email])
+ if user && !user.activated? && user.authenticated?(:activation, params[:id])
+ user.activate
+ log_in user
+ flash[:success] = "Account activated!"
+ redirect_to user
+ else
+ flash[:danger] = "Invalid activation link"
+ redirect_to root_url
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0cfff7a..c1546ac 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,7 +3,13 @@ class ApplicationController < ActionController::Base
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
include SessionsHelper
- def hello
- render html: "hello, world!"
- end
+ private
+ # Confirms a logged-in user.
+ def logged_in_user
+ unless logged_in?
+ store_location
+ flash[:danger] = "Please log in."
+ redirect_to login_url
+ end
+ end
end
diff --git a/app/controllers/microposts_controller.rb b/app/controllers/microposts_controller.rb
index c36ac0b..8c647ee 100644
--- a/app/controllers/microposts_controller.rb
+++ b/app/controllers/microposts_controller.rb
@@ -1,74 +1,29 @@
class MicropostsController < ApplicationController
- before_action :set_micropost, only: [:show, :edit, :update, :destroy]
-
- # GET /microposts
- # GET /microposts.json
- def index
- @microposts = Micropost.all
- end
-
- # GET /microposts/1
- # GET /microposts/1.json
- def show
- end
-
- # GET /microposts/new
- def new
- @micropost = Micropost.new
- end
-
- # GET /microposts/1/edit
- def edit
- end
-
- # POST /microposts
- # POST /microposts.json
- def create
- @micropost = Micropost.new(micropost_params)
-
- respond_to do |format|
- if @micropost.save
- format.html { redirect_to @micropost, notice: 'Micropost was successfully created.' }
- format.json { render :show, status: :created, location: @micropost }
- else
- format.html { render :new }
- format.json { render json: @micropost.errors, status: :unprocessable_entity }
- end
- end
- end
-
- # PATCH/PUT /microposts/1
- # PATCH/PUT /microposts/1.json
- def update
- respond_to do |format|
- if @micropost.update(micropost_params)
- format.html { redirect_to @micropost, notice: 'Micropost was successfully updated.' }
- format.json { render :show, status: :ok, location: @micropost }
- else
- format.html { render :edit }
- format.json { render json: @micropost.errors, status: :unprocessable_entity }
- end
- end
- end
-
- # DELETE /microposts/1
- # DELETE /microposts/1.json
- def destroy
- @micropost.destroy
- respond_to do |format|
- format.html { redirect_to microposts_url, notice: 'Micropost was successfully destroyed.' }
- format.json { head :no_content }
- end
- end
-
- private
- # Use callbacks to share common setup or constraints between actions.
- def set_micropost
- @micropost = Micropost.find(params[:id])
- end
-
- # Never trust parameters from the scary internet, only allow the white list through.
- def micropost_params
- params.require(:micropost).permit(:content, :user_id)
- end
+ before_action :logged_in_user, only: [:create, :destroy]
+ before_action :correct_user, only: :destroy
+ def create
+ @micropost = current_user.microposts.build(micropost_params)
+ if @micropost.save
+ flash[:success] = "Micropost created!"
+ redirect_to root_url
+ else
+ @feed_items = []
+ render 'static_pages/home'
+ end
+ end
+
+ def destroy
+ @micropost.destroy
+ flash[:success] = "Micropost deleted"
+ redirect_to request.referrer || root_url
+ end
+ private
+ def micropost_params
+ params.require(:micropost).permit(:content, :picture)
+ end
+
+ def correct_user
+ @micropost = current_user.microposts.find_by(id: params[:id])
+ redirect_to root_url if @micropost.nil?
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1d2de59..137a474 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,17 +1,20 @@
class SessionsController < ApplicationController
-
def new
end
-
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
- # Log the user in and redirect to the user's show page.
- log_in user
- params[:session][:remember_me] == '1' ? remember(user) : forget(user)
- redirect_back_or user
+ if user.activated?
+ log_in user
+ params[:session][:remember_me] == '1' ? remember(user) : forget(user)
+ redirect_back_or user
+ else
+ message = "Account not activated. "
+ message += "Check your email for the activation link."
+ flash[:warning] = message
+ redirect_to root_url
+ end
else
- # Create an error message.
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb
index d304760..3a6b09f 100644
--- a/app/controllers/static_pages_controller.rb
+++ b/app/controllers/static_pages_controller.rb
@@ -1,7 +1,10 @@
class StaticPagesController < ApplicationController
def home
- end
-
+ if logged_in?
+ @micropost = current_user.microposts.build
+ @feed_items = current_user.feed.paginate(page: params[:page])
+ end
+ end
def help
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c5853d0..cd40966 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -11,6 +11,7 @@ def index
def show
@user = User.find(params[:id])
+ @microposts = @user.microposts.paginate(page: params[:page])
end
def new
@user = User.new
@@ -18,9 +19,9 @@ def new
def create
@user = User.new(user_params)
if @user.save
- log_in @user
- flash[:success] = "Welcome to the Sample App!"
- redirect_to @user
+ UserMailer.account_activation(@user).deliver_now
+ flash[:info] = "Please check your email to activate your account."
+ redirect_to root_url
else
render 'new'
end
diff --git a/app/helpers/account_activation_helper.rb b/app/helpers/account_activation_helper.rb
new file mode 100644
index 0000000..5264b58
--- /dev/null
+++ b/app/helpers/account_activation_helper.rb
@@ -0,0 +1,2 @@
+module AccountActivationHelper
+end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index b168db8..1b59ebe 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -21,7 +21,7 @@ def current_user
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
- if user && user.authenticated?(cookies[:remember_token])
+ if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
new file mode 100644
index 0000000..6b6185f
--- /dev/null
+++ b/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: "noreply@example.com"
+ layout 'mailer'
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
new file mode 100644
index 0000000..84bc9f8
--- /dev/null
+++ b/app/mailers/user_mailer.rb
@@ -0,0 +1,22 @@
+class UserMailer < ApplicationMailer
+
+ # Subject can be set in your I18n file at config/locales/en.yml
+ # with the following lookup:
+ #
+ # en.user_mailer.account_activation.subject
+ #
+ def account_activation(user)
+ @user = user
+ mail to: user.email, subject: "Account activation"
+ end
+ # Subject can be set in your I18n file at config/locales/en.yml
+ # with the following lookup:
+ #
+ # en.user_mailer.password_reset.subject
+ #
+ def password_reset
+ @greeting = "Hi"
+
+ mail to: "to@example.org"
+ end
+end
diff --git a/app/models/micropost.rb b/app/models/micropost.rb
index 1492773..2d4b0c4 100644
--- a/app/models/micropost.rb
+++ b/app/models/micropost.rb
@@ -1,4 +1,15 @@
class Micropost < ActiveRecord::Base
- belongs_to :user
- validates :content, length: {maximum: 140}, presence: true
+ belongs_to :user
+ default_scope -> {order(created_at: :desc)}
+ mount_uploader :picture, PictureUploader
+ validates :user_id, presence: true
+ validates :content, presence: true, length: { maximum: 140 }
+ validate :picture_size
+ private
+ # Validates the size of an uploaded picture.
+ def picture_size
+ if picture.size > 5.megabytes
+ errors.add(:picture, "should be less than 5MB")
+ end
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 125c287..c3c23b7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,8 @@
class User < ActiveRecord::Base
- attr_accessor :remember_token
- before_save { self.email = email.downcase }
+ has_many :microposts, dependent: :destroy
+ attr_accessor :remember_token, :activation_token
+ before_save :downcase_email
+ before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
@@ -23,13 +25,42 @@ def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
- # Returns true if the given token matches the digest.
- def authenticated?(remember_token)
- return false if remember_digest.nil?
- BCrypt::Password.new(remember_digest).is_password?(remember_token)
+# Returns true if the given token matches the digest.
+ def authenticated?(attribute, token)
+ digest = send("#{attribute}_digest")
+ return false if digest.nil?
+ BCrypt::Password.new(digest).is_password?(token)
end
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
+
+# Activates an account.
+ def activate
+ update_attribute(:activated, true)
+ update_attribute(:activated_at, Time.zone.now)
+ end
+
+ # Sends activation email.
+ def send_activation_email
+ UserMailer.account_activation(self).deliver_now
+ end
+
+ # Defines a proto-feed.
+ # See "Following users" for the full implementation.
+ def feed
+ Micropost.where("user_id = ?", id)
+ end
+ private
+ # Converts email to all lower-case.
+ def downcase_email
+ self.email = email.downcase
+ end
+ # Creates and assigns the activation token and digest.
+ def create_activation_digest
+ self.activation_token = User.new_token
+ self.activation_digest = User.digest(activation_token)
+ end
+
end
diff --git a/app/uploaders/picture_uploader.rb b/app/uploaders/picture_uploader.rb
new file mode 100644
index 0000000..2e376d3
--- /dev/null
+++ b/app/uploaders/picture_uploader.rb
@@ -0,0 +1,62 @@
+# encoding: utf-8
+class PictureUploader < CarrierWave::Uploader::Base
+ include CarrierWave::MiniMagick
+ process resize_to_limit: [400, 400]
+
+ # Include RMagick or MiniMagick support:
+ # include CarrierWave::RMagick
+ # include CarrierWave::MiniMagick
+
+ # Choose what kind of storage to use for this uploader:
+ #storage :file
+ # storage :fog
+
+ if Rails.env.production?
+ storage :fog
+ else
+ storage :file
+ end
+
+ # Override the directory where uploaded files will be stored.
+ # This is a sensible default for uploaders that are meant to be mounted:
+ def store_dir
+ "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ end
+
+# Add a white list of extensions which are allowed to be uploaded.
+ def extension_white_list
+ %w(jpg jpeg gif png)
+ end
+ # Provide a default URL as a default if there hasn't been a file uploaded:
+ # def default_url
+ # # For Rails 3.1+ asset pipeline compatibility:
+ # # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
+ #
+ # "/images/fallback/" + [version_name, "default.png"].compact.join('_')
+ # end
+
+ # Process files as they are uploaded:
+ # process :scale => [200, 300]
+ #
+ # def scale(width, height)
+ # # do something
+ # end
+
+ # Create different versions of your uploaded files:
+ # version :thumb do
+ # process :resize_to_fit => [50, 50]
+ # end
+
+ # Add a white list of extensions which are allowed to be uploaded.
+ # For images you might use something like this:
+ # def extension_white_list
+ # %w(jpg jpeg gif png)
+ # end
+
+ # Override the filename of the uploaded files:
+ # Avoid using model.id or version_name here, see uploader/store.rb for details.
+ # def filename
+ # "something.jpg" if original_filename
+ # end
+
+end
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000..991cf0f
--- /dev/null
+++ b/app/views/layouts/mailer.html.erb
@@ -0,0 +1,5 @@
+
+
+ <%= yield %>
+
+
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
new file mode 100644
index 0000000..37f0bdd
--- /dev/null
+++ b/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/app/views/microposts/_micropost.html.erb b/app/views/microposts/_micropost.html.erb
new file mode 100644
index 0000000..da6dfcf
--- /dev/null
+++ b/app/views/microposts/_micropost.html.erb
@@ -0,0 +1,13 @@
+
+ <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
+ <%= link_to micropost.user.name, micropost.user %>
+ <%= micropost.content %>
+ <%= image_tag micropost.picture.url if micropost.picture? %>
+
+ Posted <%= time_ago_in_words(micropost.created_at) %> ago.
+ <% if current_user?(micropost.user) %>
+ <%= link_to "delete", micropost, method: :delete,
+ data: { confirm: "You sure?" } %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/microposts/show.html.erb b/app/views/microposts/show.html.erb
index 0e55f60..237d4c5 100644
--- a/app/views/microposts/show.html.erb
+++ b/app/views/microposts/show.html.erb
@@ -1,14 +1,20 @@
-<%= notice %>
-
-
- Content:
- <%= @micropost.content %>
-
-
-
- User:
- <%= @micropost.user_id %>
-
-
-<%= link_to 'Edit', edit_micropost_path(@micropost) %> |
-<%= link_to 'Back', microposts_path %>
+<% provide(:title, @user.name) %>
+
+
+
+
+ <%= gravatar_for @user %>
+ <%= @user.name %>
+
+
+
+
+ <% if @user.microposts.any? %>
+
Microposts (<%= @user.microposts.count %>)
+
+ <%= render @microposts %>
+
+ <%= will_paginate @microposts %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/shared/_error_messages.html.erb b/app/views/shared/_error_messages.html.erb
index 2474cb6..1da9f30 100644
--- a/app/views/shared/_error_messages.html.erb
+++ b/app/views/shared/_error_messages.html.erb
@@ -1,12 +1,12 @@
-<% if @user.errors.any? %>
+<% if object.errors.any? %>
- The form contains <%= pluralize(@user.errors.count, "error") %>.
+ The form contains <%= pluralize(object.errors.count, "error") %>.
- <% @user.errors.full_messages.each do |msg| %>
- <%= msg %>
- <% end %>
+ <% object.errors.full_messages.each do |msg| %>
+ <%= msg %>
+ <% end %>
<% end %>
\ No newline at end of file
diff --git a/app/views/shared/_feed.html.erb b/app/views/shared/_feed.html.erb
new file mode 100644
index 0000000..bf24156
--- /dev/null
+++ b/app/views/shared/_feed.html.erb
@@ -0,0 +1,6 @@
+<% if @feed_items.any? %>
+
+ <%= render @feed_items %>
+
+ <%= will_paginate @feed_items %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/shared/_micropost_form.html.erb b/app/views/shared/_micropost_form.html.erb
new file mode 100644
index 0000000..8034ba3
--- /dev/null
+++ b/app/views/shared/_micropost_form.html.erb
@@ -0,0 +1,18 @@
+<%= form_for(@micropost) do |f| %>
+ <%= render 'shared/error_messages', object: f.object %>
+
+ <%= f.text_area :content, placeholder: "Compose new micropost..." %>
+
+ <%= f.submit "Post", class: "btn btn-primary" %>
+
+ <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
+
+<% end %>
+
\ No newline at end of file
diff --git a/app/views/shared/_user_info.html.erb b/app/views/shared/_user_info.html.erb
new file mode 100644
index 0000000..e6302d8
--- /dev/null
+++ b/app/views/shared/_user_info.html.erb
@@ -0,0 +1,4 @@
+<%= link_to gravatar_for(current_user, size: 50), current_user %>
+<%= current_user.name %>
+<%= link_to "view my profile", current_user %>
+<%= pluralize(current_user.microposts.count, "micropost") %>
\ No newline at end of file
diff --git a/app/views/static_pages/home.html.erb b/app/views/static_pages/home.html.erb
index d7cae1d..ca4724e 100644
--- a/app/views/static_pages/home.html.erb
+++ b/app/views/static_pages/home.html.erb
@@ -1,15 +1,29 @@
<% provide(:title, "Home") %>
-
-
Welcome to the Sample App
-
-
- This is the home page for the
- Ruby on Rails Tutorial
- sample application.
-
-
- <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
-
-
-<%= link_to image_tag("rails.png", alt: "Rails logo"),
- 'http://rubyonrails.org/' %>
\ No newline at end of file
+<% if logged_in? %>
+
+
+
+ <%= render 'shared/user_info' %>
+
+
+ <%= render 'shared/micropost_form' %>
+
+
+
+
Micropost Feed
+ <%= render 'shared/feed' %>
+
+
+<% else %>
+
+
Welcome to the Sample App
+
+ This is the home page for the
+ Ruby on Rails Tutorial
+ sample application.
+
+ <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
+
+ <%= link_to image_tag("rails.png", alt: "Rails logo"),
+ 'http://rubyonrails.org/' %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/user_mailer/account_activation.html.erb b/app/views/user_mailer/account_activation.html.erb
new file mode 100644
index 0000000..603ece3
--- /dev/null
+++ b/app/views/user_mailer/account_activation.html.erb
@@ -0,0 +1,7 @@
+Sample App
+Hi <%= @user.name %>,
+
+Welcome to the Sample App! Click on the link below to activate your account:
+
+<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
+email: @user.email) %>
\ No newline at end of file
diff --git a/app/views/user_mailer/account_activation.text.erb b/app/views/user_mailer/account_activation.text.erb
new file mode 100644
index 0000000..4f11c68
--- /dev/null
+++ b/app/views/user_mailer/account_activation.text.erb
@@ -0,0 +1,3 @@
+Hi <%= @user.name %>,
+Welcome to the Sample App! Click on the link below to activate your account:
+<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
\ No newline at end of file
diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb
new file mode 100644
index 0000000..83af3c2
--- /dev/null
+++ b/app/views/user_mailer/password_reset.html.erb
@@ -0,0 +1,5 @@
+UserMailer#password_reset
+
+
+ <%= @greeting %>, find me in app/views/user_mailer/password_reset.html.erb
+
diff --git a/app/views/user_mailer/password_reset.text.erb b/app/views/user_mailer/password_reset.text.erb
new file mode 100644
index 0000000..0f06bee
--- /dev/null
+++ b/app/views/user_mailer/password_reset.text.erb
@@ -0,0 +1,3 @@
+UserMailer#password_reset
+
+<%= @greeting %>, find me in app/views/user_mailer/password_reset.text.erb
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 69d2575..4b5c789 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -3,7 +3,7 @@
<%= form_for(@user) do |f| %>
- <%= render 'shared/error_messages' %>
+ <%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb
index 62f547d..f6dd1d3 100644
--- a/app/views/users/new.html.erb
+++ b/app/views/users/new.html.erb
@@ -1,24 +1,18 @@
<% provide(:title, 'Sign up') %>
Sign up
-
- <%= form_for(@user, url: signup_path) do |f| %>
- <%= render 'shared/error_messages' %>
-
+ <%= form_for(@user) do |f| %>
+ <%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
-
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
-
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
-
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
-
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
-
-
\ No newline at end of file
+
+
-
-
-
- <%= gravatar_for @user %>
- <%= @user.name %>
-
-
-
+
+
+
+ <%= gravatar_for @user %>
+ <%= @user.name %>
+
+
+
+
+ <% if @user.microposts.any? %>
+
Microposts (<%= @user.microposts.count %>)
+
+ <%= render @microposts %>
+
+ <%= will_paginate @microposts %>
+ <% end %>
+
\ No newline at end of file
diff --git a/config/environments/development.rb b/config/environments/development.rb
index b55e214..11054bc 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -22,6 +22,12 @@
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
+
+
+ config.action_mailer.raise_delivery_errors = true
+ config.action_mailer.delivery_method = :test
+ host = 'localhost:3000'
+ config.action_mailer.default_url_options = { host: host }
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1c19f08..58ff691 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -30,7 +30,7 @@
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
-
+ config.action_mailer.default_url_options = { host: 'localhost:3000' }
# Randomize the order test cases are executed.
config.active_support.test_order = :random
diff --git a/config/initializers/carrier_wave.rb b/config/initializers/carrier_wave.rb
new file mode 100644
index 0000000..c3aa2b8
--- /dev/null
+++ b/config/initializers/carrier_wave.rb
@@ -0,0 +1,11 @@
+if Rails.env.production?
+ CarrierWave.configure do |config|
+ config.fog_credentials = {
+ # Configuration for Amazon S3
+ :provider => 'AWS',
+ :aws_access_key_id => ENV['S3_ACCESS_KEY'],
+ :aws_secret_access_key => ENV['S3_SECRET_KEY']
+ }
+ config.fog_directory = ENV['S3_BUCKET']
+ end
+end
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 7acdf2a..22db99e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -10,8 +10,9 @@
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
- resources :microposts
resources :users
+ resources :account_activations, only: [:edit]
+ resources :microposts, only: [:create, :destroy]
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
diff --git a/db/migrate/20160711062804_add_activation_to_users.rb b/db/migrate/20160711062804_add_activation_to_users.rb
new file mode 100644
index 0000000..b7b0abf
--- /dev/null
+++ b/db/migrate/20160711062804_add_activation_to_users.rb
@@ -0,0 +1,7 @@
+class AddActivationToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :activation_digest, :string
+ add_column :users, :activated, :boolean, default: false
+ add_column :users, :activated_at, :datetime
+ end
+end
diff --git a/db/migrate/20160707012303_create_microposts.rb b/db/migrate/20160712070613_create_microposts.rb
similarity index 53%
rename from db/migrate/20160707012303_create_microposts.rb
rename to db/migrate/20160712070613_create_microposts.rb
index 67117ba..75bcba8 100644
--- a/db/migrate/20160707012303_create_microposts.rb
+++ b/db/migrate/20160712070613_create_microposts.rb
@@ -2,9 +2,11 @@ class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.text :content
- t.integer :user_id
+ t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
+ add_foreign_key :microposts, :users
+ add_index :microposts, [:user_id, :created_at]
end
end
diff --git a/db/migrate/20160712093058_add_picture_to_microposts.rb b/db/migrate/20160712093058_add_picture_to_microposts.rb
new file mode 100644
index 0000000..5433286
--- /dev/null
+++ b/db/migrate/20160712093058_add_picture_to_microposts.rb
@@ -0,0 +1,5 @@
+class AddPictureToMicroposts < ActiveRecord::Migration
+ def change
+ add_column :microposts, :picture, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0a39043..3c12bf6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,23 +11,30 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160708090312) do
+ActiveRecord::Schema.define(version: 20160712093058) do
create_table "microposts", force: :cascade do |t|
t.text "content"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "picture"
end
+ add_index "microposts", ["user_id", "created_at"], name: "index_microposts_on_user_id_and_created_at"
+ add_index "microposts", ["user_id"], name: "index_microposts_on_user_id"
+
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.string "password_digest"
t.string "remember_digest"
- t.boolean "admin", default: false
+ t.boolean "admin", default: false
+ t.string "activation_digest"
+ t.boolean "activated", default: false
+ t.datetime "activated_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
diff --git a/db/seeds.rb b/db/seeds.rb
index d89ca2a..ce697e5 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -9,7 +9,9 @@
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
- admin: true)
+ admin: true,
+ activated: true,
+ activated_at: Time.zone.now)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
@@ -17,5 +19,13 @@
User.create!(name: name,
email: email,
password: password,
- password_confirmation: password)
+ password_confirmation: password,
+ activated: true,
+ activated_at: Time.zone.now)
+end
+
+users = User.order(:created_at).take(6)
+50.times do
+ content = Faker::Lorem.sentence(5)
+ users.each { |user| user.microposts.create!(content: content) }
end
\ No newline at end of file
diff --git a/test/controllers/account_activation_controller_test.rb b/test/controllers/account_activation_controller_test.rb
new file mode 100644
index 0000000..1a66567
--- /dev/null
+++ b/test/controllers/account_activation_controller_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class AccountActivationControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/controllers/microposts_controller_test.rb b/test/controllers/microposts_controller_test.rb
index be8f376..a876fa6 100644
--- a/test/controllers/microposts_controller_test.rb
+++ b/test/controllers/microposts_controller_test.rb
@@ -1,49 +1,31 @@
require 'test_helper'
class MicropostsControllerTest < ActionController::TestCase
- setup do
- @micropost = microposts(:one)
- end
-
- test "should get index" do
- get :index
- assert_response :success
- assert_not_nil assigns(:microposts)
- end
-
- test "should get new" do
- get :new
- assert_response :success
- end
-
- test "should create micropost" do
- assert_difference('Micropost.count') do
- post :create, micropost: { content: @micropost.content, user_id: @micropost.user_id }
- end
-
- assert_redirected_to micropost_path(assigns(:micropost))
- end
-
- test "should show micropost" do
- get :show, id: @micropost
- assert_response :success
- end
-
- test "should get edit" do
- get :edit, id: @micropost
- assert_response :success
- end
-
- test "should update micropost" do
- patch :update, id: @micropost, micropost: { content: @micropost.content, user_id: @micropost.user_id }
- assert_redirected_to micropost_path(assigns(:micropost))
- end
-
- test "should destroy micropost" do
- assert_difference('Micropost.count', -1) do
- delete :destroy, id: @micropost
- end
-
- assert_redirected_to microposts_path
- end
+ # test "the truth" do
+ # assert true
+ # end
+ def setup
+ @micropost = microposts(:orange)
+ end
+ test "should redirect create when not logged in" do
+ assert_no_difference 'Micropost.count' do
+ post :create, micropost: { content: "Lorem ipsum" }
+ end
+ assert_redirected_to login_url
+ end
+ test "should redirect destroy when not logged in" do
+ assert_no_difference 'Micropost.count' do
+ delete :destroy, id: @micropost
+ end
+ assert_redirected_to login_url
+ end
+
+ test "should redirect destroy for wrong micropost" do
+ log_in_as(users(:michael))
+ micropost = microposts(:ants)
+ assert_no_difference 'Micropost.count' do
+ delete :destroy, id: micropost
+ end
+ assert_redirected_to root_url
+ end
end
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 276b26f..4fa6784 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -9,10 +9,6 @@ def setup
get :index
assert_redirected_to login_url
end
- test "should get new" do
- get :new
- assert_response :success
- end
test "should redirect edit when not logged in" do
get :edit, id: @user
diff --git a/test/fixtures/microposts.yml b/test/fixtures/microposts.yml
index a5153b6..2a1fcaa 100644
--- a/test/fixtures/microposts.yml
+++ b/test/fixtures/microposts.yml
@@ -1,9 +1,46 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-one:
- content: MyText
- user_id:
+orange:
+ content: "I just ate an orange!"
+ created_at: <%= 10.minutes.ago %>
+ user: michael
+
+tau_manifesto:
+ content: "Check out the @tauday site by @mhartl: http://tauday.com"
+ created_at: <%= 3.years.ago %>
+ user: michael
+
+cat_video:
+ content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
+ created_at: <%= 2.hours.ago %>
+ user: michael
+
+most_recent:
+ content: "Writing a short test"
+ created_at: <%= Time.zone.now %>
+ user: michael
+
+<% 30.times do |n| %>
+micropost_<%= n %>:
+ content: <%= Faker::Lorem.sentence(5) %>
+ created_at: <%= 42.days.ago %>
+ user: michael
+<% end %>
+
+ants:
+ content: "Oh, is that what you want? Because that's how you get ants!"
+ created_at: <%= 2.years.ago %>
+ user: archer
+zone:
+ content: "Danger zone!"
+ created_at: <%= 3.days.ago %>
+ user: archer
+tone:
+ content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
+ created_at: <%= 10.minutes.ago %>
+ user: lana
+van:
+ content: "Dude, this van's, like, rolling probable cause."
+ created_at: <%= 4.hours.ago %>
+ user: lana
-two:
- content: MyText
- user_id:
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index 3edd50c..c515071 100644
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -3,21 +3,31 @@ michael:
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
+ activated: true
+ activated_at: <%= Time.zone.now %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
+ activated: true
+ activated_at: <%= Time.zone.now %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
+ activated: true
+ activated_at: <%= Time.zone.now %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
+ activated: true
+ activated_at: <%= Time.zone.now %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
+ activated: true
+ activated_at: <%= Time.zone.now %>
<% end %>
\ No newline at end of file
diff --git a/test/integration/microposts_interface_test.rb b/test/integration/microposts_interface_test.rb
new file mode 100644
index 0000000..39bacb3
--- /dev/null
+++ b/test/integration/microposts_interface_test.rb
@@ -0,0 +1,37 @@
+require 'test_helper'
+
+class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
+ # test "the truth" do
+ # assert true
+ # end
+ def setup
+ @user = users(:michael)
+ end
+ test "micropost interface" do
+ log_in_as(@user)
+ get root_path
+ assert_select 'div.pagination'
+ # Invalid submission
+ assert_no_difference 'Micropost.count' do
+ post microposts_path, micropost: { content: "" }
+ end
+ assert_select 'div#error_explanation'
+ # Valid submission
+ content = "This micropost really ties the room together"
+ assert_difference 'Micropost.count', 1 do
+ post microposts_path, micropost: { content: content }
+ end
+ assert_redirected_to root_url
+ follow_redirect!
+ assert_match content, response.body
+ # Delete a post.
+ assert_select 'a', text: 'delete'
+ first_micropost = @user.microposts.paginate(page: 1).first
+ assert_difference 'Micropost.count', -1 do
+ delete micropost_path(first_micropost)
+ end
+ # Visit a different user.
+ get user_path(users(:archer))
+ assert_select 'a', text: 'delete', count: 0
+ end
+end
diff --git a/test/integration/users_profile_test.rb b/test/integration/users_profile_test.rb
new file mode 100644
index 0000000..c7af388
--- /dev/null
+++ b/test/integration/users_profile_test.rb
@@ -0,0 +1,19 @@
+require 'test_helper'
+class UsersProfileTest < ActionDispatch::IntegrationTest
+ include ApplicationHelper
+ def setup
+ @user = users(:michael)
+ end
+ test "profile display" do
+ get user_path(@user)
+ assert_template 'users/show'
+ assert_select 'title', full_title(@user.name)
+ assert_select 'h1', text: @user.name
+ assert_select 'h1>img.gravatar'
+ assert_match @user.microposts.count.to_s, response.body
+ assert_select 'div.pagination'
+ @user.microposts.paginate(page: 1).each do |micropost|
+ assert_match micropost.content, response.body
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/integration/users_signup_test.rb b/test/integration/users_signup_test.rb
index b17a886..8fb1141 100644
--- a/test/integration/users_signup_test.rb
+++ b/test/integration/users_signup_test.rb
@@ -1,19 +1,45 @@
require 'test_helper'
-
class UsersSignupTest < ActionDispatch::IntegrationTest
-
- # test "the truth" do
- # assert true
- # end
- test "valid signup information" do
- get signup_path
- assert_difference 'User.count', 1 do
- post_via_redirect users_path, user: { name: "Example User",
- email: "user@example.com",
- password: "password",
- password_confirmation: "password" }
+ def setup
+ ActionMailer::Base.deliveries.clear
end
- assert_template 'users/show'
- assert is_logged_in?
- end
-end
+ test "invalid signup information" do
+ get signup_path
+ assert_no_difference 'User.count' do
+ post users_path, user: { name: "",
+ email: "user@invalid",
+ password: "foo",
+ password_confirmation: "bar" }
+ end
+ assert_template 'users/new'
+ assert_select 'div#error_explanation'
+ assert_select 'div.field_with_errors'
+ end
+ test "valid signup information with account activation" do
+ get signup_path
+ assert_difference 'User.count', 1 do
+ post users_path, user: { name: "Example User",
+ email: "user@example.com",
+ password: "password",
+ password_confirmation: "password" }
+ end
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ user = assigns(:user)
+ assert_not user.activated?
+ # Try to log in before activation.
+ log_in_as(user)
+ assert_not is_logged_in?
+ # Invalid activation token
+ #get edit_account_activation_path("invalid token")
+ #assert_not is_logged_in?
+ # Valid token, wrong email
+ get edit_account_activation_path(user.activation_token, email: 'wrong')
+ assert_not is_logged_in?
+ # Valid activation token
+ get edit_account_activation_path(user.activation_token, email: user.email)
+ assert user.reload.activated?
+ follow_redirect!
+ assert_template 'users/show'
+ assert is_logged_in?
+ end
+end
\ No newline at end of file
diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb
new file mode 100644
index 0000000..8d7e4f7
--- /dev/null
+++ b/test/mailers/previews/user_mailer_preview.rb
@@ -0,0 +1,15 @@
+# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
+class UserMailerPreview < ActionMailer::Preview
+# Preview this email at
+# http://localhost:3000/rails/mailers/user_mailer/account_activation
+ def account_activation
+ user = User.first
+ user.activation_token = User.new_token
+ UserMailer.account_activation(user)
+ end
+# Preview this email at
+# http://localhost:3000/rails/mailers/user_mailer/password_reset
+ def password_reset
+ UserMailer.password_reset
+ end
+end
\ No newline at end of file
diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb
new file mode 100644
index 0000000..9fbf031
--- /dev/null
+++ b/test/mailers/user_mailer_test.rb
@@ -0,0 +1,14 @@
+require 'test_helper'
+class UserMailerTest < ActionMailer::TestCase
+ test "account_activation" do
+ user = users(:michael)
+ user.activation_token = User.new_token
+ mail = UserMailer.account_activation(user)
+ assert_equal "Account activation", mail.subject
+ assert_equal [user.email], mail.to
+ assert_equal ["noreply@example.com"], mail.from
+ assert_match user.name, mail.body.encoded
+ assert_match user.activation_token, mail.body.encoded
+ assert_match CGI::escape(user.email), mail.body.encoded
+ end
+end
\ No newline at end of file
diff --git a/test/models/micropost_test.rb b/test/models/micropost_test.rb
index def8e93..dc7e172 100644
--- a/test/models/micropost_test.rb
+++ b/test/models/micropost_test.rb
@@ -4,4 +4,28 @@ class MicropostTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
+ def setup
+ @user = users(:michael)
+ # This code is not idiomatically correct.
+ @micropost = @user.microposts.build(content: "Lorem ipsum")
+ end
+ test "should be valid" do
+ assert @micropost.valid?
+ end
+ test "user id should be present" do
+ @micropost.user_id = nil
+ assert_not @micropost.valid?
+ end
+
+ test "content should be present " do
+ @micropost.content = " "
+ assert_not @micropost.valid?
+ end
+ test "content should be at most 140 characters" do
+ @micropost.content = "a" * 141
+ assert_not @micropost.valid?
+ end
+ test "order should be most recent first" do
+ assert_equal Micropost.first, microposts(:most_recent)
+ end
end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index ea2c37a..aba79dc 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -67,6 +67,14 @@ def setup
end
test "authenticated? should return false for a user with nil digest" do
- assert_not @user.authenticated?('')
+ assert_not @user.authenticated?(:remember, '')
+ end
+
+ test "associated microposts should be destroyed" do
+ @user.save
+ @user.microposts.create!(content: "Lorem ipsum")
+ assert_difference 'Micropost.count', -1 do
+ @user.destroy
+ end
end
end
From 4b5c40a0e3bbd00c959ae09e94c9ac9289f1266e Mon Sep 17 00:00:00 2001
From: hungz23
Date: Wed, 13 Jul 2016 13:00:45 +0700
Subject: [PATCH 3/5] add password reset
---
app/assets/javascripts/password_resets.coffee | 3 +
app/assets/stylesheets/password_resets.scss | 3 +
app/controllers/password_resets_controller.rb | 59 ++++++++++++++++++
app/helpers/password_resets_helper.rb | 2 +
app/mailers/user_mailer.rb | 7 +--
app/models/user.rb | 16 ++++-
app/views/password_resets/edit.html.erb | 15 +++++
app/views/password_resets/new.html.erb | 11 ++++
app/views/sessions/new.html.erb | 1 +
app/views/user_mailer/password_reset.html.erb | 12 ++--
app/views/user_mailer/password_reset.text.erb | 8 ++-
config/routes.rb | 7 ++-
.../20160712155715_add_reset_to_users.rb | 6 ++
db/schema.rb | 4 +-
.../uploads/micropost/picture/301/HBLAB.png | Bin 0 -> 87267 bytes
test/integration/password_reset_test.rb | 59 ++++++++++++++++++
test/integration/users_signup_test.rb | 4 +-
test/mailers/previews/user_mailer_preview.rb | 4 +-
test/mailers/user_mailer_test.rb | 10 +++
19 files changed, 214 insertions(+), 17 deletions(-)
create mode 100644 app/assets/javascripts/password_resets.coffee
create mode 100644 app/assets/stylesheets/password_resets.scss
create mode 100644 app/controllers/password_resets_controller.rb
create mode 100644 app/helpers/password_resets_helper.rb
create mode 100644 app/views/password_resets/edit.html.erb
create mode 100644 app/views/password_resets/new.html.erb
create mode 100644 db/migrate/20160712155715_add_reset_to_users.rb
create mode 100644 public/uploads/micropost/picture/301/HBLAB.png
create mode 100644 test/integration/password_reset_test.rb
diff --git a/app/assets/javascripts/password_resets.coffee b/app/assets/javascripts/password_resets.coffee
new file mode 100644
index 0000000..24f83d1
--- /dev/null
+++ b/app/assets/javascripts/password_resets.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/password_resets.scss b/app/assets/stylesheets/password_resets.scss
new file mode 100644
index 0000000..eb9649c
--- /dev/null
+++ b/app/assets/stylesheets/password_resets.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the PasswordResets controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb
new file mode 100644
index 0000000..d735d4d
--- /dev/null
+++ b/app/controllers/password_resets_controller.rb
@@ -0,0 +1,59 @@
+class PasswordResetsController < ApplicationController
+ before_action :get_user, only: [:edit, :update]
+ before_action :valid_user, only: [:edit, :update]
+ before_action :check_expiration, only: [:edit, :update]
+ def new
+ end
+ def create
+ @user = User.find_by(email: params[:password_reset][:email].downcase)
+ if @user
+ @user.create_reset_digest
+ @user.send_password_reset_email
+ flash[:info] = "Email sent with password reset instructions"
+ redirect_to root_url
+ else
+ flash.now[:danger] = "Email address not found"
+ render 'new'
+ end
+ end
+ def edit
+ end
+ def update
+ if password_blank?
+ flash.now[:danger] = "Password can't be blank"
+ render 'edit'
+ elsif @user.update_attributes(user_params)
+ log_in @user
+ flash[:success] = "Password has been reset."
+ redirect_to @user
+ else
+ render 'edit'
+ end
+ end
+ private
+ def user_params
+ params.require(:user).permit(:password, :password_confirmation)
+ end
+ # Returns true if password is blank.
+ def password_blank?
+ params[:user][:password].blank?
+ end
+ # Before filters
+ def get_user
+ @user = User.find_by(email: params[:email])
+ end
+ # Confirms a valid user.
+ def valid_user
+ unless (@user && @user.activated? &&
+ @user.authenticated?(:reset, params[:id]))
+ redirect_to root_url
+ end
+ end
+ # Checks expiration of reset token.
+ def check_expiration
+ if @user.password_reset_expired?
+ flash[:danger] = "Password reset has expired."
+ redirect_to new_password_reset_url
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/helpers/password_resets_helper.rb b/app/helpers/password_resets_helper.rb
new file mode 100644
index 0000000..0c9d96e
--- /dev/null
+++ b/app/helpers/password_resets_helper.rb
@@ -0,0 +1,2 @@
+module PasswordResetsHelper
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 84bc9f8..ae8c9cd 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -14,9 +14,8 @@ def account_activation(user)
#
# en.user_mailer.password_reset.subject
#
- def password_reset
- @greeting = "Hi"
-
- mail to: "to@example.org"
+ def password_reset(user)
+ @user = user
+ mail to: user.email, subject: "Password reset"
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index c3c23b7..bc4119e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,6 @@
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
- attr_accessor :remember_token, :activation_token
+ attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
@@ -47,11 +47,25 @@ def send_activation_email
UserMailer.account_activation(self).deliver_now
end
+ # Sets the password reset attributes.
+ def create_reset_digest
+ self.reset_token = User.new_token
+ update_attribute(:reset_digest, User.digest(reset_token))
+ update_attribute(:reset_sent_at, Time.zone.now)
+ end
+ # Sends password reset email.
+ def send_password_reset_email
+ UserMailer.password_reset(self).deliver_now
+ end
# Defines a proto-feed.
# See "Following users" for the full implementation.
def feed
Micropost.where("user_id = ?", id)
end
+ # Returns true if a password reset has expired.
+ def password_reset_expired?
+ reset_sent_at < 2.hours.ago
+ end
private
# Converts email to all lower-case.
def downcase_email
diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb
new file mode 100644
index 0000000..6eeb28a
--- /dev/null
+++ b/app/views/password_resets/edit.html.erb
@@ -0,0 +1,15 @@
+<% provide(:title, 'Reset password') %>
+Reset password
+
+
+ <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
+ <%= render 'shared/error_messages' %>
+ <%= hidden_field_tag :email, @user.email %>
+ <%= f.label :password %>
+ <%= f.password_field :password, class: 'form-control' %>
+ <%= f.label :password_confirmation, "Confirmation" %>
+ <%= f.password_field :password_confirmation, class: 'form-control' %>
+ <%= f.submit "Update password", class: "btn btn-primary" %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb
new file mode 100644
index 0000000..b8aa69d
--- /dev/null
+++ b/app/views/password_resets/new.html.erb
@@ -0,0 +1,11 @@
+<% provide(:title, "Forgot password") %>
+Forgot password
+
+
+ <%= form_for(:password_reset, url: password_resets_path) do |f| %>
+ <%= f.label :email %>
+ <%= f.email_field :email, class: 'form-control' %>
+ <%= f.submit "Submit", class: "btn btn-primary" %>
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb
index 3df2579..ec600d8 100644
--- a/app/views/sessions/new.html.erb
+++ b/app/views/sessions/new.html.erb
@@ -9,6 +9,7 @@
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
+ <%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb
index 83af3c2..f86d184 100644
--- a/app/views/user_mailer/password_reset.html.erb
+++ b/app/views/user_mailer/password_reset.html.erb
@@ -1,5 +1,9 @@
-UserMailer#password_reset
-
+Password reset
+To reset your password click the link below:
+<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
+email: @user.email) %>
+This link will expire in two hours.
- <%= @greeting %>, find me in app/views/user_mailer/password_reset.html.erb
-
+If you did not request your password to be reset, please ignore this email and
+your password will stay as it is.
+
\ No newline at end of file
diff --git a/app/views/user_mailer/password_reset.text.erb b/app/views/user_mailer/password_reset.text.erb
index 0f06bee..9c35b0d 100644
--- a/app/views/user_mailer/password_reset.text.erb
+++ b/app/views/user_mailer/password_reset.text.erb
@@ -1,3 +1,5 @@
-UserMailer#password_reset
-
-<%= @greeting %>, find me in app/views/user_mailer/password_reset.text.erb
+To reset your password click the link below:
+<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
+This link will expire in two hours.
+If you did not request your password to be reset, please ignore this email and
+your password will stay as it is.
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 22db99e..be59ded 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,10 +1,14 @@
Rails.application.routes.draw do
+ get 'password_resets/new'
+
+ get 'password_resets/edit'
+
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
- post '/signup' => 'users#create'
+ #post '/signup' => 'users#create'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
@@ -13,6 +17,7 @@
resources :users
resources :account_activations, only: [:edit]
resources :microposts, only: [:create, :destroy]
+ resources :password_resets, only: [:new, :create, :edit, :update]
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
diff --git a/db/migrate/20160712155715_add_reset_to_users.rb b/db/migrate/20160712155715_add_reset_to_users.rb
new file mode 100644
index 0000000..f650b49
--- /dev/null
+++ b/db/migrate/20160712155715_add_reset_to_users.rb
@@ -0,0 +1,6 @@
+class AddResetToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :reset_digest, :string
+ add_column :users, :reset_sent_at, :date_time
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3c12bf6..848dc03 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160712093058) do
+ActiveRecord::Schema.define(version: 20160712155715) do
create_table "microposts", force: :cascade do |t|
t.text "content"
@@ -35,6 +35,8 @@
t.string "activation_digest"
t.boolean "activated", default: false
t.datetime "activated_at"
+ t.string "reset_digest"
+ t.time "reset_sent_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
diff --git a/public/uploads/micropost/picture/301/HBLAB.png b/public/uploads/micropost/picture/301/HBLAB.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0c6ebb1589e509da208f8a3bdbb8101212954f5
GIT binary patch
literal 87267
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