Awesome Q&A Rails App
From start to finish:
$ rails new goodquestion
$ rails g controller Questions index
-
on routes.rb:
replace
get "questions/index"
with
root :to => 'questions#index'
-
remove
public/index.html
-
test
$ rails s
-
on layouts/application.html.erb
replace <title>GoodQuestion</title> with <title><%= yield(:title) %></title>
-
on questions/index.html.erb
add <%= provide(:title, ‘GoodQuestion’) %>
-
on application.html.erb
<div id=“container”> <div id=“header”> <%= link_to ‘GoodQuestion’, root_path %> </div><!– end header –> <div id=“nav”> <ul> <li> <%= link_to ‘Home’, root_path %> </li> </ul> </div><!– end nav –>
<div id=“content”> <%= flash.each do |key, value| %> <p id=“message”><%= value %></p> <% end %> <%= yield %> </div><!– end content –>
<div id=“footer”> © GoodQuestion <%= Date.today.year %> </div><!– end footer –> </div><!– end container –>
-
creating User model:
$ rails g model User username:string password_digest:string
-
On User Migration, add indexes to prevent duplicates:
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :username t.string :password_digest
t.timestamps end
add_index :users, :username, unique: true end end
-
run
$ rake db:migrate
-
on user.rb, add has_secure_password
class User < ActiveRecord::Base
attr_accessible :password_digest, :username has_secure_password
end
-
has_secure_password requires bcrypt gem on gemfile
gem ‘bcrypt-ruby’
bundle install
-
edit user.rb to:
class User < ActiveRecord::Base attr_accessible :password, :password_confirmation, :username
has_secure_password
validates :username, presence: true, uniqueness:{ case_sensitive: false }, length: { in: 4..12 }, format: { with: /^[a-z]*$/, message: ‘can only contain lower-case letters and numbers’} validates :password, length: { in: 4..8 } validates :password_confirmation, length: { in: 4..8 } end
-
generate User controler
$ rails g controller Users new
-
on routes.rb replace get “users/new” with resources :users, only: [:new, :create]
-
Edit new action on users controller def new @user = User.new end
-
Edit users/new to:
<% provide(:title, ‘GoodQuestion - Register’) %>
<h1>Register</h1>
<%= form_for(@user) do |f| %> <%= render ‘common/form_errors’, object: @user %> <% end %>
-
create views/common folder
-
create common/_form.html.erb file
-
edit _form.html.erb
<% if object.errors.any? %> <ul id=“form-errors”> <% object.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %>
-
edit users/new.html.erb
<% provide(:title, ‘GoodQuestion - Register’) %>
<h1>Register</h1>
<%= form_for(@user) do |f| %> <%= render ‘common/form_errors’, object: @user %>
<p> <%= f.label :username %><br /> <%= f.text_field :username %> </p>
<p> <%= f.label :password, “Password” %> <%= f.password_field :password %> </p>
<p> <%= f.label :password_confirmation, “Confirm” %><br /> <%= f.password_field :password_confirmation %> </p>
<p> <%= f.submit “Register” %> </p>
<% end %>
-
edit layouts/application.html.erb
add to nav div: <li> <%= link_to ‘Register’, new_user_path %> </li>
-
add create action to users controller def create @user = User.new(params) if @user.save flash = “Thanks for registering!” redirect_to root_url else render ‘new’ end end
-
edit en.yml
en:
hello: "Hello world" activerecord: attributes: user: password_digest: 'Password'
-
Sessions controller
$ rails g controller Sessions new
-
Edit routes.rb
replace
get "sessions/new"
with resources :sessions, only: [:new, :create]
-
Edit sessions/new.html.erb
<% provide(:title, ‘GoodQuestion - Login’)%>
<h1>Login</h1>
<%= form_for(:session, url:sessions_path) do |f| %> <p> <%= f.label :username, “Username” %> <%= f.text_field :username %> </p>
<p> <%= f.label :password, “Password” %> <%= f.password_field :password %> </p>
<p> <%= f.submit “Login” %> </p> <% end %>
-
Edit nav div on layout/application.html.erb
<% if logged_in? %> <li> <%= link_to “Logout (#{current_user.username})”,‘#’ %> </li> <% else %> <li> <%= link_to ‘Register’, new_user_path %> </li> <li> <%= link_to ‘Login’, new_session_path %> </li> <% end %>
-
Edit layout/application.html.erb to
class ApplicationController < ActionController::Base
protect_from_forgery helper_method [:current_user, :logged_in?] # this enables current_user and logged_in? methods across the the app protected def current_user current_user ||= User.find(session[:user_id]) if session[:user_id] end def logged_in? !current_user.nil? end
end
-
Add create action to sessions_controller def create user = User.find_by_username(params[:username]) if user && user.authenticate(params[:password]) login user flash = ‘You are now logged in!’ redirect_to root_url else flash.now = ‘Your username/password combination was incorrect. ’ render ‘new’ end end
-
Add login action to sessions_controller def login(user) session = user.id end
-
Edit routes.rb
add:
match '/register', to: 'users#new' match '/login', to: 'sessions#new' match '/logout', to: 'sessions#destroy', via: :delete
-
Edit nav paths on application.html.erb
<% if logged_in? %> <li> <%= link_to “Logout (#{current_user.username})”, logout_path, method: ‘delete’ %> </li> <% else %> <li> <%= link_to ‘Register’, register_path %> </li> <li> <%= link_to ‘Login’, login_path %> </li> <% end %>
-
Adding destroy action to sessions controller: def destroy session = nil redirect_to root_url, notice: ‘You are now logged out!’ end
-
add login @user to create action on users controller (login user automatically after registering) def create @user = User.new(params) if @user.save login @user flash = “Thanks for registering!” redirect_to root_url else render ‘new’ end
-
Creating Question model
$ rails g model Question user:references body:string solved:boolean
end
-
Adding defaut to Question migrations def change
create_table :questions do |t| t.references :user t.string :body t.boolean :solved, default: false t.timestamps end add_index :questions, :user_id
end
-
run
$ rake db:migrate
-
edit user.rb
add: has_many :questions
-
add validations to questions.rb validates :body, presence: true, length: { in: 10..255 } validates :solved, inclusion: { in: [true, false] }
-
add question paths to routes.rb
resources :questions, except: [:new]
-
edit index action on Questions Controller def index @question = Question.new end
-
edit questions/index.html.erb
<%= provide(:title, ‘GoodQuestion’) %> <div id=“ask”> <h1>Ask a Question</h1> <% if logged_in? %> <%= render ‘question_form’ %> <% else %> <p>Please login to ask or answer questions.</p> <% end %> </div><!– end task –>
-
edit questions/_question_form.html
<%= form_for(@question) do |f| %> <%= render ‘common/form_errors’, object: @question %>
<p> <%= f.label :body, “Question” %> <%= f.text_field :body %> <%= f.submit “Ask a Question” %> </p> <% end %>
-
add create action to questions controller
def create @question = current_user.questions.build(params) if @question.save flash = “Your question has been posted!” redirect_to root_url else render ‘index’ end end
-
add auth method to application controller
def auth redirect_to login_url, alert: 'You must login to access that page' unless logged_in? end
-
add before_filter to questions controller before_filter :auth, only: [:create]
-
add unsolved method to question model def self.unsolved(params) where(solved: false).paginate(page: params, order: ‘created_at DESC’, per_page: 3) end
-
edit questions controller to include unsolved method def index @question = Question.new
@questions = Question.unsolved(params)
end
def create @question = current_user.questions.build(params) if @question.save flash = “Your question has been posted!” redirect_to root_url else
@questions = Question.unsolved(params)
render ‘index’ end end
-
add div questions to questions/index.html.erb
<div id=“questions”> <h2>Unsolved Questions</h2> <% if @questions.any? %> <ul> <%= render @questions %> <!– no need for a for loop yay! it only needs question.html.erb –> </ul> <%= will_paginate %> <% else %> <p>No questions have been asked.</p> <% end %> </div><!– end questions –>
-
create questions/_question.html.erb
<li>
<%= truncate(question.body, length: 35) %> by <%= question.user.username.capitalize %> </li>
-
add show action to questions controller def show
@question = Question.find(params[:id])
end
-
create questions/show.html.erb
<%= provide(:title, ‘View Question’) %>
<h1> <%= @question.user.username.capitalize %> asks: </h1>
<p> <%= @question.body %> </p>
-
edit questions/_question.html.erb to:
<li> <%= link_to truncate(question.body, length: 35), question %> by <%= question.user.username.capitalize %> </li>
-
edit redirect_to on questions_controller’s create action
def create @question = current_user.questions.build(params) if @question.save flash = “Your question has been posted!” redirect_to @question else
@questions = Question.unsolved(params)
render ‘index’ end end
-
add your_questions route to routes.rb match ‘/your_questions’, to: ‘questions#your_questions’
-
add your_questions method to user.rb def your_questions(params) questions.paginate(page: params, order: ‘created_at DESC’, per_page: 3) end
-
edit before_filter on Questions controller before_filter :auth, only: [:create, :your_questions]
-
create and edit questions/your_questions.html.erb
<%= provide(:title, ‘Your Questions’) %>
<h1>Your Questions</h1>
<% if @questions.any? %> <ul> <%= render @questions %> </ul> <%= will_paginate %> <% else %> <p>You’ve not posted any questions yet.</p> <% end %>
-
add on_your_questions_page? to questions_helper
def on_your_questions_page? action_name == “your_questions” end
-
add on_your_questions_page? conditional to _question.html.erb
<% if on_your_questions_page? %> - <%= link_to ‘Edit’, edit_question_path(question) %> <% end %>
-
add edit action to questions_controller def edit
@question = current_user.questions.find(params[:id])
end
-
add edit to before_filter before_filter :auth, only: [:create, :your_questions, :edit]
-
create/edit questions/edit.html.erb
<% provide(:title, ‘Edit Your Question’) %>
<h1>Edit Your Question</h1>
<%= form_for(@question) do |f| %> <%= render ‘common/form_errors’, object: @question %>
<p> <%= f.label :body, “Body” %> <%= f.text_field :body %> </p>
<p> <%= f.label :solved, “Solved” %> <%= f.check_box :solved %> </p>
<p> <%= f.submit “Update” %> </p>
<% end %>
-
add update action to questions_controller
def update
@question = current_user.questions.find(params[:id]) if @question.update_attributes(params[:question]) flash[:success] = "Your question has been updated!" redirect_to @question else render 'edit' end
end
-
add update to before_filter
before_filter :auth, only: [:create, :your_questions, :edit, :update]
-
add display_solved to questions_helper
def display_solved(question) (question.solved) ? ‘ - (Solved) ’ : ” end
-
create Answer model
$ rails g model Answer user:references question:references body:string
-
Run migration
$ rake db:migrate
-
add association to User model has_many :answers
-
add association to Question model has_many :answers
-
add validations to Answer model
validates :body, presence: true, length: { in: 2..255 }
-
create answers controller
$ rails g controller Answers
-
add answers routes resources :questions, except: [:new] do
resources :answers, only: [:create]
end
-
edit show action on questions_controller def show
@question = Question.find(params[:id]) @answer = Answer.new
end
-
edit show.html.erb to:
<%= provide(:title, ‘View Question’) %>
<h1> <%= @question.user.username.capitalize %> asks: </h1>
<p> <%= @question.body %> </p>
<div id=“post-answer”> <h2>Answer this Question</h2>
<% if logged_in? %> <%= render ‘answers/answer_form’ %> <% else %> <p>Please login to post an answer for this question.</p> <% end %> </div><!– end post-answer –>
-
create/edit _answer_form.html.erb
<%= form_for([@question, @answer]) do |f| %> <%= render ‘common/form_errors’, object: @answer %>
<p> <%= f.label :body, “Answer” %> <%= f.text_field :body %> <%= f.submit “Post Answer” %> </p> <% end %>
-
add create action to answers controller
def create @question = Question.find(params) @answer = @question.answers.build(params) @answer.user = current_user
if @answer.save flash = “Your answer has been posted!” redirect_to @question else render ‘questions/show’ end end
-
add before_filter to create action on answers controller
before_filter :auth, only: [:create]
-
add answers div to show.html.erb
<div id=“answers”> <h2>Answers</h2>
<% if @question.answers.any? %> <ul> <%= render @question.answers %> </ul> <% else %> <p>This question has not been answered yet.</p> <% end %> </div><!– end answers –>
-
created _answer.html.erb
<li> <%= answer.body %> - by <%= answer.user.username.capitalize %> </li>
-
fix bug on create action of Answers Controller
def create @question = Question.find(params) @answer = @question.answers.build(params) @answer.user = current_user
if @answer.save flash = “Your answer has been posted!” redirect_to @question else @question = Question.find(params) render ‘questions/show’ end end
-
edit _question.html.erb
- (<%= pluralize(question.answers.count, ‘answer’) %>)
-
add search route to routes.rb
match ‘/search’, to: ‘questions#search’
-
add search div to layout/application.html.erb
<body> <div id=“container”> <div id=“header”> <%= link_to ‘GoodQuestion’, root_path %> </div><!– end header –> <div id=“searchbar”> <%= render ‘common/search_form’ %> </div><!– end searchbar –>
-
Add search method to Question model
def self.search(params) where(“body LIKE ?”, “%#{params}%”).paginate(page: params, order: ‘created_at DESC’, per_page: 3) end
-
Add search action to questions controller
def search
@questions = Question.search(params)
end
-
Create/edit questions/search.html.erb
<% provide(:title, ‘Search Results’) %> <h1>Search Results</h1>
<% if @questions.any? %> <ul> <%= render @questions %> </ul> <%= will_paginate %> <% else %> <p>Nothing found, please try a different search.</p> <% end %>
-
Create and edit assets/javscript/goodquestion.js.coffee
GoodQuestion = {}
GoodQuestion.autoSelectSearch = -> window.onload = -> keyword = document.getElementById ‘keyword’ keyword.onclick = -> @select()
GoodQuestion.autoSelectSearch()
-
add twitter bootstrap gem
gem ‘bootstrap-sass’
-
bundle
-
edit layout/application.html.erb
<!DOCTYPE html> <html> <head>
<title><%= yield(:title) %></title> <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %>
</head> <body> <header class = “navbar navbar-fixed-top” > <div class=“navbar-inner”> <div class=“container”> <%= link_to ‘GoodQuestion’, root_path, id: ‘logo’ %> <nav> <ul class=“nav pull-right”> <li> <%= render ‘common/search_form’ %> </li> <li> <%= link_to ‘Home’, root_path %> </li> <% if logged_in? %> <li> <%= link_to “Your Q’s”, your_questions_path %> </li> <li> <%= link_to “Logout”, logout_path, method: ‘delete’ %> </li> <% else %> <li> <%= link_to ‘Register’, register_path %> </li> <li> <%= link_to ‘Login’, login_path %> </li> <% end %> </ul> </nav><!– end nav –> </div> </div> </header>
<div class=“container”> <div class=“main-content”> <div> <% flash.each do |key, value| %> <%= content_tag(:div, value, class: “alert alert-#{key}”) %> <% end %> </div> <%= yield %> </div> <footer> © GoodQuestion <%= Date.today.year %> </footer><!– end footer –> </div><!– end content –> </body> </html>
-
edit application.css.scss
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the top of the * compiled file, but it's generally better to create a new file per style scope. * *= require_self *= require_tree . */
@import “bootstrap”;
/* mixins, variables, etc. */
$grayMediumLight: #eaeaea;
/* universal */
html {
overflow-y: scroll;
}
body {
padding-top: 60px; text-align: center;
}
nav { margin-top: 10px; }
section {
overflow: auto;
}
textarea {
resize: vertical;
}
ul {
list-style: none;
}
.center {
text-align: center; h1 { margin-bottom: 10px; }
}
.main-content{ margin-top: 50px; }
/* typography */
h1, h2, h3, h4, h5, h6 {
line-height: 1;
}
h1 {
font-size: 3em; letter-spacing: -2px; margin-bottom: 30px; text-align: center;
}
h2 {
font-size: 1.7em; letter-spacing: -1px; margin-bottom: 30px; text-align: center; font-weight: normal; color: $grayLight;
}
p {
font-size: 1.1em; line-height: 1.7em;
}
/* header */
#logo {
float: left; margin-right: 10px; font-size: 1.7em; color: #555; letter-spacing: -1px; padding-top: 9px; font-weight: bold; line-height: 1; &:hover { color: #aaa; text-decoration: none; }
}
/* footer */
footer {
margin-top: 45px; padding-top: 5px; border-top: 1px solid $grayMediumLight; color: $grayLight; a { color: $gray; &:hover { color: $grayDarker; } } small { float: left; } ul { float: right; list-style: none; li { float: left; margin-left: 10px; } }
}
-
edit common/_form_errors.html.erb
<% if object.errors.any? %> <ul id=“form-errors”> <% object.errors.full_messages.each do |message| %> <div class=“alert alert-error”> <li><%= message %></li> </div> <% end %> </ul> <% end %>
-
Deploy to heroku
$ RAILS_ENV=production bundle exec rake assets:precompile $ heroku create goodquestion $ git add . $ git commit -m “first commit to heroku” $ git push heroku master $ heroku run rake db:migrate $ heroku open