DEV Community

Cover image for How I solved a harder part of my Rails hike finder/reviewer app
Kevin Kirby
Kevin Kirby

Posted on • Updated on

How I solved a harder part of my Rails hike finder/reviewer app

I recently finished my rails project at Flatiron School. We were to create a Rails MVC content Management system app, and so I created a hike finder/reviewer app called Nail the Trail.

Alt Text

Nail the trail allows users to find hikes, see intel and reviews for certain hikes to enable success on-trail, create reviews for hikes, edit and delete those reviews, and more.

In envisioning the site, I wanted someone cloning into the project to be able to see many hikes, and reviews for these hikes, as soon as they got onto my site. I also of course wanted users to be able to create/see/update/delete reviews of their own.

With this goal in mind, I decided to make a complex seeds file in which I'd use a lot of faker functionality. The 3 models in my project are Hike, User, and Review - Review being the join. So, in my seed file I created 100 faker'ed hikes, 80 faker'ed users, and 600 faker'ed reviews.

I made it so the hikes were randomly distributed across 12 cities and that the user_id and hike_id for the faker'ed reviews were set to a random user's id of the 80 faker'ed users and then a random hike's id of the 100 faker'ed hikes. This turned out to be a good balance as when a user selects to see hikes in a city, they see hikes in each city and they see already left reviews for almost every hike.

One of the harder parts of my project to get working correctly was the following: Though not necessary, I strongly wanted all hikes on my site to be given an average rating out of 5 that was the average of the star_rating attribute that each of their reviews had - both already existing reviews and reviews that would be manually created. And, I wanted this average rating attribute of each hike to change correctly depending on newly created reviews, updated reviews, and deleted reviews and to actually be continually updated in the database.

I got this all working correctly after some time! Below are the steps I took to get this working.

-I made it so the hikes table had an average star rating attribute that defaulted to 0 for all the created hikes. Also, instead of creating all of my faker'ed hikes so they had some random average rating, I left that attribute out as I wanted to take the more difficult approach of making it so the average rating of hikes were solely based on the average of their reviews' ratings.

-I then tried making a method called average_rating in the Hike model that averages a hike's reviews with the thought that then when I was iterating over hikes in the hikes index view that I could then call hike.average_rating to display a hike's average rating. The logic in my method was not calculating the averages correctly and I also realized the database was not being updated with the average ratings of these hikes....each hike's average_rating attribute in the database was still 0.

-I knew there had to be a way to have the average rating of hikes, which defaults to 0 in the database, be set/changed through some process. After some time I landed on the idea of trying to use a lifecycle method to update hike instances after review instances are created for them.

-In this thinking, I tried instead having a method in the Review model called update_hikes_average where I was trying to calculate the new average rating of the hike that the current review instance was for. Then in that method, I also tried to update the hike associated with the review by updating the hike's average star rating to this new average rating.

-I then added an after_save callback to the top of my Review model with the following line of code: after_save :update_hikes_average. This line was to make sure that the update_hikes_average method would be called after each time a review was saved for a hike. I then made sure I was displaying each hike's average rating attribute in the index view for hikes.

-One of the things I saw right after this big change was great- the average_rating attribute of hikes was being updated to different numbers for different hikes in the database. I then ran rails s to hop onto my website, go through some hikes and their reviews, and do manual math to see if the average rating of hikes was being calculated correctly. What I noticed was that for some hikes I saw, the average rating of the hike was correct without factoring in decimal places, but for some other hikes the average rating for the hike was considerably off. I felt like I was closer to my overall goal, but not there yet.

-I questioned whether the problem was in my logic in the update_hikes_average method or if it was with how my seed file was arranged. I then got a recommendation to put 3 byebugs in the update_hikes_average method: one at the beginning, middle, and then end of the method. This was to check the values at each step to see if they are what I expect in hopes of narrowing down the problem.

-I created a new hike manually in the console, manually created reviews for it, and did some digging around with the 3 byebug debug recommendation. I found that the average rating calculation of a hike was not even working correctly when I manually created a hike and some reviews for that hike.... no seeding was involved in this case. This made me think that the problem was with the logic in the update_hikes_average method, not in the seeds file.

-Thus, I was certain I needed to change the logic in the update_hikes_average method. Getting the correct calculation of a hike's average rating seemed initially like something that would be rather quick, however it turned out to take me quite a long time as researched and recommended solutions were unfortunately not working correctly time and time again.

-I tried a few other ways to code the update_hikes_average method and I finally landed on code that worked for it! This code correctly calculated the average rating for hikes based on any reviews a hike was given through faker seed data and based on reviews that would be manually created for that hike in the future:
At the top of the Review model I had after_save :update_hikes_average and for the updates_hikes_average method I used this

def update_hikes_average
new_avg = ( { |review| review.star_rating.to_f } /
self.hike.update(avg_star_rating: new_avg.to_f)

-Basically, above I am setting the variable new_avg to the average rating of reviews for self's hike- self being the current review instance and self's hike being the hike that review was created for. Then, I am updating that hike's avg_star_rating attribute with what is set in the new_avg variable.

-Once this is done I made sure I was still displaying the average_star_rating attribute for hikes in the index view for hikes, so that the working average_star_rating would be displaying for hikes.

-With that working, it was simple to make it so that deleting a review of yours would also update that hike's average rating accordingly and correctly. All I did to implement this functionality was add an after_destroy callback at the top of my Review model as well, like so:

after_destroy :update_hikes_average

-I also noticed that while the average_star_rating for hikes was now working, the average star ratings were displaying as integers despite efforts to change that earlier. I instead wanted the ratings to display as floats such as 4.2, 4.5, etc.

-I did more thinking on this and troubleshooting with pry in the update_hikes_average method and that led me to an idea that popped into my head: I had the average_star_rating attribute set to an integer datatype since the beginning- why don't I change my Hikes migration file to change that attribute to a float datatype.

-That minor change, along with setting a precision value of 3 and a scale value of 2, worked to display the average rating for hikes the way I desired! This working line of code in the Hikes migration file is shown below:

t.float :avg_star_rating, default: 0, precision: 3, scale: 2

What you put for the precision value represents the total number of digits in the number, and what you put for scale represents the total number of digits following the decimal point.

At last, I had all of this working correctly and how I wanted it to! A glimpse of the result is shown below:

Top comments (0)