If Sinatra is considered a lightweight DSL web framework, then Ruby on Rails (or just “Rails”) is the heavyweight champion of the world. While both Rack-based systems are built on the model-view-contr0ller (MVC) paradigm in the Ruby programming language, Rails adds significantly more out-of-the box functionality (though in some cases less is more).
With my expanded knowledge of web programming through learning about Rails, I expanded and built on my Entertainment Management System (originally my Sinatra project) as my Rails assessment project, recreating the existing application in Rails’ framework with the same shows (now performances), artists, and venues models, as well as adding in additional models for contracts (for performances), documents (for contracts), approvals (for contracts), and implementing a more comprehensive user management system. While I’m not going to go into a full explanation of all of the features (particularly those discussed in my Sinatra-version), I’ll review some of the changes and enhancements that I made along with some lessons learned.
One of the new concepts Rails presented was its comprehensive routing system and its use of RESTful conventions, but how it can also be modified to fit an application’s needs. As such, one of the requirements of the project was to implement a nested form and nested resources. I decided to implement these requirements through the addition of the two new models- approvals and documents. First though, I knew I wanted to add functionality for a contract. Every Performance should have a Contract, which can contain various pieces of information including a series of documents and sign-off/approvals, and tracking when the contract was issued and finalized. As I thought about how to implement a contract, I considered including some of these elements within the Performance model itself. However in looking at my application from a holistic perspective, while also thinking in terms of separation of concerns, I opted to let a contract be its own model- if I were to enhance this application further (i.e. to be able to sell tickets for a given performance), I would likely want to keep this information separate from the contract, yet linked to the performance.
I made a design decision to add the ability to designate an approver on a contract as a nested form within the main Contract form. Rather than creating the contract and having to go through additional steps to add an approver, one could be added at the same time. To do this, I implemented a custom approval attribute writer method within the contract model (as an alternative to using the accepts_nested_attributes_for macro) to ensure that an approver is only added when necessary and to set the initial status of the approval (pending).
def approvals_attributes=(approval_attributes) approval_attributes.values.each do |approval_attribute| if !approval_attribute[:user_id].blank? approval = Approval.find_or_initialize_by(approval_attribute) approval.status = "pending" if !approval.persisted? approval.save self.approvals &amp;amp;lt;&amp;amp;lt; approval end end end
The approval and it’s status can then be viewed on the Contract “show” view. Additionally, individuals assigned to a specific approval (e.g. authorized to approve) are presented with buttons to Approve or Reject the contract. An administrator is also presented with a button to cancel the approval request. These buttons, while on the Contract view, rely on methods in the Approval model called by the Approval controller. However, there are no specific “views” for this model.
Next, with the notion that a contract has a one-to-many relationship with Documents, I configured the documents resource as a nested resource of contracts within the routes file.
resources :contracts do resources :documents, only: [:new, :create, :edit, :update, :destroy, :show], as: :documents end
As a result, all documents would have a URL with the format of /contracts/:contract_id/documents/:document_id. To edit a document’s information, for example, a “/edit” would be appended to the end of the URL. Currently a document stores a name of the document and a URL to the document itself (i.e. stored on Dropbox or some other file storage service). In the future, I could see adding the ability to allow an authorized user to upload a file (i.e. an image, Word doc, Excel doc, etc.) as a worthwhile enhancement.
The requirements for the project also included implementing an authentication system with the ability to authenticate via a 3rd party service (i.e. Facebook, Google, etc.). To meet this requirement, I implemented the Devise gem. Devise sits on top of the user model and is composed of a number of modules capable of controlling user registration, authentication, tracking of user logins, and implementing an OmniAuth solution to enable logins via other services (among others). While utilizing Devise on the base user model, I also added additional fields to the model (i.e. name, company, phone number). This required customization of the generic Devise views and its allowable strong parameters.
Due to the confidential nature of the types of business transaction the application will manage, a user must be approved by an administrator in order to utilize the application. Subsequently, a user administration view was developed to approve or unapprove users, as well as assign their role and associated organization as well.
Once a user had a basic account created, they would be able to associate their user account with a 3rd party login account, in this case Google and/or LinkedIn (yes, both!) utilizing OmniAuth. To maintain information on these multiple logins, an additional Identity model was required as well (note: due to the intricate nature of implementing OmniAuth within Devise, I did utilize a number of tutorials.1 However, each user could only be linked to one Google, and one LinkedIn account and the e-mail address for those accounts needed to match their registered e-mail address for the Entertainment Management System. This is to ensure an organizational account is being used to view privileged information.
class Identity < ActiveRecord::Base belongs_to :user validates_presence_of :uid, :provider validates_uniqueness_of :uid, :scope => :provider validates_uniqueness_of :user_id, :scope => :provider def self.find_for_oauth(auth) find_or_create_by(uid: auth.uid, provider: auth.provider) end end
A user’s role and associated organization comes into play with the authorization system, implemented through Pundit policies. Pundit allows for granular control over controller actions, as well as content displayed in views. This enabled me to create a Policy for each model dictating what information a venue user, an act user, or an administrator could view, create, edit, or delete. Pundit scopes were implemented for a number of policies as well to ensure that a user associated with an Act only saw information on their own Act’s performances, the venues they would be performing at, and their Act’s contracts. Similarly, a user associated with a venue would only be able to view information on their performances, the acts performing at their venues, and the associated contracts.
Don’t Repeat Yourself (DRY)
One of the key principles in programming is “Don’t Repeat Yourself” or DRY. In my previous Sinatra version, I was re-creating and displaying many of the same tables of information across related models (i.e. all shows at a given Venue was almost the same code as all Shows for a given act). In order to reduce the amount of code as well as be able to better maintain consistency across all models where such information was required, I implemented this information into a number of partials where I could call these ERB HTML-based elements where needed and pass in the data that was being pulled by the particular model’s controller. I also implemented a few helpers used across the application to perform function such as formatting data attributes including dates and times.
Depth vs. Breadth
As I look at what I created with this project, I feel very proud and accomplished with what I produced. I felt that I met the requirements of the assessment along with adding a LOT more depth and functionality to the application. However, at a certain point in the development process, I felt that for the purposes of completing this project for the course, I may have been a little overly ambitious. I determined I had close to 80 user stories that needed to be addressed, yet I was far enough in that it would have been difficult to turn back and defer some of these as future enhancements. As a result, this project ultimately took me about 4-5 weeks to complete. Had I done some additional planning in the beginning, I would have targeted working on the project for about 3 weeks to continue on in the curriculum and add the additional functionality I desired at a later time. Consequently, as I continue on in my journey of programming, one of the less technical, but crucial lessons learned that I away will be to take additional time to plan and determine the appropriate scope.