DEV Community

Vikram Chatterjee
Vikram Chatterjee

Posted on

Phase 3 project - PushWith

For my phase 3 project I designed an application called PushWith - The definitive application for gyms that want to help members connect through workout groups and post their workouts online for others to see!

Routes: The root route to sessions@welcome contains the landing page from which users can go to the signup, login, and Omniauth routes. Get and post routes to signup and login have been created and the get routes have been aliased as #signup and #login so that the users know what they are doing at those pages (signup, for instance, is more intuitive to a user than /users/new). A delete route to 'logout' has been implemented so that users can logout without having to clear their cookies. A match route to the google omniauth route has been created so that users may log in or sign up with Google. A match route to '*path' has been created so that a user entering an invalid route within the domain is safely routed back to the landing page without having to see a spooky Rails error. Resources for workout groups and nested resources for workouts have been created so that workout groups and Workouts have full CRUD functionality and so that routes for workouts are properly embedded within the routes for the workout groups that they belong to. New, Create, and Destroy resources for UserGroups have been created so that instances of a join table between a User and a Workout Group can be created when a User wants to create or join a Workout Group.

Login, Signup, and Omniauth: When users first navigate to the website, they are able to create accounts by clicking sign up on the landing page and entering a username and password. The user model validates the presence of a username and uses BCrypt's #has-secure-password method to validate the presence of a password. If a user fails to fill out either form, they will be met with a flash error that will tell the user that the username and/or password cannot be blank. When the user creates an account successfully, a session will be created and the user id will be saved, which logs in the user and allows them to interact with the application. When the required fields are filled out and submitted, the User object will be saved to the database, allowing a user to login with their credentials from the landing page at a later time. Before the user is saved into the database, the password is salted and converted to a hash (salted meaning that a short string is added to the end of the password) by the BCrypt gem and saved as a password digest in order to ensure that the raw password never enters the database, in order to ensure maximum security.

When a user logs into the application, the sessions controller #create method finds the user by username and uses BCrypt to authenticate the user by adding the salt (a short string) to the raw password and uses its algorithm to translate the raw password with salt into the hashed password digest, and compares the hashed value to the password digest value in the database. If the values match, the user will be logged in and redirected to the workout groups index page, where they will be able to see the list of workout groups which have already been created.

On this platform, users are given the option to login with Google instead of entering a username and password on the signup page. On the landing page there is a link to "Log in with Google", which uses Omniauth to create, save, and access user accounts by entering their Google credentials via a Google secured path. This offers users added security, because even if the application is hacked, there will not even be a password digest on the server for a hacker to try and decrypt. Omniauth was implemented by going into Google's developer console and creating an OAuth 2.0 Client ID, and using the credentials generated to initialize an instance of Omniauth in the omniauth.rb file in /config/initializers. In order to get Omniauth to work, 4 gems were added to the gemfile: 'omniauth', 'dotenv-rails', 'omniauth-google-oauth2', and 'omniauth-rails_csrf_protection'. The provider google_oauth2 was set in the omniauth initializer file, and the Google Client key and Client secret were securely stored in a local .env file which is not pushed to Github, so that the credentials are only accessible to the machine that is running the server.

When a user logs in with Google, the #google method in the Sessions controller finds or creates a user by making a request to Google using the Client ID and Client Key stored in the .env file and gets the name of the user from their Google profile. Then, a 10 character hexadecimal password is generated in order to generate the session and login the user. This means that a user that signs up with Google must always login using Google, as the SecureRandom password is never accessible to the user or the database. If the user is saved to the database they will be logged in and their username will be their name according to Google. Otherwise, they will be redirected to the landing page or 'root path'.

Models, Validations and Associations: The PushWith application has a total of 4 models that inherit from ApplicationRecord: The models are for User, Workout Group, UserGroup, and Workout. The User model has many user_groups, and has many workout_groups through user_groups. The User model validates the presence and uniqueness of Username, and uses the has_secure_password method to validate the presence of a password and to encrypt the password as was discussed in the previous section.

The WorkoutGroup model has many UserGroups and deletes all UserGroups it is associated with when a given WorkoutGroup is deleted. The WorkoutGroup model has many Users through UserGroups. It also has many Workouts and deletes all associated workouts when an instance of the class is deleted. The WorkoutGroup model validates that its instances have a name. The WorkoutGroup model is special in that it accepts nested attributes for UserGroups and optional nested attributes for Workouts. This comes in useful when building a new WorkoutGroup. As we will discuss in an upcoming section, the New form for a workout group will generate a UserGroup and new Workouts using nested forms. There are two methods in the WorkoutGroup class that utilize the ActiveRecord query method #where. The first of these methods uses a search bar on the WorkoutGroups index page to search for Workout Groups whose name contains the string that is typed out in the search bar. The second method, #find_current_user_group, helps to find the UserGroup that belongs to a given WorkoutGroup and is associated with the user that is currently logged in.

The UserGroup model serves as a join table between Users and WorkoutGroups, and as such it belongs to a workout_group and belongs to a user. The UserGroup class validates the uniqueness of a workout_group_id within the scope of user_id, which is to say that it prevents a given User from entering a WorkoutGroup that he or she has already joined. The UserGroup class has a string attribute of mantra, which must be present in order for a given user_group to be saved. The mantra of the UserGroup is displayed on the WorkoutGroup's show page next to the username of the user that the UserGroup is associated with.

The Workout model is a simple model whose instances belong to a WorkoutGroup. The Workout validates the presence of all of its attributes: a string of 'name', and integer values for 'sets' and 'reps'.
The above models make for an application in which each user can join many workout groups, and each workout group may contain many users. Each time a user enters a workout group, they must submit their mantra to join, and they may have a different mantra for each workout group that they join. When a workout group is created or at any time while the workout group exists, workouts can be created for it and are listed in a nested route that displays the workouts for a given workout group.

Controllers: As with any application, the controllers for the PushWith application are essential to navigating through the application, and in particular for Creating, Reading, Updating, and Deleting objects contained within the application. All controllers in the application inherit from ApplicationController and may use any of the methods within it.

The ApplicationController contains 5 methods, and two of these methods are helper methods, #user_signed_in? and #current_user, that may be accessed by views in addition to being accessible by the other controllers. The #user_signed_in? method is used to confirm that a session currently exists for a user; it checks to make sure that a user is logged in. The #current_user method actually employs the #user_signed_in? method and goes further to check to see which user is signed in, by finding that user by their user id. The login_user method uses the instance variable @user to get the id of a user that is logging in or signing up, and creates a session for that user in order to log the user in. The #redirect_if_not_logged_in method checks to see if a user is signed in, and if there is no user signed in, it is employed in various cases in the other controllers in order to prevent a user that is not logged in from performing certain actions, and to notify the user that they must sign in in order to perform that action. Finally, there is a fallback method that redirects a user back to the landing page 'root_path' if they attempt to use the URL bar to navigate to a route that does not exist within the application.

The first controller that inherits from the ApplicationController is the SessionsController, which houses the landing page 'root_path' at sessions#welcome. The sessions controller takes care of logging in users; besides the landing page, the sessions controller only handles one view in the application: the login page. The #new action in SessionsController routes a user to this page, which asks for the username and password of an existing user. The #create action in SessionsController handles post requests that are made in this view, and creates an instance variable @user that is set equal to the instance of an existing user whose username matches the username that is typed in the 'username' field. As was discussed in a previous section, the #create method only proceeds if a user with a matching username is found. Upon finding a matching username, the #create method uses BCrypt to authenticate the user in the manner that was previously discussed. If a user is authenticated, they will be logged in and redirected to the workout_groups_path. Otherwise, the flash error "Invalid Username or Password" will show and the new form will be re-rendered to the user. The #destroy action in SessionsController is attached to the "Log Out" button in the nav bar, and will clear the session that saved the logged in status of a given user, and it will redirect that user to the root path. The #google method allows Omniauth to find or create a user by assigning the username attribute to the 'name' extracted from google, and creates a SecureRandom 10-digit hex password for authentication purposes. The #google method employs the private method, #auth, which goes into the .env file and uses the Google Client ID and Secret Key to extract information from Google, as was discussed in a previous section.

While the SessionsController takes care of logging in users and Omniauth actions, the UsersController takes care of creating new users. The #new action in UsersController navigates the user to the /users/new or signup route, and creates the instance variable @user which will be reused in the #create method. The #create method takes the instance variable #user, and takes the argument of user_params provided by a private method, in order to make sure that a username and password, and only a username and password, have been entered and save it to the database. If valid information is input into @user, the #create method will validate that the User model did save the user and it will use the login_user method to create a session for that user, and then redirect the user to the workout_groups_path. Otherwise, flash errors will pop up saying that the username and/or password cannot be blank, and the new page will be rendered again.

The UserGroups Controller is a simple controller that is only directly associated with one view, which allows a user to join an already existing workout group. The private method, #user_group_params, permits only the params of mantra, workout_group_id, and user_id to be entered into the params for @user_group. The #new action renders the Join a Workout page and sets the instance variable @user_group to a new UserGroup object. The #create method takes care of post requests to the user_groups_path and employs the user_group_params method to make sure that only valid attributes are entered into the @user_group instance variable. It then checks with the UserGroups model to make sure that all of the required attributes have been submitted, and if the UserGroup is saved into the database, the user will be redirected to the workout_groups_path. Otherwise a flash error will come up telling the user which required attributes have not been entered, and re-render the Join a Workout Group form. The #destroy action in User Groups Controller is linked to the Workout Groups show view, which contains a link to leave a given workout group which only renders if the user is currently in that workout group. The Workout Group controller employs the #find_current_user_group method in the WorkoutGroup model to find the user group that is associated with both the workout group and the current user in order to display the link. When that link is clicked, the #destroy action and the #find_user_group method in the UserGroups controller, select and destroy the current user's UserGroup, which removes the given user and their mantra from the workout groups roster.

The Workout Groups Controller handles the New Workout Group route and the Workout Group Show route, where it uses the WorkoutGroup model's find_current_user_group method to help to render the leave workout group link for logged in users that are in a given workout group. It also handles as the Workout Group Index route, and its #create method handles any post requests to the New Workout Group route. Furthermore, the Workout Groups Controller renders the edit page for a Workout Group which allows users in the Workout Group to change the Workout Group's name, handles Patch requests to a workout group, and handles requests to delete a workout group, by finding the workout group by id and destroying all associated user groups and the workout group, and leaving a flash notice that mentions the name of the workout group and saying that it was deleted. The #index action contains a conditional to check for a query entered in the search bar, and uses the WorkoutGroup model's search method to find only the workout groups whose name contains the query entered in the search bar. The #show action uses #find_workout_group to find a given workout group by id, and contains instance variables for @user_groups and @user_group to respectively display the user groups that belong to the workout group and, optionally, the link to leave the workout group if the current user is in that workout group. The #new action instantiates a @workout_group variable and three instances of workouts for the user to optionally add three workouts to a workout group when the workout group is created. The #workout_group_params method is used in the #create action when a post request is made to make sure that only a name, a user group id and mantra, and workouts with their names, sets and reps are entered into the workout group params. If the workout group is saved after the new workout group form is filled, the user will be redirected to the workout groups path. Otherwise, the new form will be re-rendered to the user. The Workout Groups Controller also contains a redirect method that defends the workout group from being edited or deleted by a user that is not in that workout group, as well as a redirect method that prevents the user from navigating to a workout group that does not exist.

The Workouts Controller handles the #new and #create action for workouts to be created in a workout group. The #new action creates a @workout instance variable and associates the workout with the workout group that it is being created in. The #create action uses #workout params to make sure that only valid attributes of a workout, :name, :sets, :reps, and :workout_group_id are being passed into a new Workout object before it is saved. If a workout is saved, the user will be redirected to the workout_group_workouts_path (the list of workouts in a workout group), otherwise, the new form will be re-rendered to the user. The #show action handles the show page for a workout, which displays its name, sets, and reps. The #edit action renders the edit page, which allows the user to change any of these attributes of a workout. The update action takes care of post requests and verifies that only permissible #workout_params are being entered, like the #create action does. The #index action handles the index page; it checks to see that there is an associated workout group and displays all of the workouts associated with that group. The #destroy action selects and destroys a given workout by ID, and renders a flash notice saying that the workout with the given name was deleted, and redirects them to the new list of workouts. In the Workouts controller, there are redirect methods that prevent the user from navigating to a workout that doesn't exist, a workout that does not have an associated workout group, or a workout that belongs to a group other than the workout group with the associated workout group id. There is also a method that ensures that a user that is not in a given workout group cannot create, edit, or delete workouts in that workout group.

In the User_Groups controller, the WorkoutGroups Controller and the Workouts Controller, the before_action method was used in order to target instances of objects as well as to facilitate redirections if an invalid path was selected for a given controller. The before_action method calls a method and runs the logic in that method before the action is run. One case in which a redirect method was called in the action instead of using a before is in the Workouts Controller's new method, because a @workout instance variable needed to be generated before the 'redirect_if_not_in_group' method could be run.

Forms, Nested Forms, Layouts and Partials:
The Application.html.erb file is an important view that renders the materialize CDN as well as javascript methods that allow for methods such as delete to be called in forms. It also instantiates jquery, which must be used to allow materialize initializers to run. The nav partial as well as the errors partial for flash errors are rendered in this view above the body of the website. The errors partial detects and iterates over flash errors, displaying them at the top of the page. The _formerrors partial is called upon in forms like the Workout Group and Workout _form partials, and facilitates the display of errors within the forms that display when invalid data is entered into the forms above the form which is re-rendered to the user. The nav partial calls upon a method in the application helper to render nav links, and renders different links depending on whether the user is signed in or not.

The UserGroups new form creates a post request to the user_groups_path and contains a hidden field for user_id which is set to the id of the current user. The UserGroups form contains a collection select form that allows the user to access a drop-down menu of all of the workout groups that exist, and select the workout group which they would like to join. At the bottom of the view, there is an initializer that allows Materialize to render this drop-down menu. Below the drop-down menu, a text-field for mantra provides a field for the user to enter their mantra when joining a workout group. The Workout Groups _form partial provides instructions for what to render in the edit and new pages for workout groups. It contains a label and a text field for the name of the workout group. The new page for workout groups renders the layouts/formerrors partial and the _form partial within the form for the workout group. These partials provide errors when they are rendered, and the label and textfield for the workout group's name, respectively. The new form also renders the fields_user partial with locals of f: f and workout_group: @workout_group so that these variables are accessible to the partial. The fields_user partial uses the formbuilder variable, f, to nest itself within the overall form, and the workout_group variable is used to create an associated build between the workout group and the user group. The _fields_workout partial takes locals for 'f' and for 'workout_group' to nest itself within the overall form and to create an associated build between workout_group and workouts. When an associated build is performed, the object that is being created automatically assigns itself to the workout group by adding the value of workout_group_id to the key designated 'workout_group_id' in their respective instances. In this case, since UserGroups and Workouts both belong to Workout Group, their instances contain the foreign key, workout_group_id, which designates which workout group they belong to. The edit form for Workout group contains the _formerrors partial and the _form partial in order to display errors and to render the text field for the name of the workout group. The local 'item:' is assigned to @workout group in the _formerrors partial so that the errors for @workout group are displayed above the form if invalid data is submitted. When the _form partial is rendered, f is passed as a local so that the partial knows about the form builder, and 'workout_group:' is assigned to @workout_group so that the 'name' attribute is assigned to the instance of the WorkoutGroup that is being created.

The index form for Workout Groups contains a form tag with a GET method that routes to the workout_groups_path so that the query in the text field tag can be used by the Workout Groups controller to filter for workout groups that match the query that is passed into the search bar. Below that, @workout_groups (which the controller designates as being all workout groups, or just the workout groups matching the search query), are iterated over and links are provided that display the name of the workout group and route to the workout group path of that particular workout group. At the bottom of the index page, a link to create a workout group is displayed that routes to the new_workout_group path, which when clicked will display the new workout group form. The show page for a workout group displays the name of the workout group at the top of the page, and below that, it iterates over the user groups that belong to that workout group, displaying the username of the user that owns each user group, and the mantra of that Usergroup afterwards. Below the list of UserGroups, links to the workouts index page, the edit page for the workout group, and the delete route for the workout group are displayed. As was discussed earlier, a link to leave the workout group is displayed if the current user is in the workout group that is being viewed.

The Workouts' views folder also contains a partial labelled _form that takes the arguments workout_group, and workout. The argument of workout_group is given so that the hidden field in _form for :workout_group_id can be filled with the workout group id of the workout group that the given workout belongs to. The argument of workout is given so that the form knows that the fields within are assigning values for the attributes of the @workout which is initialized in the controller. In the workout _form, the /layouts/formerrors partial is rendered where the local of 'item:' is set as workout, so that the validations provided by the Workout model can display errors when someone forgets to enter the workout name, sets or reps. The new and edit pages of Workout both render the form with locals of workout_group: @workout_group and workout: workout so that the form knows what instance variables it is dealing with. They both contain locals for button_name, but with different values so that a dynamically named button can be rendered on each page.

The index page for Workouts displays the name of the Workout Group at the top and then iterates over the workouts owned by that group, creating a link with the name of that workout that routes to the show page of that workout, where the user can see the sets and reps of the workout to be performed. Two arguments, @workout_group and workout, are passed into workout_group_workout_path, so that the controller knows both the id of the workout group that the workout belongs to and the id of the workout that is being selected. Below this list, there is a link to create a workout for the workout group that routes to 'new_workout_group_workout_path' and takes the user to the new workout form so that they can create a new workout object for the workout group. Below that, there is a link to go back from the workout list to the workout group show page. The show page for workout contains the name of the workout and its sets and reps, as well as links to edit or delete the workout or to go back to the workout group's workout list or the workout group's show page.

Conclusion: Pushwith is a Ruby on Rails application that employs a many-to-many relationship between users and workout groups so that users can have many workout groups and workout groups can have many users. It also employs nested routing so that Workout Groups can create their own workouts, and these workouts are only visible within routes for that particular workout group. The Join table between Users and Workout Groups contains a user submittable attribute of mantra so that each User enters an encouraging message to their team when they create or join a workout group. Each model validates the presence of its attributes so that users cannot submit empty forms or forms that are missing values for the attributes of the objects that are being created. Class level ActiveRecord scope methods are included within the Workout Group model so that users can enter search queries to more easily find a workout group that they are looking for, and so that users that are in a particular workout group are able to see a link to leave the workout group on that workout group's show page. Signup, login, and logout features have been implemented in the application so that users can have their own profiles from which to interact with workouts and workout groups. Users are encouraged to use the Omniauth feature to sign in with Google and therefore gain added convenience and additional security. Flash errors and form errors have also been displayed in the application so that users know what is wrong when they submit a form and the form fails to submit. Overall, PushWith offers a variety of features so that each user can have a fulfilling collaborative experience with others at their gym.

Top comments (0)