Add a Simple Wiki to your Rails App

I added a wiki to one of my sites. This may inspire you to create one yourself.

me@jaykilleen.com wrote this about 7 years ago and it was last updated about 7 years ago.


← Back to the Posts

_form.html.erb

<%= bootstrap_form_for(@wiki_page, html: { class: 'form-horizontal' } ) do |f| %>
  <fieldset>
    <%= f.text_field :title %>
    <%= f.text_area :content, :rows => 15, placeholder: "Write about this wiki"  %>
    <%= f.text_field :path %>
    <%= f.text_field :wistia %>
    <%= f.collection_select :parent_id, WikiPage.all, :id, :title, {:include_blank => ''}, class: 'form-control input-sm' %>
    <%= f.text_field :tag_list, value: @wiki_page.tag_list.to_s, placeholder: "Add a comment", label: "Tags (separated by commas)", class: "form-control" %>
    <div class="form-actions">
      <%= f.button :submit, class: 'btn btn-success' %>
    </div>
  </fieldset>
<% end %>

_side_panel.html.erb

<div id="wiki-side-panel" class='col-md-3'>
  <div class="well well-sm" >
    <div id="side-panel" class="panel panel-default">
      <% if @wiki_page.present? %>
        <div class="panel-heading"><h4>Wiki</h4></div>
        <div class="panel-body">
          <%= link_to 'Home', wiki_pages_path %>
        </div>
        <div class="panel-body">
          <% if params[:action] != 'new' %>
            <% if policy(@wiki_page).new? %>
              <p><%= link_to 'New Wiki', new_wiki_page_path %></p>
            <% end %>
          <% else %>
            <p><%= @wiki_page.back %></p></p>
          <% end %>
        </div>
        <% if @wiki_page.parent != nil %>
          <div class="panel-heading"><h4>Parent Page</h4></div>
          <div class="panel-body">
            <%= link_to @wiki_page.parent.title, wiki_page_path(@wiki_page.parent) %>
          </div>
        <% end %>
        <% if @wiki_page.persisted? && @wiki_page.has_children? %>
          <div class="panel-heading"><h4>Child Pages <span class="badge"><%= @wiki_page.children.count %></span></h4></div>
          <% @wiki_page.children.each do |child_page| %>
            <div class="panel-body">
              <%= link_to child_page.title, wiki_page_path(child_page) %>
            </div>
          <% end %>
        <% end %>
      <% else %>
        <div class="panel-heading"><h4>Wiki Pages</h4></div>
        <% WikiPage.first(5).each do |wiki_page| %>
          <div class="panel-body">
          <%= link_to wiki_page.title, wiki_page_path(wiki_page) %>
          </div>
        <% end %>
      <% end %>
    </div>
    <div id="wiki-tag-cloud" class="panel panel-default">
      <div class="panel-heading"><h4>Most Used Tags</h4></div>
        <div style="padding:5px;">
          <% ActsAsTaggableOn::Tag.most_used(25).each do |tag| %>
              <%= link_to tag.name, tag_path(tag.name) %>
          <% end %>
        </div>
    </div>
  </div>
</div>
<div class='col-md-12'>

_wistia_embed.html.erb


<div id="wistia_<%= wistia_id %>" class="wistia_embed" style="width:640px;height:400px;">&nbsp;</div>
<script charset="ISO-8859-1" src="//fast.wistia.com/assets/external/E-v1.js"></script>
<script>
wistiaEmbed = Wistia.embed('<%= wistia_id %>', {
  videoFoam: true
});
</script>


edit.html.erb


<div class="page-header">
  <h1>Editing Wiki Page</h1>
  <p><%= @wiki_page.back %></p>
</div>
<div class="col-md-9">
  <%= render 'form' %>
  <p><%= @wiki_page.pun_destroy_link %></p>
</div>
<%= render 'side_panel' %>


Gemfile


source 'https://rubygems.org'
ruby '2.0.0'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.0'
gem 'passenger'
gem 'bcrypt', '~> 3.1.7'
gem 'devise', '~> 3.4.1'
gem 'pundit'
gem 'ancestry'

# Use postgresql as the database for Active Record
gem 'pg'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 4.0.3'
# All the things for Bootstrap Sass
gem 'bootstrap-sass', '~> 3.3.1'
gem 'bootstrap_form'
gem 'draper', '~> 1.3'

# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails', '~> 4.0.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
gem 'therubyracer',  platforms: :ruby
gem 'will_paginate', '~> 3.0.7'
gem 'will_paginate-bootstrap', '~> 1.0.1'
gem 'simple_form'
gem 'activerecord-session_store'
gem 'ransack', '~> 1.6.3'

# markdown in comments
gem 'html-pipeline', '~> 1.11.0'
gem 'github-markdown', '~> 0.6.7'
gem 'gemoji', '~> 2.1.0'
gem 'sanitize', '~> 3.0.3'

#wiki and tags
gem 'acts-as-taggable-on', '~> 3.4'
gem 'wistia-api', '~> 0.2.3'

group :development, :test do
  gem 'minitest'
  gem 'rspec', '~> 2.14'
  gem 'rspec-rails'
  gem 'capybara', '~> 2.2.0'
  gem 'factory_girl_rails', '4.2.1'
  gem 'faker'
  gem 'launchy', '~> 2.1.2'
  gem "shoulda-matchers", "~> 2.4.0"
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-commands-rspec'
  gem 'jazz_hands'
  gem 'pry-byebug'
end

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
# gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.

group :doc do
  gem 'sdoc', '~> 0.4.0', :require => false
end

group :development do
  gem 'better_errors'
  gem 'binding_of_caller'
  gem 'annotate', '~> 2.6.5'
  gem 'guard-rspec', require: false
  gem 'web-console', '~> 2.0'
end

gem 'rails_12factor', group: :production do
  gem 'capistrano',  '~> 3.1'
  gem 'capistrano-rails', '~> 1.1.1'
end


index.html.erb


<% content_for :title, "Wiki" %>
<div class="page-header">
  <h1><%= link_to 'Wiki', wiki_pages_path %></h1>
  <small><p>Your complete guide to this app</p></small>
</div>

<div class="row">
  <div class="col-sm-9">
    <%= search_form_for @query, url: request.path, html: { class: "input-group customer-search-form"}  do |f| %>
      <% name = :title_or_content_cont %>
      <%= f.text_field(name, :id => :query_field, :class => 'input form-control') %>
      <span class="input-group-btn">
        <%= button_tag(type: "submit", class: "btn btn-default", id: :search_button) do %>
          <i class="glyphicon glyphicon-search"></i>
        <% end %>
      </span>
    <% end %>
  </div>
  <div class="col-sm-3">
    <%= link_to "Clear Search", request.path %>
  </div>
</div>

<div class='col-md-9'>
  <div class="text-center">
    <div class="pagination centre">
      <%= will_paginate @wiki_pages, renderer: BootstrapPagination::Rails %>
    </div>
  </div>

  <div class="table-responsive">
    <table class="table table-hover">
      <tr>
        <th><%= sort_link(@query, :title) %></th>
        <th><%= sort_link(@query, :content) %></th>
        <th>Video</th>
        <th>Comments</th>
        <th></th>
        <th></th>
      </tr>
      <% policy_scope(@wiki_pages).each do |wiki_page| %>
        <% wiki_page = wiki_page.decorate %>
        <tr>
          <td><%= link_to wiki_page.title, wiki_page_path(wiki_page) %></td>
          <td><%= wiki_page.content[0..30] + "..." %></td>
          <td><%= boolean_to_human(wiki_page.wistia.present?) %></td>
          <td><%= wiki_page.comments.count %></td>
          <% if policy(wiki_page).update? %>
            <td><%= wiki_page.pun_edit_link %></td>
          <% else %>
            <td></td>
          <% end %>
          <% if policy(wiki_page).destroy? %>
            <td><%= wiki_page.pun_destroy_link %></td>
          <% else %>
            <td></td>
          <% end %>
        </tr>
      <% end %>
    </table>
    <div class="text-center">
      <div class="pagination centre">
        <%= will_paginate @wiki_pages, renderer: BootstrapPagination::Rails %>
      </div>
    </div>
    <% if policy(@wiki_pages).new? %>
      <%= link_to 'Create Wiki', new_wiki_page_path, class: 'btn btn-default' %>
    <% end %>
  </div>
</div>
<%= render 'side_panel' %>


new.html.erb


<div class="page-header">
  <h1>New Wiki Page</h1>
  <p><%= @wiki_page.back %></p>
</div>

<div class="col-md-9">
  <%= render 'form' %>
</div>
<%= render 'side_panel' %>


show.html.erb


<% provide(:title, @wiki_page.title) %>

<div class="page-header">
  <h1><%= @wiki_page.title %></h1>
  <p><%= @wiki_page.back %></p>
</div>

<div class="col-md-9">
  <small><p>Created by <%= @wiki_page.created_by_user_name + " " + @wiki_page.time_since_created %></p></small>
  <small><p>Updated by <%= @wiki_page.updated_by_user_name + " " + @wiki_page.time_since_updated %></p></small>
  <div class="bs-callout bs-callout-info">
    <%= markdownify(@wiki_page.content) %>
  </div>
  <% if @wiki_page.wistia.present? %>
    <div style="margin-right:5px;">
      <%= render partial: "wistia_embed", locals: {wistia_id: @wiki_page.wistia} %>
    </div>
  <% end %>
  <p>Tags: <%= raw @wiki_page.tag_list.map { |t| link_to t, tag_path(t) }.join(', ') %></p>
  <% if policy(@wiki_page).update? %>
    <%= link_to "Edit", edit_wiki_page_path(@wiki_page), class: 'btn btn-primary' %>
  <% end %>
  <%= render partial: "comments/comments", locals: {commentable: @wiki_page} %>
  <%= render partial: "comments/form", locals: {commentable: @wiki_page} %>
</div>
<%= render 'side_panel' %>


user_creates_a_wiki_page_spec.rb


require 'spec_helper'

feature 'User creates a wiki page' do
  
  let(:wiki_page) { create(:wiki_page) }
  let(:user) { create(:user) }
  let(:super_admin) { create(:super_admin) }

  before(:each) do
    sign_in_as_user(super_admin)
  end

  scenario 'they can create a wiki from the index page' do
    visit wiki_pages_path
    click_link 'Create Wiki'
    fill_in 'wiki_page_title', with: "A wiki"
    fill_in 'wiki_page_content', with: "You know its a wiki"
    click_button 'submit'
    expect(WikiPage.last.title).to eql("A wiki")
  end

  scenario 'only super admin can create pages' do
    change_user_role(user, 'account')
    visit wiki_pages_path
    click_link 'Create Wiki'
    expect(page).to_not have_content("Create Wiki")
  end

  scenario 'even if they try and force their way in' do
    change_user_role(user, 'account')
    visit new_wiki_page_path
    expect(page).to_not have_content("Create Wiki")
    expect(page).to_not have_content("You cannot perform this action.")
  end
end


wiki_page.rb


# == Schema Information
#
# Table name: wiki_pages
#
#  id         :integer          not null, primary key
#  path       :string
#  title      :string
#  content    :string
#  ancestry   :string
#  created_by :integer
#  updated_by :integer
#  created_at :datetime
#  updated_at :datetime
#  wistia     :string
#

class WikiPage < ActiveRecord::Base

  has_ancestry
  acts_as_taggable

  # RELATIONSHIPS
  has_many :comments, as: :commentable
  belongs_to :user, :foreign_key => 'created_by'

  # VALIDATIONS
  validates_presence_of :title
  validates_presence_of :content

  # VALIDATIONS
  #scope :has_video, where("wistia <> ''")

  def created_by_user
    user_id = self.created_by
    if !user_id.blank?
      author = User.find(user_id)
    end
  end

  def updated_by_user
    user_id = self.updated_by
    if !user_id.blank?
      updater = User.find(user_id)
    end
  end

end


wiki_page_decorator.rb


class WikiPageDecorator < ApplicationDecorator
  delegate_all
  decorates_association :comments

  # Define presentation-specific methods here. Helpers are accessed through
  # `helpers` (aka `h`). You can override attributes, for example:
  #
  #   def created_at
  #     helpers.content_tag :span, class: 'time' do
  #       object.created_at.strftime("%a %m/%d/%y")
  #     end
  #   end

  def created_by_user_name
    object.created_by_user.present? ? h.link_to(object.created_by_user.name, h.user_path(object.created_by_user)) : ""
  end

  def updated_by_user_name
    object.updated_by_user.present? ? h.link_to(object.updated_by_user.name, h.user_path(object.updated_by_user)) : ""
  end

  def back
    h.link_to 'Back', h.wiki_pages_path
  end

  def pun_edit_link
    h.policy(object).update? ? h.link_to('Edit', h.edit_wiki_page_path(object)) : ""
  end

  def pun_destroy_link
    h.policy(object).destroy? ? h.link_to('Destroy', object, method: :delete, data: { confirm: 'Are you sure?' } ) : ""
  end

end


wiki_page_policy.rb


class WikiPagePolicy < ApplicationPolicy
  class Scope < Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all unless @user.blocked?
    end
  end

  def show?
    return true unless @user.blocked?
  end

  def create?
    return true if @user.super_admin?
  end

  def new?
    create?
  end

  def update?
    return true if user.super_admin?
  end

  def edit?
    update?
  end

  def destroy?
    return true if user.super_admin?
  end
end


wiki_page_policy_spec.rb


require 'spec_helper'
require 'pundit/rspec'

describe WikiPagePolicy do

  let!(:user) { create(:user) }
  let!(:super_admin) { create(:super_admin) }
  let!(:blocked_user) { create(:blocked_user) }

  subject { WikiPagePolicy }

  permissions ".scope" do
  end

  permissions :index? do
  end

  permissions :create? do
    it "grants access if user is super admin" do
      expect(subject).to permit(super_admin)
    end

    it "rejects access to everyone else" do
      #rejects
      expect(subject).to_not permit(user)
      expect(subject).to_not permit(blocked_user)
    end
  end

  permissions :show? do
    it "grants access if user is super admin" do
      expect(subject).to permit(super_admin)
    end

    it "rejects access if user is blocked" do
      expect(subject).not_to permit(blocked_user, supplier)
    end
  end

  permissions :update? do
    it "grants access if user is super admin" do
      expect(subject).to permit(super_admin)
    end
  end

  permissions :destroy? do
    it "grants access if user is super admin" do
      expect(subject).to permit(super_admin)
    end
  end
end


wiki_pages_controller.rb


class WikiPagesController < ApplicationController

  before_action :authenticate_user!
  before_action :redirect_blocked_user
  before_action :set_wiki_page, only: [:show, :edit, :update, :destroy]

  after_filter :verify_authorized,  except: :index
  after_filter :verify_policy_scoped, only: :index

  def index
    if params[:tag]
      @query = policy_scope(WikiPage.tagged_with(params[:tag])).ransack(params[:q])
      @wiki_pages = @query.result(distinct: true).paginate(:page => params[:page])
    else
      @query = policy_scope(WikiPage).ransack(params[:q])
      @wiki_pages = @query.result(distinct: true).paginate(:page => params[:page])
    end
  end

  def show
    authorize @wiki_page
    @wiki_page = @wiki_page.decorate
  end

  def new
    @wiki_page = WikiPage.new.decorate
    authorize @wiki_page
  end

  def edit
    authorize @wiki_page
    @wiki_page = @wiki_page.decorate
  end

  def create
    @wiki_page = WikiPage.new(wiki_page_params)
    @wiki_page.created_by = current_user.id
    authorize @wiki_page
    respond_to do |format|
      if @wiki_page.save
        format.html { redirect_to @wiki_page, notice: 'WikiPage was successfully created.' }
      else
        format.html { render :new }
      end
    end
  end

  def update
    @wiki_page.updated_by = current_user.id
    authorize @wiki_page
    respond_to do |format|
      if @wiki_page.update(wiki_page_params)
        format.html { redirect_to @wiki_page, notice: 'WikiPage was successfully updated.' }
      else
        format.html { render :edit }
      end
    end
  end

  def destroy
    authorize @wiki_page
    @wiki_page.destroy
    respond_to do |format|
      format.html { redirect_to wiki_pages_url, notice: 'WikiPage was successfully destroyed.' }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_wiki_page
      @wiki_page = WikiPage.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def wiki_page_params
      params.require(:wiki_page).permit(:title, :content, :path, :wistia, :parent_id, :tag_list, :created_by, :updated_by)
    end
end


wiki_pages_helper.rb


module WikiPagesHelper

  def wistia_embed
     Wistia.embed(@wiki_page.wistia, {
      videoFoam: true
    });
  end
end


wiki_pages_routing_spec.rb


require "spec_helper"

describe WikiPagesController do
  describe "routing" do
    it("routes to #index") { get("/wiki_pages").should route_to("wiki_pages#index") }
    it("routes to #show") { get("/wiki_pages/1").should route_to("wiki_pages#show", :id => "1") }
    it("routes to #new") { get("/wiki_pages/new").should route_to("wiki_pages#new") }
    it("routes to #create") { post("/wiki_pages").should route_to("wiki_pages#create") }
    it("routes to #edit") { get("/wiki_pages/1/edit").should route_to("wiki_pages#edit", :id => "1") }
    it("routes to #update") { put("/wiki_pages/1").should route_to("wiki_pages#update", :id => "1") }
    it("routes to #destroy") { delete("/wiki_pages/1").should route_to("wiki_pages#destroy", :id => "1") }
  end
end


yyyymmddmmhhss_create_wiki_pages.rb


class CreateWikiPages < ActiveRecord::Migration
  def change
    create_table :wiki_pages do |t|
      t.string :path
      t.string :title
      t.string :content
      t.string :ancestry
      t.integer :created_by
      t.integer :updated_by
      t.timestamps
    end
  end
end