Wikit app view

Wikit


Project Date: July 2017

DESCRIPTION

A production quality SaaS application built on Ruby on Rails that allows users to create public and private markdown wikis and share them with other collaborators. A step-by-step overview of implementation will not be outlined below, but rather, discussion will focus on key components including, authentication, associations, file attachment, Markdown incorporation, Stripe integration, and scope/authorization. There will also be a cursory overview of decisions regarding user flow. The write-up assumes basic familiarity with the technology on the part of the reader. Environment setup is outlined in README in the GitHub repository, and is not covered here. TDD (Test-Driven Development) was not used for this app, but I have used it in others with the RSpec framework.
UPCOMING FEATURES
  • Email notification to collaborators about being added to collaborate on a wiki.
  • Sign in with user name, not just email address.
CODER COMMENT
Collaborative and educational software is of particular interest to me. It goes without saying that information availability and sharing is the greatest social benefit the internet has offered, and I valued the opportunity to explore one corner of technologies used for these applications. Because of this app’s functionality, including multiple user roles and a subscription option, user flow took consideration. Determining paths and redirects after actions informed the app’s features.

LINKS

Live App  please read testing below before trying app
GitHub Repo

TESTING

CONFIGURATION
  • Two-factor authentication is enabled. You will need to confirm an email account before signing in to the application. Confirmation emails can take a few minutes to be delivered. If you do not see one within five minutes, please check your spam folder.
  • Stripe Test Data (used for the Account Upgrade feature)
    • Email: choose any email address
    • Card Number: 4242 4242 4242 4242 or any of the test card numbers here.
    • Expiration: choose almost any month and year in the future
    • CVC: choose any three numbers
FEATURES
  • Authenticated user accounts
  • User accounts with two plan options
    • Free/Standard (default) account – create public wikis
    • Premium account – create public or private wikis
  • Upgrade a user account (from the Free plan to a Premium plan)
  • Downgrade a user account (from a Premium plan to a Free plan)
  • Create Markdown wikis with two privacy settings
    • Public (default) – can be viewed and edited by any user
    • Private (Premium user feature) – can only be viewed and edited by the wiki author, or any user added to the wiki as a collaborator
  • Browse wikis
  • Edit wikis
  • Delete wikis
  • Add collaborators to private wikis
  • Collaborators can edit private wikis to which they have been added
TASKS AND USER FLOW
The following is a set of tasks and instructions that are recommened to cover all major app features. A video overview of the app will be uploaded in the future for those who would rather watch instructions versus read them. It is recommened that you proceed through the tasks sequentially to avoid missing steps that are relied on in later tasks.

TASK 1 – VIEW AND CREATE PUBLIC WIKIS

  1. Use either of the “Sign Up” links on the Home page to create an account. Note down the email address and password you enter, as they will need to be used again after a logout. This will be the first account you use.
    • By default, this is a free (Standard) account that allows creating public wikis
  2. Use the “Browse all Wikis” link to go to the wikis page. The list of wikis on the wikis page are public wikis only. Private wikis do not appear here.
  3. Click on any of the wiki links to edit or delete the wiki.
  4. Use the “Create a Wiki!” link in the side menu, or on the “My Wikis” page to create a new Markdown wiki, with title, body, and image.
  5. A link to a Markdown Cheat Sheet is available on the New Wiki form page.
  6. Use the “My Wikis” link to view wikis you have authored. Click on any of the wiki links to edit or delete the wiki.
  7. Use the “Browse all Wikis” link to see your wiki(s) in the public wikis list.
TASK 2 – CREATE PRIVATE WIKIS

  1. Use the “Upgrade” link in the top navigation bar OR on your “My Account” page to upgrade to a Premium account.
    • Email: choose any email address
    • Card Number: 4242 4242 4242 4242 or any of the test card numbers here.
    • Expiration: choose almost any month and year in the future
    • CVC: choose any three numbers
  2. Use the “Create a Wiki!” button to create a new wiki.
    • In the new wiki, select the option for “Make private”, and save the wiki.
  3. Use the “Browse all Wikis” link to see your wikis in the list.
  4. Use the “Log out” link to log out of your account.
  5. Use the “Sign Up” link to create a second account. Note down the email address and password you enter, as they will need to be used again after a logout.
  6. Once logged in to the second account, use the “Browse all Wikis” link to view the list of public wikis (verify that you CANNOT see the private wikis created in the first account).
TASK 3 – ADD A COLLABORATOR TO A PRIVATE WIKI

  1. Log back into your first account.
  2. Use the “My Wikis” link to view your wikis, and click on the link for the private wiki you created.
  3. On the page for your private wiki, enter the email address for the second account you created, in the text box for “Add a Collaborator”. Then click “Submit a Collaborator”.
  4. Verify that you can see the address listed in the Collaborators table.
TASK 4 – EDIT A WIKI AS A COLLABORATOR

  1. Log out of your account, and sign back in with your second account.
  2. Use the “Browse all Wikis” link to verify that you can see all public wikis AND the private wiki on which you are a collaborator.
  3. Use the “My Wikis” link to see the wiki on which you are a collaborator.
  4. Click on the link for the wiki on which you are a collaborator. Verify that you can edit this wiki.
TASK 5 – DOWNGRADE AN ACCOUNT

  1. Log out of the second account, and log back in with your first account.
  2. Use the “My Account” link in the side menu to see your account page.
  3. Read the Downgrade Warning and click on the “Downgrade” button to downgrade your account back to Standard. You will have to click “OK” on a pop-up warning before the Downgrade is submitted.
  4. Use the “My Wikis” link to verify that your private wiki no longer has collaborators or an add collaborators option (it is now public).
  5. Log out of this account. Once logged out, use the “Browse all Wikis” link to verify that your once private wikis are now public.

Authentication

After creating the Rails app structure, I updated the Gemfile with, among other gems, sqlite3 for the Development database and pg, Heroku Postgres database, for the Production environment. After generating the Welcome Controller and related views, authentication was added using Devise. This app only required one Devise model, User, and views, redirects, and error messages were customized. Customization included overriding the default RegistrationsController and a creating a CustomFailure class to redirect paths after failed authentications or account creation. A _form.html.erb partial with customized Devise forms was rendered on the Welcome page, and resource mapping was used in the ApplicationController to allow these forms to access Devise variables and methods. The full implementation of this customization is not covered here, but rather discussed in a separate blog entry, Rails - Devise Customization.
config/routes.rb
devise_for :users, controllers: { registrations: "registrations", passwords: "passwords", confirmations: "confirmations" }
 
Devise controller filters and helper methods are used in application.html.erb and throughout the templates and controllers. In application.html.erb in particular, user_signed_in? is used to display items in the sidebar.
app/views/layouts/application.html.erb
<% if user_signed_in? %> 
    <%= link_to "My Wikis", users_show_path, :class => "list-group-item" %> 
    <%= link_to "My Account", account_management_path, :class => "list-group-item" %> 
<% end %>
 

MODELS

ASSOCIATIONS
Three models were generated for this app User, Wiki and Collaborator. The Rails generator was used, so the Ruby model classes and database migration files were created. User was discussed in Authentication, and I’ll refer you to the GitHub repo to review the models’ attributes. As to relationships, User and Wiki have has_many and belongs_to associations, as do Wiki and Collaborator. Wiki also has a destroy dependency with Collaborator. As a reminder (from the Features section), collaborators are users who have been given permission by the Wiki owner, User, to edit a private Wiki.
The belongs_to relationship between Collaborator and Wiki is relied on just once - the wiki method for Collaborator is used in the _show.html.erb partial in views/collaborators. The partial is rendered on the show view for users. The users show view first lists links to wikis the signed in user has created, and below that, there are links to wikis on which the user is a collaborator.
app/views/collaborators/_show.html.erb
<% collaborators.where(emailid: current_user.email).each do |collaborator| %>
    <%= link_to markdown(collaborator.wiki.title), wiki_path(collaborator.wiki.id) %>
<% end %>
 
Several options were considered for creating collaborators on wikis. One option was a Has Many Through (HMT) relationship with User. I decided not to create a HMT relationship with User for several reasons. Foremost, I wanted to easily add non-users as collaborators – once the non-user collaborator finally sets up a User account, the Collaborator email address would match the User account email address, and that User would then already have access to edit the private Wiki. A more efficient implementation of this may have been possible – e.g. one where fewer ActiveRecord objects are saved to the database (right now, multiple wikis can have collaborators with the same emailid key) – but the current means worked easily for what was needed.
VALIDATION
As previously alluded to, Collaborator objects, unlike User are not unique by a primary key. Instead, Collaborator has a unique relationship based on its foreign key, wiki_id. The Collaborator model’s validations, ensure that a Wiki cannot have more than one Collaborator with the same email address.
app/models/collaborator.rb
validates :emailid, uniqueness: { scope: :wiki_id, :message => "The user you entered is already a collaborator on this wiki." } 
validates_format_of :emailid, { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create, :message => "This is not a valid email address." } 
 

WIKI FEATURES

IMAGES
Paperclip was used for file attachment to wikis. The Paperclip helper was used to add the image database fields to the existing Wiki model. The has_attached_file method and a security validation are added to the Wiki model class. The styles hash just has two image sizes specified.
app/models/wiki.rb
has_attached_file :image, styles: { medium: "300x300>", thumb: "150x150#" } #, default_url: "/images/:style/missing.png"
validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/
 
The image is attached (new or updated) and removed on the wiki edit view. A couple removal methods were tried, and the one that was kept allows the user to select removal in conjunction with updating the other wiki attributes. This implementation uses a checkbox in the wiki update form. remove_image is a non-persistent field, and is used to flag removal of the attachment.
app/views/wikis/edit.html.erb
<div class="form-group">  
    <% if @wiki.image.exists? %>
        <br>
        <%= f.check_box :remove_image %>
        <label for="remove_image", style="color: red;">Remove Image</label>
    <% end %>
</div>
 
attr_accessor is used in the Wiki model class to make this instance variable editable and readable. Then a method reference callback is used to update the wiki’s image attribute based on the value (checkbox checked or not checked) of remove_image. !image_updated_at_changed? is used to determine if the user is trying to upload a new image while also checking Remove Image. In that case, image is not updated to nil.
app/models/wiki.rb
attr_accessor :remove_image
before_save :delete_image 
...
private

def delete_image
    if self.remove_image =="1" && !image_updated_at_changed?
        self.image = nil
    end
end
MARKDOWN
Redcarpet was used for Markdown processing on Wiki content. A markdown helper method is defined in application_helper.rb. Two hashes are specified containing Markdown options and extensions. A Redcarpet::Markdown object is instantiated with these settings. An HTML renderer is also initialized. Finally, the markdown text passed into the method is rendered. The markdown method is called in three views, including app/views/wikis/show.html.erb.

ROLES

UPGRADING AN ACCOUNT
One of Wikit’s features includes a user account being upgraded from the Standard (default) to a Premium account. An upgraded account allows the user to create private wikis. Account status is assigned in the role attribute of User. role is set to ‘standard’ upon object instantiation, using the after_initialize callback.
app/models/user.rb
after_initialize :init

def init
    self.role  ||= 'standard' 
end
 
To upgrade an account, and change role from ‘standard’ to ‘premium’, a user makes a payment via Stripe. The user can also downgrade a Premium account back to Standard. Integration was done using the Stripe gem. Per Stripe’s prescribed pattern, after configuration with the initialization file, I generated a ChargesController with new and create actions. customer and charge objects in the ChargesController. The new and create actions are standard for Stripe. A Customer is created as opposed to a Charge, so the card is available later for subscription charging.
Error handling with flash alerts was included in the new and destroy methods. Additionally, a User class method, update_role, is called in create and destroy to update the User role attribute and utilize customer.id. customer.id is stored in the User cid attribute in the create method, and current_user.cid is used in destroy to retrieve the customer object to be deleted. This method also updates the User role attribute, which is used in scopes and authorization.
app/controllers/charges_controller.rb
def destroy
    if current_user.role == 'standard'
        flash[:alert] = "You already have a Standard account."
        redirect_to account_management_path 
    else 
        cu = Stripe::Customer.retrieve(current_user.cid)
        cu.delete
        current_user.update_role('standard', nil)
        flash[:notice] = "#{current_user.email}, your Premium account has been downgraded back to a Standard account."
        redirect_to account_management_path 
    end 
end 
 
Finally, resource routing is used for the charges controller with the only option to specify creating only the new and create routes. I then use the as option to declare two custom routes for charges.
config/routes.rb
resources :charges, only: [:new, :create]

as :charge do  
    get 'account_management' => 'charges#account'
    delete 'downgrade' => 'charges#destroy', as: :destroy_charge
end 
SCOPE AND FILTERS
With three user roles (Standard, Premium, and Admin) and access for non-signed in visitors, Pundit was used to restrict wiki access based on User roles. The Admin role was not discussed previously, as it is not available via the app’s user functions (sign up, or upgrade). scope is used to restrict which wikis appear on the index page. An inner scope class is added to wiki_policy.rb to do this. The policy_scope method is then used in the index action in WikisController to return the array to be iterated over.
policy_scope was not used in views. The retrieval of appropriate objects there was done via the has_many or belongs_to associations between models. Also, instead of Pundit’s authorize method, before_action filters are used to halt the request cycle. Devise’s authenticate_user method is used for the new and create actions. The check_permission method is used for all others. This implementation seemed more efficient than using authorize in Pundit, but the code could perhaps be refactored to use authorize.
app/controllers/wikis_controller.rb
before_action :authenticate_user!, only: [:new, :create] 
before_action :check_permission, except: [:index, :new, :create]
...
def check_permission
    @wiki = Wiki.find(params[:id])
    if current_user 
        if not (@wiki.private == false || @wiki.user_id == current_user.id || current_user.role == 'admin' || @wiki.collaborators.where(emailid: current_user.email).length == 1)
            redirect_to root_url, :flash => { :alert => "You do not have access to this wiki" }
        end
    else 
        if not (@wiki.private == false)
            redirect_to root_url, :flash => { :alert => "You do not have access to this wiki" }
        end 
    end 
end

RESPONSIVE DESIGN

Responsive design was employed in this app, particularly for navigation, but also for view layouts. This is discussed in Responsive Design