Guide to simple Rails featuresMicah ShuteBlockedUnblockFollowFollowingFeb 20I recently made my first Ruby on Rails app — here are some resources and strategies I used to make it work, including:OAuth login/signup (Google)Authorization helpers and controller methods ( current_user, logged_in, authorize(user), render 403 errors)Polymorphic ActiveRecord relationships (follow users, message, and react to posts)Complex ActiveRecord class methods (most popular post)Setting up Bootstrap (incorporate with Rails’ forms)Rendering Markdown from user input ( kramdown setup, rouge syntax highlighting, custom sanitize)Google Authentication[Google OAuth gem](https://github.
com/zquestz/omniauth-google-oauth2)You can add a google_uid column and a google_refresh_token to the user table if you need a unique property with which to query a user, or you want to use more advanced features of Google's API (and allow an offline refresh of the token).
Because I have a uniqueness validation of a user's email and I am only using Google for authentication, I didn't need those columns but added them anyway just in case I had a design change in the future.
In my schema, I made a user's email validate uniqueness so that logging in with Google would check for an existing account with that email.
If found, you are logged into that account (with an updated google_uid and your password untouched), and if not found, it creates a whole new account.
→ Note: To make this method foolproof, it would be important to have a user verify their email if the account is made traditionally (ie without OAuth).
Also, you need to have an action dedicated to handling the OAuth callback.
This action needs to be associated with the callback route you specify in the Google API page (the link to that page is on the GitHub of the Google OAuth gem linked above).
My custom route for the callback that I defined in my routes.
rb file was: get 'auth/google_oauth2/callback, to: 'sessions#googleAuth'My callback action in my SessionsController utilized a custom class method written in the User model, to which it passed the return from the OAuth process, like so:From there, you just check to see if the user is valid, and log them in if it is, and otherwise display an error.
→ Note: The RandomPasswordStrategy is just a class I made that uses the sysrandom/securerandom method SecureRandom.
hex(64) combined with random ordering of required symbols and numbers (as required by the password policy) to create a complex, valid random password.
Authorization Methods, Error PagesI put multiple private methods in my `ApplicationController` to abstract away authentication throughout my app, making it easier to authenticate a user in the correct manner and display an appropriate message to them if there was an issue.
I wanted logged out users to be able to see most of the app but restrict certain aspects to only be accessible to users with accounts.
Also, I wanted to ensure that only the owner can edit/delete their posts and publish new posts under their name.
Additionally, I wanted some pages to display differently based on whether or not the user is logged in.
To help perform these tasks in a modular and simple way, I added two methods toApplicationController so they are available to all Controllers (which inherit from ApplicationController), as well as adding them as helper_methods to make them available in the Views if necessary:These are primarily used as decision-makers on what is to be shown on certain pages, or as building blocks to more advanced authorization methods.
If an unauthorized user tries to access a page you don't want them to, it is appropriate to show a 403 error page.
This is easily accomplished by creating a public/403.
erb file, and calling it via another method in ApplicationControllerWhich can accept a custom flash message as a parameter, and which serves as a good building block for other methods, like this:If authorize is called without a user parameter, it will only display a 403 error page if the user is not logged in.
If it is called with a user parameter (which is not null), the 403 will be called unless that specific user is logged in.
It also returns a boolean to indicate success or failure.
→ Note: If you need to use current_user in the controller after calling authorize, you will get an error if the authorization fails.
An easy way to fix this is to use an if-else block with the authorization check rather than using it as a standalone function at the top of your controller action.
This way, any code which calls current_user will not be run unless authorization succeeds, and if it fails, once the controller action is complete it will render the 403 error page.
Following Users, Sending Messages, Reacting to Posts, Scope MethodsFor some relationships, ActiveRecord relations aren't as simple as something likehas_many :classes, through: :user_classes.
Three situations particularly I had to get more creative to make work:Following other Users:Many to many relationships, i.
a user is following many users and is followed by many users.
The answer to the problem is just a simple join table, but it's just slightly different than a standard join table, as it joins a table to itself, and requires a few extra words in the ActiveRecord class methods.
The join table I used was:And then to make the User model work correctly:MessagingThis was pretty similar to implementing following/followers, except the model `Message` is the join table, and is also the model that we are interested in, so it is simply just:Reacting to PostsThere were a few solutions to this problem that are viable, but I decided to make a database table reaction_types which has a many-to-many polymorphic relationship to anything that is reactable, in my case topics and posts.
reaction was the join table betweenreactables and the reaction_types.
I did this instead of reactions holding the reaction type via a string in order to minimize errors and to allow for flexibility – for example, the easy expansion of allowed reaction-types in the future.
Doing it this way made reactions a 3-way join table between user, reactables, and reaction_types.
reaction_types have to be seeded to the types you want to be available to the user.
I chose like, dislike, genius, and report.
In the same fashion using polymorphic relationships, you can create tags that can be attached to taggable objects, and post replies to objects which are postable.
Advanced ActiveRecord class methodsWhen I was trying to make the correct ActiveRecord class methods to retrieve specific "statistics" for a user dashboard or to find popular topics for my homepage, I found that the best resource for me was to see other advanced and specific class methods and use those examples as a reference.
I will put some of mine here in the hopes that others can use it as a reference.
To create a "spotlight topic" of the day for my homepage, take the most positively reacted topic in the past 24 hours:Find theTopic which was written by a certain User with the most reactions of a specific type.
You can use this to add to their dashboard, so they can see what their most liked post is, for example:Setting up Bootstrap[This post](https://medium.
org/add-bootstrap-to-your-ruby-on-rails-project-8d76d70d0e3b) walks through how to set up your rails app with bootstrap in just a few minutes.
There is a complication with bootstrap that won't be ideal right 'out of the box': while rails will give your form_for fields the fields_with_errors class automatically when there are issues with the form inputs, Bootstrap will not respond to it.
Instead, it responds to the class is-invalid.
– [This blog](https://jasoncharnes.
com/bootstrap-4-rails-fields-with-errors/) shows a good working solution to make form errors work well with bootstrap.
It just includes adding a file and code to your config/initializers folder, and basically all it does is change the default field-with-errors class to is-invalid, which is what bootstrap uses to indicate a field which was filled out incorrectly.
Rendering MarkdownI was between the two libraries redcarpet and kramdown.
I ended up choosing [kramdown](https://github.
com/gettalong/kramdown) because:It is more recently committed and seems to be maintained moreIts popularity is growing while redcarpet is decreasingIt allows integration with MathJax which renders LaTex equations.
Getting basic integration was relatively simple.
It got a little more complicated to achieve the following 2 goals:→ Syntax highlighting for code→ Being able to sanitize the user's input while allowing all of the Markdown features to implement correctly.
I used [rouge](https://github.
com/jneen/rouge) for syntax highlighting.
I'll show you the code below to get it working with Kramdown, but the hard piece of information to find was how to get the proper CSS files that make the highlighting happen.
Here's a good [stack overflow answer](https://stackoverflow.
com/questions/43905103/kramdown-rouge-doesnt-highlight-syntax) that's hard to find.
Basically, once you tell kramdown that you want to use rouge (and you have installed both the kramdown gem and the rouge gem), you can run rougify help style in your terminal, and you can see all of the custom CSS files you can add to your app/assets/stylesheets path.
To get the raw CSS, type rougify style followed by one of the allowed styles in your terminal.
For example: rougify style colorful.
The CSS will be printed in your terminal.
You could also runrougify style colorful > .
cssNow it’s relatively easy to render markdown.
I made a helper method to make it extra simple:Custom sanitize method:As you can see here, I am not using raw or .
html_safe to render this HTML data because ultimately it came from a user and cannot be trusted.
However, the safe sanitize method disallows certain things I want to be rendered, such as tables.
You can get proper sanitation AND your desired HTML tags by manually appending to the whitelisted tags allowed through `sanitize` by doing something like what is shown below.
General tipsSet up the app with PostgreSQL [Here's a blog post](https://medium.
shute/setting-up-windows-subsystem-for-linux-wsl-6346ff23b8bb) I wrote about setting up WSL, but there's a section at the bottom that covers using pgAdmin and setting up a Rails app with PostgreSQLSave all secret keys as environment variables I used [Figaro](https://github.
com/laserlemon/figaro) to make it easy.
Avoid the N+1 problem This occurs when you get a collection of models, of which you want to query and show nested models.
If you iterate over your models `n` times to do this, you are making `n+1` database calls which can be quite expensive with large amounts of data, especially when requesting over a network.
This is easily fixed by using the `includes` method when making your initial query.
[Check this site out](https://guides.
html) and go to the section called "Solution to N + 1 queries problem" for more info.
Clean up the database automatically when objects are destroyed When you appropriately add dependent: :destroy to your ActiveRecord class methods (examples can be seen in code snippets above), you are telling ActiveRecord to destroy these relations upon the destruction of the model.
As you can see above, I implemented this for reactions within post.
This tells rails that when a post is destroyed, all user reactions to it should also be destroyed.
This prevents disconnected likes and dislikes, etc, from floating around in your database for no reason.
Also, note this should be a one-sided relationship.
You would not want a post to be destroyed if a user destroys their one reaction to it.
Adding custom files and classes There are going to be things you want your app to do that are outside of MVC.
This means you are going to want to add files outside of the standard `model`, `view` and `controller` directories.
Here are some resources with advice on how to do this:→ [A StackOverflow answer](https://stackoverflow.
com/questions/15260984/guidelines-for-where-to-put-classes-in-rails-apps-that-dont-fit-anywhere)→ [A GitHub gist post](https://gist.
com/maxim/6503591)Personally, I used a lib directory inside of my app directory because it is eagerly loaded in production and lazy loaded in development.
I did not need to alter any configuration or environment files for the classes within files in that directory were referenced.