key: cord-0061492-8nppdirq authors: Notodikromo, Adam title: Building the Timeline date: 2020-10-25 journal: Learn Rails 6 DOI: 10.1007/978-1-4842-6026-5_6 sha: 4c2ed6cb987bb3e5b9000805849065e24ba59502 doc_id: 61492 cord_uid: 8nppdirq Timelines are newish inventions. According to the dictionary, a timeline is “a collection of online posts or updates associated with a specific social media account.” And that is what we will build in this chapter. See Figure 6-1. The timeline is a fundamental part of any social media app. There, users can see anyone's updates (an activity known as stalking), reply to any of them (commenting), or post an update themselves (posting). In addition, we will skin the timeline dark because the dark mode is currently the trend. No one uses social media that doesn't follow the trend, right? While building our timeline, you will also learn how to test our views à la carte (that's a cool way no one used to say unit testing). You will also learn how to do request testing, where we test all the way down from the router to rendering the view. Indeed, testing is something the previous chapter skipped. But don't worry, being the good software engineers that we are, we will consider testing a first-class citizen. So, this chapter will cover all the necessary testing techniques commonly employed by software engineers. First, let's keep in mind that there are two types of timeline we will build. The first one is the personal timeline, which reflects updates belonging solely to a user. The second one is the mass timeline, which contains all the updates made by both the signed-in user and the accounts they follow. authenticate :user do resources :timelines, only: [:index, : show] end end With that, we are done preparing the controller and the router. If we look back at our home page, we can see that a navigation bar is attached to the top of the page. However, it is in the template instead of in the layout. If we are to use the same application layout for the timeline, a navbar needs to be manually rendered on both the index and show pages, which doesn't feel so DRY. Our timeline will also use a dark mode skin, which requires a different design altogether. Using the application layout, how can we alter the coloring? Using yield and content_for to specify additional classes on the body tag feels quite indecorous, doesn't it? See Figure 6 -2. Chapter 6 Building the timeline Instead, let's create a new template. Let's call it member, which is used only for rendering pages mainly for logged-in users-which is why it's named member. We could name the layout timeline, but what if in the future we wanted to use it to render any nontimeline page such as a settings page to use the same look and feel? It's essential to name a layout as inclusively as possible. Note there are only two hard things in computer science: cache invalidation and naming things. Let's start making the layout. First, considering that all of our layouts currently have repeating head tags, let's DRY them out. Let's create the layouts/_header.html.erb partial and add the code shown in Listing 6-3. ' %> <%= evil_icons_sprite %> Then, let's replace entirely any head block with the code to render the header partial. Let's start with the application layout, as shown in Listing 6-4. After that, let's do the same for the session layout, as shown in Listing 6-5. Listing 6-5. DRY-ing the Head Tag in the session Layout <%= yield :page_title %> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_pack_tag 'application' %> <%= render "layouts/header" %> ... Now, let's create a navbar partial at layouts/member/_navbar.html.erb, and add the code shown in Listing 6-6. Listing 6-6. Code for the Navigation Bar In the previous code, we use timeline_path(current_user) to generate the link to a user's timeline. Typically, the show action uses the id of a resource. So, it spells out a path such as /timelines/1 or /timelines/2. Notice the use of an ID, such as 1 and 2, which looks unfriendly and antisocial to our friendly, sociable users. So that the path gets spelled with a username, rather than an ID, let's add the code shown in Listing 6-7 to the User model. The active class gives an underline effect on a specific icon on the navbar, but only when the user is at a certain page. For instance, if the user is on the mass timeline page, the first icon is underlined, as shown in Figure 6 -3. To do this, we apply the active class conditionally by using helper methods such as on_bunch?, which we should define in TimelinesHelper as shown in Listing 6-10. Let The controller_path method returns the snake-cased full name of the rendering controller without the word controller. The action_name returns the name of the rendering action. For the timeline icon, we use ei-timeline, which is not shipped with the Evil Icons, so we need to define it ourselves. Let's add the code shown in Listing 6-11 in the app/views/layouts/_header.html.erb partial for the ei-timeline icon. Let's add it near the end of the file. Listing 6-11. SVG Code for ei-timeline-icon ... Last but not least is the styling. Before doing that, let's extend Tailwind by adding the highlighted code in Listing 6-12 to css/tailwind.js. Listing 6-12. Code to Extend Tailwind.js colors: { main: { default: "#60b0e2", "200": "#6cc1ff", "500": "#41a1dd", "800": "#04609a", }, dark: { "400": "#8a8a8a", "600": "#343538", "700": "#2a2b2d", "800": "#18191a" } }, spacing: { '14': '3.5rem ', '72': '18rem', '84': '21rem', '96': '24rem', '168': '42rem', '192': '48rem', shows the stylesheet for the navbar and should be saved as app/javascript/css/member/nav.css. Listing 6-13. Stylesheet for the Navbar .body-member nav.topnav { @apply flex flex-wrap items-center justify-between; @apply pl-6 pr-6 pt-1 pb-1; @apply bg-dark-700; } Listing 6-14. Code for layouts/member.html.erb <%= render "layouts/header" %>
<%= render "layouts/member/navbar" %> <%= render "layouts/alert" %>
<%= yield %>
Listing 6-15 shows the stylesheet code for the layout and should be saved as app/ javascript/css/member.css. Listing 6-15. Code for the Member Layout @import "member/nav"; .body-member { @apply bg-gray-100 font-sans; } .body-member .container { @apply mx-auto; } .body-member.dark { @apply bg-dark-800; @apply text-gray-100; } Then, let's import the member stylesheet into css/application.css and we are done. See Listing 6-16. Listing 6-16. Importing the Member Stylesheet into the Main Application Stylesheet ... @import "session"; @import "member"; After that, let's open app/controllers/application_controller.rb, and then let's change the layout_by_resource method to use the member layout when necessary, as shown here: class ApplicationController < ActionController::Base ... private def member_controller? return false if controller_path == "home" true end def layout_by_resource case when devise_controller? then "session" when member_controller? then "member" else "application" end end def layout_by_resource devise_controller? ? "session" : "application" end We have defined a lot of new styles and created a new layout that we will use as our foundation on which to build Tandibi. The plan is to use @posts to hold all the posts to be displayed in the timeline. To make things DRY, we create a line partial to render each post. As you will soon see, the line partial is the crème de la crème of the whole timeline architecture. Let's start with TimelinesController. Open app/controllers/timelines_ controller.rb and then let's redefine the index action as shown in Listing 6-17. As you can see in the previous code, we call not_reply on the Post model. We should define not_reply soon as a scope at the Post model. But first, let's understand why we need to do this. We know that a user's timeline consists of a lot of posted messages. Each of those pieces may have any number of replies. Yet, it might be better to display those replies if the user expands, or clicks, the post where those replies were made. This is what not_ reply will do, which will filter out posts that are replies to another post. Listing 6-21. The all Partial
<% @posts.each do |post| %>
<%= render "timelines/line", post: post %>
<% end %>
Let's define the missing methods that the line partial uses-first, the humanized_ activity_type, which we should define in TimelinesHelper. This method will allow us to display more human-friendly text when the app encounter a Sight activity. Let's remember that a user can post either a status or an activity such as a checkin or a checkout. Without using humanized_activity_type, the user interface will not be so user-friendly, since the activity type is not meant to be displayed to humans. See Listing 6-22. Listing 6-26. Importing Member/Timeline into css/member.css @import "member/nav"; @import "member/timeline"; ... All is done! If we visit http://localhost:3000/timelines, we can see the page as shown in Figure 6 -4. Please ensure to log in before visiting the page. This has one catch! If a user is signed in and they visit the root URL http:// localhost:3000/, our app renders the home page for the user. It would be nicer if a signed-in user is redirected to the timeline page directly. To do that, let's add the code shown in Listing 6-27 to HomeController. Rails engineers usually reach for the decorator pattern to help make a model slimmer. However, making a model slimmer is usually just a side effect of applying the decorator pattern. The pattern is primarily here to enhance an object's capability when extending the class is either not possible or not desired, which results in a better object-oriented design overall. Let's get into practice. First, let's add the active_decorator gem into the gemfile, and then let's run bundle install. See Listing 6-28. Listing 6-28. Adding active_decorator into the Gemfile gem 'premailer-rails', '~> 1.11' gem 'active_decorator', '~> 1.3' After that, let's run the command in Listing 6-29 to generate a decorator for the User model. Even if our User model lacked the name method, our views would still work just fine when calling name on any User instances. That is because we have defined them in the decorator module, which active_decorator uses to decorate any User instances on the view. Using this pattern benefits us in two ways. First, it helps us make our models focus only on the basic building blocks. For example, they don't need to know view-related logic. Second, it precludes us from defining that view logic as helpers. As we know, helpers are globally namespaced. As such, those functions should be extremely reusable instead of making them tied to a particular data type. Isn't it uncanny if we are working with an object-oriented language but we have procedures highly tied to a specific class? Based on the second argument, let's remove humanized_activity_type from TimelinesHelper and define a similar function within a newly created app/decorators/ sight_decorator.rb file. See Listing 6-31. describe "#name" do it "returns a full name" do expect(user.name).to eq "#{user.first_name} #{user.last_name}" end end describe "#profile_picture_url" do it "returns a gravatar URL" do expect (user.profile_picture_url) .to start_with("https://www.gravatar.com") end end end One thing that a decorator function shouldn't do is to change/modify the underlying data of the object it decorates. It's a "decorator" after all. It's also possible to unit test a view. Just like any other unit testing, this simple testing procedure ignores architectural interactions outside the unit in question. In this section, let's see how we can unit test our line partial. In addition, you'll learn how to do simple debugging by using the pry-byebug gem. First, let's add pry-byebug into the development and test groups. See Listing 6-34. It's the first time we are using a trait. We can use a trait to bundle field modifications that are applied only if the trait is specified when the record is constructed, for example, such as in Listing 6-36. Note it's possible to define and apply as many traits as we want. After that, let's create spec/views/timelines/_line.html.erb_spec.rb and add the code shown in Listing 6-37. Indeed, there are many places where we can drop in binding.pry, such as within a controller or a model. Anywhere the interpreter sees binding.pry, the execution flow is halted so that we can do some debugging or finding out. Thanks to our highly reusable components architecture, rendering the personal timeline simply means rendering the all partial in the show, with @posts set to a user's posts. First, let's change timelines/show.html.erb, as shown in Listing 6-47. Then, let's define the written_by scope in the Post model as shown in Listing 6-49. We will use this to fetch all posts written by a specific user. Notice that we look up the user record and join off a primary key, instead of joining and scanning the username, which is never going to be very performant, especially if there is no index defined for the username field. The code in Listing 6-50 is a pattern that should be avoided. That's that! Now we have a fully functioning personal timeline. For example, if we click a username, let's say Sam's, we will then be directed to Sam's personal timeline page. See Figure 6 -5. Let's allow our users to post a status or write some replies. We will also learn a new design pattern known as service object, and let's also learn how to do request testing. First, let's prepare the user interface. Let's create a new PostsController by running the command shown in Listing 6-51. We pass in the no-test-framework option because we are not going to do any controller testing, but instead, we will do some request testing, which is more comprehensive in scope than just controller testing. Since Rails 5, the official recommendation of the Rails team and the RSpec core team is to write request specs instead of controller specs, since the speed of running such tests has improved. First, let's define the routing for the controller, as shown in Listing 6-52. Since Post is polymorphic, we don't know what it represents in advance. Still, it is much simpler for the form to work with the polymorphic Post model, rather than with the concrete Status or Sight. As such, we need to store the data both on the Post model, as well as one of the concrete model-with both of the data related to one another. We may arguably use a pattern known as a form object in this case. But, the form is currently so simple that doing so feels like over-engineering. Note a form object is another pattern aimed to match the form of the form, no pun intended. While we can lump this data together into the User model so that we can render the form, it makes the User model take on responsibilities it doesn't have to. not only that, but a lot of logic must also be lumped together on the controller for handling the record creation for each class of data, as shown here: it looks chaotic over there. to avoid that, we can use the form object pattern, which knows all the data it needs and how to create, update, or validate it as used in the form. Let's explore this a little bit. We know that, in practice, a software engineering problem can have many solutions, each with its pros and cons. If we are to use the form object now, that means we better replicate the validation logic both in the model and in the form object. Can we afford to do that now? How much repetitive code will that produce? Will it make it easier to maintain in the long run? Is it safe to move the validation responsibilities entirely to the form object, or can we force ourselves to always work with the form object? Is it worth it now? Those questions are challenging to answer without knowing the context; they ought to be evaluated on a case-by-case basis. But, if we are not sure, solving a problem using techniques just for some abstract benefits is usually premature. For our case, let's keep it simple and agile. Let's not introduce significant boilerplate code. But also, let's have an open-minded attitude toward the form object pattern because sometimes it is a gift to the world. Note a time when the form object pattern shines is when the form has become too involved that it's unclear in which model we should define the transient attributes. imagine a checkout form where there can easily be four different models taking part: Item, Order, OrderItem, and Coupon. in this case, having a TransactionForm makes things simple and agile. Although we keep the status_text on the model, we will separate the logic to handle data insertion into a service object, which we will create later. For now, as far as the UI is concerned, we just need to add some styling. Let's create a css/member/poster.css file with the content shown in Listing 6-57. Listing 6-58. Importing member/poster into member.css @import "member/nav"; @import "member/timeline"; @import "member/poster"; Let's add a top margin to create some space between the form and the timeline. To do that, add the code shown in Listing 6-59 to timeline.css. See Figure 6- We know that we will receive two fields from the form: postable_type and status_text. So, let's permit those fields by defining the callable sanitizer function shown in Listing 6-60 to the PostsController. The create action simply delegates a Post to a service object called Post::Creator that we will create soon. If the creation is successful, we redirect the user to the page. Otherwise, we let the user know that there's an issue. That is a kind of commonly seen pattern in a create or update action. Let's now create the service. First, let's create an ApplicationService class inside app/services/application_service.rb, and yes, the services folder within the app folder needs to be created as well. See Listing 6-62. Listing 6-62. Code for the ApplicationService class ApplicationService def self.call(*args) new(*args).call end end Rails uses Spring in the development environment to keep our app running on the background so that we can restart the server or spawn a rails console command faster. However, since it's caching some information about our Rails project, at some point it fails to detect a newly added folder (and the files it contains). For instance, if we now execute rails console and try to access ApplicationService, an error will be raised. Note Spring is currently not supported on Windows, so Windows users may not encounter this issue. What we need to do is to stop Spring from running and run rails c to start a Rails console to boot up Spring again. Ensure that the service class can be called upon. Now, the constant missing issue should have been gone away. See Listing 6-63. Listing 6-63. ApplicationService Is Now Accessible irb(main):001:0> ApplicationService => ApplicationService Having the ApplicationService at hand, we are now ready to create the service object. But what exactly is a service object? A service object is a class to represent a specific, actionable work. It mostly reflects what a user can do to the application. Inspecting the services folder must give us a glimpse of what the application does, which is not always apparent by looking at controllers, models, or somewhere else. A service object has several conventions, which although not enforced by Rails, are usually adopted by the community. Those conventions are as follows: • Service objects should go under the app/services directory. They can be subcategorized further per model. For instance, Post::Creator is a service object for the Post model. • Services should end with an actor, for example, Post::Creator, Newsletter::Sender, Scenario::Exporter. • A service object should have only one public method. The method might be named call to imitate Proc#call. Identifying it with another verb may make it inconsistent between services, for example, Transaction::Approver#approve versus Newsletter::Sender#send. Another possible naming includes execute and perform. The Post::Creator service object is responsible for creating a new post. The file should be saved as app/services/post/creator.rb with the code shown in Listing 6-64. Note You can create a folder at your discretion, and rSpec can just pick it up. Now let's try posting a new status by going to a timeline page. If we have a Rails server previously running, it's advisable to stop and relaunch the server, since the program might use an older copy of our app as cached by Spring before we added the service folder. See Figure 6 -7. So, it's October 2020, and the pandemic 1 is making life a bit harder for almost everyone. Adam is no exception. He misses his friends. So, Adam wrote a status update for fun. Upon clicking the Say button, the new status will be posted, and Adam is taken back to the page where he wrote the status. Everything works as expected! See Figure 6 -8. Request testing is when integrated units from the router to the view are tested as a whole unit. This kind of testing is also known as integration testing in other languages or frameworks. Let's create our first request spec for posting a status. Let's create a file named posts_ controller_spec.rb inside the spec/requests folder, which we also need to create. After that, add the code shown in Listing 6-66. (1) expect(response).to redirect_to(timelines_path) follow_redirect! expect(response.body).to include "Howdy!" end end end end That's our request spec. Simple, right? The test uses the sign_in method provided by Devise. To make the sign_in method available in our spec files, let's create the spec/support/devise.rb file and then add the code shown in Listing 6-67. Listing 6-67. Including Devise::Test::IntegrationHelpers for Request Specs RSpec.configure do |config| config.include Devise::Test::IntegrationHelpers, type: :request end Notice that our test case makes a POST request to the /posts path. Although the path was written as a string, we can also use the equivalent posts_path function instead. In addition to sending a POST request, we can send a GET request using get. Similarly, there are also delete, patch, put, as well as head functions for sending DELETE, PATCH, PUT, and HEAD requests, respectively. We can also attach a request body to any of them by using params. And it's also possible to customize the header by specifying the values and their keys through header. The code shown in Listing 6-68 demonstrates an example. Although a request spec falls under integration testing, it's not an end-to-end test where a real browser is involved. As such, we cannot interact with elements on the page. For instance, it is not possible to fill in a form and then click a specific button to test what is going to happen, as seen by the user's browser. To do that, we need to involve a real browser, which we will talk about it in the next chapter. That being said, request testing is definitely much faster than an end-to-end test. Albeit, it's a bit slower than a unit test for the obvious reason that it runs all the way through the stack. Because of its nature, request testing is usually the go-to choice for testing an application's endpoints. It would be more fun if users could reply with an update, so let's make this feature! See Figure 6 -9. To send a reply, users first click the bubble icon. A pop-up will then appear, inside which they type in their reply. Upon clicking the Say button, their reply will be posted. This is a functional requirement. There are two types of requirements: functional and non-functional requirements. A functional requirement describes a function that a system component must be able to perform, while the nonfunctional requirements elaborate on the characteristic of the system behind the scene. A nonfunctional requirement usually deals with accessibility, scalability, availability, maintainability, security, testability, and documentation. For an example of nonfunctional requirements, let's say we want the pop-up dialog to be highly reusable. See Listing 6-69. Listing 6-69. An Example Snippet to Make and Call a Pop-up <%= render "layouts/inside_modal", title: "Send a reply", modal_class: "mypopup" do %> <%= render "posts/form", thread: post %> <% end %> Open mypopup popup In the previous snippet, we just need to wrap the content of a pop-up within a block that renders the layouts/inside_modal partial. Please note that we don't have to add any of the previous code to any file just yet. We want rendering a pop-up, its trigger, and its content to be no-brainer. Let's work on it! Let's begin with preparing the inside_modal partial within the layouts folder. The file should be app/views/layouts/_inside_modal.html.erb, and then let's add the code shown in Listing 6-70. Listing 6-70. Code for the inside_modal Partial