My Sinatra App: Integral CRM
For my second project in Flatiron School, I created a Sinatra application using the MVC Framework. This is the first time I have ever worked with a domain specific language and I certainly learned a great deal over the course of the project. I am interested to further the learning from creating my app by learning more about Ruby on Rails; to better understand how to rapidly deploy highly functional web apps. But, enough about future plans.
I decided to build a CRM (highly ambitious, I know) to better understand ORM database concepts and gain more knowledge on what makes a Client Relationship Management system tick. I also planned for this to be an ongoing project, as this would be a great example to showcase the development of my front end skills; weaving in aspects of the back end to improve functionality of the CRM. I also wanted to use an API. I wanted to use an API in my first project. But, due to not having much experience with webscraping I decided against it and stuck with something simpler. Going in to the project, I felt confident in my abilities, so I decided to integrate Google’s API; specifically Google Calendar. Working with the API was surprisingly easy, after getting over the initial knowledge gap, and the model for the CRM came together easily. So, let’s look at what I built.
First, let’s take a look at the User model.
class User < ActiveRecord::Basehas_many :taskshas_many :caseshas_many :notes, through: :caseshas_secure_passwordvalidates_presence_of :username, :password_digestend
One of the requirements of the project was to enforce secure passwords for all users signing up/ using the application. Bcrypt, a Ruby gem, allows us to achieve this very easily with the macros
has_secure_password
And
validates_presence_of
to ensure a password has been created with the username. In order to allow sign ups and log ins to happen, their needs to be a controller for the User model. MVC, or Model View Controller, is a design pattern used for user interfaces. It commonly occurs in web development, allowing logic to be separated into distinct domains.
The code in the User Controller that handles sign ups is as follows:
post "/users/signup" do@user = User.new(:username => params[:username], :password => params[:password])if !User.find_by(username: @user.username) && @user.saveflash[:green] = {:title => “Success”, :text => “Created user account”}redirect "/users/login"elseflash[:red] = {:title => “Failure”, :text =>"unsuccessful sign up attempt"}redirect “/”endend
The sign up route creates the object initially with the new keyword. Unless this has the save method called upon it, and that method evaluates correctly, the new user will note be saved to the database. Referring back to the code for the User model, the “validates_presence_of” macro ensures we cannot save new records without the symbols referring to each piece of information being present. In this way, I ensure that the new user has a password and username. I also check for uniqueness of new usernames by attempting to find that username in the database, casting to a Boolean, and inverting the casted value. For example, if the username is found the “find_by” method will evaluate to true. This will then be cast to a Boolean by the prepended Bang character, then inverted from True to False. This means the If statement will not pass and will then move to the else clause.
Similar logic is used throughout the project, with Flash messages also being implemented. Let’s take a look at the Client model.
class Client < ActiveRecord::Basehas_many :caseshas_many :taskshas_many :notes, through: :casesvalidates_presence_of :name, :contact_number, :emailend
All models in the project utilise the ActiveRecord gem to creating ORM (Object Relational Mapping) for tables in my database and models in Ruby code. The “has_many” macro creates a one to many relationship with the symbol appended. The “through:” piece within the 3rds “has_many” allows data to form a hierarchy. This means it is possible for notes added to the database to know their case and client. In this way, we can ensure referential integrity throughout the database.
Looking at the Client Controller action to create a new client:
post "/clients" do@client = Client.create(params[:client])if @client.save@client.created_by = Helpers.current_user(session).idflash[:green] = {:title => "Success", :text => "Successfully created client"}redirect "/clients/#{@client.id}"elseflash[:red] = {:title => "Failure", :text => "Failed to create client"}redirect "/dashboard"endend
We can see a similar pattern used regarding the save method. This again ensures we are saving data to the database that is valid. To ensure Users cannot delete clients they did not create, we use a helper method to expose the id for the current user and assign that to a field within the database. This will then be used in the case of a delete request being sent to verify if the delete should resolve correctly. The Flash messages here are to ensure users get feedback for what happened with the create request. If the request is successful, the new Client is rendered within the view.
<!-- Header --><header class="w3-container" style="padding-top:22px"><div class="w3-right"><form action="/clients/<%=@client.id%>/delete" method="post"><input type="hidden" name="_method" value="delete"><button class="w3-button w3-red w3-padding-large w3-large w3-margin-top" type="submit">Delete Client</button></form></div><h5><b><i class="fa fa-address-card"></i> Clients</b></h5></header><div class="w3-row-padding w3-responsive"><div class="w3-cell-row"><div class="w3-third w3-padding"><h5><b>Details </b></h5><table class="w3-table-all w3-border w3-left"><tr><td>Name </td><td><%=@client.name%></td></tr><tr><td>Contact Number </td><td><%=@client.contact_number%></td></tr><tr><td>Email </td><td><%=@client.email%></td></tr><tr><td>1st Line of Address </td><td><%=@client.address1%></td></tr><tr><td>2nd Line of Address </td><td><%=@client.address2%></td></tr><tr><td>Postcode </td><td><%=@client.postcode%></td></tr><tr><td>City </td><td><%=@client.city%></td></tr><tr><td>Additional Info </td><td><h5><%=@client.add_info%></td></tr></table></div><div class="w3-twothird w3-padding"><h5><b>Cases</b></h5><table class="w3-table-all w3-hoverable w3-border"><thead><th>Id</th><th>Status</th><th>Open Date</th><th>Close Date</th><th>Last Note</th></thead><tbody><%@cases.each do |kase| %><tr><td><%=kase.id%></td><td><a href="/cases/<%=kase.id%>"><%=kase.status%></a></td><td><%=kase.open_date%></td><td><%=kase.close_date%></td><td><%=@client.notes.where("case_id = '#{kase.id}'").last.content%></td></tr></tbody><%end%></table></div><div class="w3-twothird w3-padding"><h5><b>Notes</b></h5><table class="w3-table-all w3-hoverable w3-border"><thead><th>Date</th><th>Owner</th><th>Title</th><th>Content</th></thead><tbody><%@notes.each do |note|%><tr><td><%=note.created_at%></td><td><%=note.owner%></td><td><%=note.title%></td><td><%=note.content%></td></tr><%end%></tbody></table></div><div class="w3-bar"><a href="/clients/index" class="w3-button w3-orange w3-padding-large w3-large w3-margin-top">Go back</a><a href="/clients/<%=@client.id%>/edit" class="w3-button w3-yellow w3-padding-large w3-large w3-margin-top">Edit Client Details</a><a href="/clients/<%=@client.id%>/cases/new" class="w3-button w3-green w3-padding-large w3-large w3-margin-top">Create new Case</a><a href="/clients/<%=@client.id%>/notes/new" class="w3-button w3-green w3-padding-large w3-large w3-margin-top">Add Note</a></div></div>
The html page above is the full view shown to the user when they create a client. This view allows for the creation of new notes and cases, other pieces of the CRM domain related to the client. This is not the full HTML page, but is the portion that is rendered within a template. With Sinatra, we can use the
<%=yield%>
To render an html page within another html page. This means the full page can be much smaller than normal and further works within the MVC principle of separating logic and data streams into distinct pieces.
This project also utilises Google OAuth2.0 and Calendar API. In future, this should be further developed to include all of Gsuite. For now, it only renders the users calendar on the dashboard page of the app. The process of utilising the Google API was simple, and I intend to write a guide breaking down the moving pieces of the API call so new users can get up to speed and create ideas quicker. So, to explore the API call in brief.
get '/oauth2callback' doclient_secrets = Google::APIClient::ClientSecrets.loadauth_client = client_secrets.to_authorizationauth_client.update!(:scope => 'https://www.googleapis.com/auth/calendar.readonly',:redirect_uri => url('/oauth2callback'))if request['code'] == nilauth_uri = auth_client.authorization_uri.to_sredirect to(auth_uri)elseauth_client.code = request['code']auth_client.fetch_access_token!auth_client.client_secret = nilsession[:credentials] = auth_client.to_jsonredirect to("/dashboard")endend
This route is found within my Application Controller. This is because I want the application itself to have ownership of the authorisation process, not any of the models. The route utilises a client_secrets.json file, which is obtained from Google Cloud Console. This is then used to create a credentials object utilised in building a service call. The route checks the code returned by the initial request, saving the credentials object parsed to a json in the session hash. This hash will be used whenever an API call is made so needs to be available throughout the program.
The API service then needs to be built.
client_opts = JSON.parse(session[:credentials])auth_client = Signet::OAuth2::Client.new(client_opts)calendar_service = Google::Apis::CalendarV3::CalendarService.newcalendar_service.authorization = auth_client
The credentials hash is used to create an authorisation object to be utilised by the service. Building the service is done by simply instantiating a new API service and setting the authorisation of it to the object just above. This service can then be used with the slew of methods found within Google’s documentation. The code for Google API was adapted from the Github’s own page, which is comprehensive. But, better explanation could be given to how the service works as well as how to properly set up your environment for use with the API. While this won’t be an issue for experienced programmers, if you have never used an API before, the amount of options and caveats is somewhat overwhelming.
So, this was an exploration of the key pieces of this Sinatra App. If this was an interesting read I’d highly recommend checking out the Github page https://github.com/Captainmango/IntegralCRM
I intend to continue updating this project as my skills develop. I feel this was a great success and am now looking forward to the next big Flatiron project.