How to create Discussion forum

how to create discussion forum in WordPress and how to create a private forum for discussion and how to create a discussion forum using html
Dr.MohitBansal Profile Pic
Published Date:25-10-2017
Your Website URL(Optional)
C H A P T E R 5 ■ ■ ■ Building a Discussion Forum In this chapter, we will look at how we can build a discussion forum for our community site. This will allow our users to discuss various aspects of Ruby and Rails development. We will also use the forum to talk about this book and allow users to discuss the code or sites that they have developed using this book. The premise of the forum is that an administrator user will create a number of forums. Within each of these forums, users can create topics. Each topic then has any number of posts within it about that topic. There are already a number of open source forum implementations on the Web, of which the PHP-based phpBB and PunBB are the most popular. We can easily build similar functionality in Rails very quickly and have it fully integrated into our site. To build this functionality, we will use the Rails generator script to create basic controllers, models, views, and tests for the relevant resources and adapt this scaffolding code to our needs. Then I will show how you can easily develop this scaffolding code to support nested resources. Specifying the Discussion Forum Requirements Before we dive into the code, we should establish the structure and design of the discussion forum. The application will allow for a moderator user to create a number of forums along with a description of the forum. This will allow for forums to be set up for different conversations on different topics. Within each forum, any logged-in user can create a new topic, which will consist of a number of posts. This tells us that we are going to need three models: Forum, Topic, and Post. A forum can have many topics, and a topic, many posts. For these models, we are going to make use of counter caches. A counter cache does exactly what it says—it keeps a cache of a counter. For instance, we’ll display the number of topics in a given forum. While performing a count of the number of topics in a forum is not an especially taxing database query, if the application has to show the number of topics for each forum, an extra database query has to be done for each forum. Implementing a counter cache is trivial in Rails and can save your application making unnecessary database queries. While the Rails community generally advocates optimizing only when necessary, if you know you will need to show this counter, it is worth adding it now. We will use counter caches for the topics-per-forum counter and the posts-per-topic counter. Since the topic counter is on a per-forum basis, including it necessitates an extra field in the forums database table. Keeping with the Rails conventions, this field should be called topics_count. You are free to rename this to something else, but you have to specify the field 117 118 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M name in thebelongs_to statement in the model when you set up the counter cache. However, it is much easier if you stick to conventions and usetopics_count. Defining the Forum Model The Forum model will consist of simply a name and a description. Forums can be created, edited, and deleted only by a moderator or administrator. Since we are using a counter cache to store the number of topics per forum, atopics_count field is added to each row. Table 5-1 shows the complete structure of the Forum model’s database. Table 5-1. The Forum Model Database Structure Field Name Field Type Description id integer The primary key name string The forum name description text Description of the forum created_at datetime The date and time that the forum was created updated_at datetime The date and time that the forum was last edited topics_count integer The topic counter cache Defining the Topic Model The Topic model has the topic name along with theuser_id of the user who created the topic. Any logged-in user can create a new topic, but only moderators can edit or delete topics. Deleting a topic will delete all of the posts within that topic. Since we need a counter cache to store the number of posts per topic, aposts_count field has been added. The Topic model’s database structure is show in Table 5-2. Table 5-2. The Topic Model Database Structure Field Name Field Type Description id integer The primary key forum_id integer The id of the forum that this topic belongs to user_id integer The id of the user who created the topic name string The subject of the topic created_at datetime The date and time that the topic was created updated_at datetime The date and time that the topic was last updated posts_count integer The posts counter cache CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 119 Defining the Post Model Each post has a body, a text field containing the body of the post, and theuser_id of the user who created the post. Any logged-in user can create a post; only moderators can edit or delete. Table 5-3 shows the Post model’s database structure. Table 5-3. The Post Model Database Structure Field Name Field Type Description id integer The primary key topic_id integer The id of the topic that this post belongs to user_id The id of the user who created this post integer body text The body of the post created_at datetime The date and time that the post was created updated_at datetime The date and time that the post was last edited Figure 5-1 shows the relationships among the Forum, Topic, and Post models. Figure 5-1. Entity relationship diagram for the Forum, Topic, and Post models The Moderator Role Since the task of moderating a busy discussion forum is too much for one administrator to handle, we will create a new user role to allow other nonadministrative users to be given the right to edit or remove forum posts. This user role will be called “moderator.” Since we built the role management system in Chapter 3, it will be very simple for the site administrator to assign this role to trusted users who can actively monitor and moderate the forums. The Forum, Topic, and Post Controllers The forum, topic, and post controllers will all be standard REST-style controllers. However, since each topic belongs to a particular forum, and each post belongs to a particular topic, these resources need to be nested. 120 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M The topics resource will be nested beneath a forum resource, assessable via URLs such as /forums/1/topics and/forums/1/topics/2. The posts resource will be nested beneath a topic resource and, in turn, a forum resource. Therefore, the posts resource is assessable via URLs such as/forums/1/topics/2/posts and /forums/1/topics/2/posts/3. Building the Forum In the previous chapters, we used thegenerate script to create skeleton controllers and models and then wrote all the code ourselves. In this chapter, we are going to use thescaffold_resource generator. This automatically generates a controller, a model, and views for a given resource name. The generated code implements the basic CRUD functions of a REST resource. Doing this gives us a basic skeleton of a controller, speeding up our development process. Building the Forum, Topic, and Post Models The forums consist of three models: Forum, Topic, and Post. A forum has many topics, which in turn have many posts. In addition to this, each topic and each post belong to the user who created them. To create these models, we are going to use thescaffold_resource generator to build the scaffolding code for the forum resources and edit the generated code to fit our needs. First, let’s create the forum resource: ruby script/generate scaffold_resource Forum exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/forums exists test/functional/ exists test/unit/ create app/views/forums/index.rhtml create app/views/forums/show.rhtml create app/views/forums/new.rhtml create app/views/forums/edit.rhtml create app/views/layouts/forums.rhtml identical public/stylesheets/scaffold.css create app/models/forum.rb create app/controllers/forums_controller.rb create test/functional/forums_controller_test.rb create app/helpers/forums_helper.rb create test/unit/forum_test.rb create test/fixtures/forums.yml exists db/migrate create db/migrate/008_create_forums.rb route map.resources :forums CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 121 Next, we’ll create the Topic model: ruby script/generate scaffold_resource Topic exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/topics ... create db/migrate/009_create_topics.rb route map.resources :topics and finally, the Post model: ruby script/generate scaffold_resource Post exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/posts ... create db/migrate/010_create_posts.rb route map.resources :posts Model Relationships Before working on the migration scripts, we should work on the model files to make sure that we have the necessary database fields to support the relationships among the models. First, open the model fileforum.rb, and add the relationship statements as shown in Listing 5-1. Listing 5-1. The Forum Model File class Forum ActiveRecord::Base has_many :topics, :dependent = :delete_all has_many :posts, :through = :topics end This model uses a relationship that you might not have encountered before—has_many :through. This specifies an intermediate object that sits between one object and another. In this case, it allows us to access all of the posts in a forum, irrespective of the topic. You will also notice that thehas_many :topics statement includes an extra parameter that states what should happen to dependent objects should this object be deleted. In this case, we want all dependents of this object to be deleted, meaning that if you delete a forum, all the topics in that forum will be deleted. Add the relationships for thetopic.rb model as shown in Listing 5-2. 122 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M Listing 5-2. The Topic Model File class Topic ActiveRecord::Base belongs_to :forum, :counter_cache = true belongs_to :user has_many :posts, :dependent = :delete_all end You will notice that here we are setting up the counter cache described earlier. This model also declares that all dependents should be deleted in the case of this object being deleted, in the same way as the forum deletes the dependent topics if it is deleted. Now, if a forum is deleted, all of the topics and all the posts within those topics will be deleted, keeping the database clean. Finally, add the relationships for thepost.rb model as shown in Listing 5-3. Listing 5-3. The Post Model File class Post ActiveRecord::Base belongs_to :topic, :counter_cache = true belongs_to :user, :counter_cache = true end There should be no surprises here. Again, this model uses a counter cache, this time for the number of posts in a given topic. This means that aposts_count integer field needs to be added to thetopics database table. This also adds a counter cache to the User model. This will allow the application to easily display and cache the number of posts made in the forum by a particular user. Model Validations Since we are already working on the models, we should add some basic validations to them. The Forum model has two attributes that we should validate: name anddescription. The name field must be filled in; having a forum with an empty name is not helpful. We will also set a maximum of 255 characters for this field, enabling us to use a databasestring type rather than atext type. The forum names should be a short title, not a lengthy description. Thedescription field obviously needs to be longer but should still be kept within sensible limits. Also, some sites may decide not to fill in thedescription field; the name may be enough. Therefore, we do not have to test for the presence of this field. To perform these validations, add the following to theforum.rb model file: validates_presence_of :name validates_length_of :name, :maximum = 255 validates_length_of :description, :maximum = 1000 For the Topic model, there is only one field that we need to validate—thename field. Since this is intended to be a simple description of the topic in discussion, it makes sense to keep this reasonably short. We will use a databasestring type, so we should validate that the text entered is 255 characters or less. Also, we want each topic to have a name; having a blank topic name isn’t useful to anyone visiting the site, so we will check that the field is not empty. CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 123 Edit thetopic.rb model file, and add the following validations: validates_presence_of :name validates_length_of :name, :maximum = 255 The Post model also has only one field that is entered by a user and needs to be validated: thebody field. Since this is the actual meat of the discussion, we don’t want to limit the size of the post too much. However, it is a good idea to place some limit on the size, albeit a high one. Again, we don’t want this field to be empty. Add the validations to thepost.rb model file: validates_presence_of :body validates_length_of :body, :maximum = 10000 Migration Scripts We can now build the migration scripts for these models. Edit theself.up method in the 008_create_forums.rb file to simply create the necessary fields as follows: def self.up create_table :forums do t t.column :name, :string t.column :description, :text t.column :created_at, :datetime t.column :updated_at, :datetime t.column :topics_count, :integer, :null = false, :default = 0 end end The009_create_topics.rb migration file adds the necessary files along with adding a data- base index for thisforum_id field. This will speed up the database queries for retrieving the list of topics in a given forum. Edit theself.up method as shown: def self.up create_table :topics do t t.column :forum_id, :integer t.column :user_id, :integer t.column :name, :string t.column :created_at, :datetime t.column :updated_at, :datetime t.column :posts_count, :integer, :null = false, :default = 0 end add_index :topics, :forum_id end Edit theself.up method in the010_create_posts.rb file to add the fields necessary for the posts table. Again, this creates a database index to speed up the database queries. 124 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M def self.up create_table :posts do t t.column :topic_id, :integer t.column :user_id, :integer t.column :body, :text t.column :created_at, :datetime t.column :updated_at, :datetime end add_index :posts, :topic_id end We also want to cache the number of posts by a given user. To do this, theusers table will need to be modified and aposts_count integer field added to it; create a migration for this: ruby script/generate migration AddUserPostsCount exists db/migrate create db/migrate/011_add_user_posts_count.rb Edit the migration file to add the necessary up and down methods to add or remove the column as shown in Listing 5-4. Listing 5-4. Migration to Add a Posts Counter Cache to the Users Model class AddUserPostsCount ActiveRecord::Migration def self.up add_column :users, :posts_count, :integer, :null = false, :default = 0 end def self.down remove_column :users, :posts_count end end Before you run the migrations, we should create the new role calledModerator, and add this role to the roles of the Admin user. Of course, you can add the role to any other users that you wish to give forum moderator permissions. Create a new migration script with the following command: ruby script/generate migration AddModeratorRole exists db/migrate create db/migrate/012_add_moderator_role.rb Edit the generator migration script as shown in Listing 5-5. CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 125 Listing 5-5. Migration to Add the Moderator Role class AddModeratorRole ActiveRecord::Migration def self.up moderator_role = Role.create(:name = 'Moderator') admin_user = User.find_by_username('Admin') admin_user.roles moderator_role end def self.down moderator_role = Role.find_by_name('Moderator') admin_user = User.find_by_username('Admin') admin_user.roles.delete(moderator_role) moderator_role.destroy end end As in the previous chapter, this simply creates a new role, finds the existing user with the usernameAdmin, and adds the new role to this user’s list of roles. You should now run these migrations, creating the database tables necessary for the forum: rake db:migrate (in /Users/alan/Projects/rails/railscoders) == CreateForums: migrating ==================================================== create_table(:forums) - 0.0105s == CreateForums: migrated (0.0112s) =========================================== == CreateTopics: migrating ==================================================== create_table(:topics) - 0.4358s add_index(:topics, :forum_id) - 0.2001s == CreateTopics: migrated (0.6372s) =========================================== == CreatePosts: migrating ===================================================== create_table(:posts) - 0.0743s add_index(:posts, :topic_id) - 0.0178s == CreatePosts: migrated (0.0934s) ============================================ == AddUserPostsCount: migrating =============================================== add_column(:users, :posts_count, :integer, :default=0, :null=false) - 0.1475s == AddUserPostsCount: migrated (0.1481s) ====================================== 126 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M == AddModeratorRole: migrating ================================================ == AddModeratorRole: migrated (0.2128s) ======================================= Checking a User’s Roles for Moderator Rights First of all, we should add code to allow us to easily check if a user has moderator access, in the same way as we did for the administrator and editor roles. Open thelib/login_system.rb module, and add thecheck_moderator_role method before the private methods: def check_moderator_role check_role('Moderator') end This allows you to simply addbefore_filter :check_moderator_role for any controllers or actions that should be protected from unauthorized users. Adding the Nested Resource Route Mappings Before we work on the code, we should add the URL mappings to theconfig/routes.rb file. You will notice that the generation script added basic resource mappings for forums, topics, and posts. However, for the forums, we are going to use nested routes: posts belong to a topic, and topics belong to a forum. Remove the following automatically generated resource mappings from the routes file: map.resources :topics map.resources :forums map.resources :posts Replace them with the nested routes for the resources: map.resources :forums do forum forum.resources :topics do topic topic.resources :posts end end This tells Rails the structure of the resources in relation to their URLs. This means that you cannot reference a post simply with/post/id; it must be accessed with/forum/forum_id/ topics/topic_id/posts/post_id. Modifying the Layout Template and Style Sheet By modifying the default templates and adding some simple markup to our style sheet, we can improve the look of our forum and make the pages much easier to read and follow. The styles can be changed and improved later, but adding some sensible defaults makes our lives a little easier. CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 127 Removing the Generated Layouts Take a look in the/app/views/layouts directory. You will notice that thegenerate command has created layout files calledforums.rhtml,posts.rhtml, andtopics.rhtml, corresponding to the resources that were scaffolded. Rails will automatically use a layout file matching the name of the controller if one is present. Since we are using a common layout file for our application, you should remove these files, leaving just theapplication.rhtml layout file that you created. Adding CSS for the Forum Tables All of the forums data is displayed using HTML tables. We can greatly improve the look of the default tables by adding some table styling to our CSS file. Open thepublic/stylesheets/main.css file, and add the styles for the forum tables: / Forum styling / tableforums width: 100%; background-color: fff; border: 1px solid c33; tableforums width: 60% tableforums td.topic width: 20%; text-align: center; tabletopics width: 100%; background-color: fff; border: 1px solid 000; tabletopics width: 60% tabletopics td.reply width: 20%; text-align: center; tabletopics width: 20%; text-align: center; tableposts width: 100%; background-color: fff; border: 1px solid 000; tableposts width: 20%; vertical-align: top; tableposts td.body width: 80% .forumname font-size: 1.1em; .forumdescription font-size: 0.7em; padding-top: 0.4em; The Forums Controller and Views Now it is time to work on the action methods. Because the controllers were created with the scaffold_resource generation script, they are already prepopulated with code to provide basic REST services. If you open the controllers and take a look at the generated code, you will see that it is similar to the code that you wrote for all of the existing REST resource controller files. We can now use this as a basis for our forum. The top level of the forum feature, the forums resource, does not require many changes. Most of the actions already work as we want them with the generated code. However, there are a few changes that should be made and the views need to be modified to suit our needs. The Forum Index Action The generated index action method for the forums controller just retrieves all of the forum objects. Since we want to display all of the forums on the forum index page, we do not have to change this. 128 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M The Forum Index Page Open the generatedapp/views/forums/index.rhtml. While this provides a simple table of the forum data, we want to improve on this, adding extra information and formatting. Also, we do not want the Create New Forum, Edit Forum, or Delete Forum links being available to nonmoderator users. The new index.rhtml is shown in Listing 5-6. Listing 5-6. The Forum Index View h2Forums/h2 % if is_logged_in? and logged_in_user.has_role?('Moderator') % p%= link_to 'Create New Forum', new_forum_path -%/p % end % table id="forums" tr th class="name"Forum name/th th class="topic"Topics/th /tr % forums.each do forum -% tr class="%= cycle('odd', 'even') %" td class="name" div class="forumname" %= link_to, topics_path(:forum_id = forum) -% /div div class="forumdescription" %= forum.description -% /div % if is_logged_in? and logged_in_user.has_role?('Moderator') -% br / small %= link_to 'edit', edit_forum_path(forum) % %= link_to 'delete', forum_path(forum), :method = :delete, :confirm = 'Are you sure? This will delete this entire forum.' -% /small % end -% /td td class="topic"%= forum.topics_count %/td /tr % end -% /table This view makes the table view a little more interesting by adding classes to the table cells, allowing us to use CSS to style the table. The class of the table row uses thecycle helper to alternate CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 129 between rows with the classesodd andeven, allowing us to style alternating table rows with different colors or backgrounds to make the table a little easier to read. The Create New Forum, Edit Forum, and Delete Forum links are shown only for users with moderator permissions by using thehas_role method of theuser object. The view also shows the number of topics within each forum using thetopics_count counter cache attribute that is defined in the model. The Show Action In our design, the topics are subsets of a forum, and posts are subsets of a topic. This means that if you request a forum, the list of topics should be returned rather than just the details of the forum. Using the generated response would just display the name and description of a forum, rather than the topics within that forum. To show the topics, we can just forward the request of one particular forum to the topics path for that forum. The URL/forums/4 would be forwarded to/forums/4/topics. We should do the same for any requests for a specific topic, forwarding the request to the list of posts for that topic. The URL/forums/4/topics/3 becomes/forums/4/topics/3/posts. Openapp/controllers/forums_controller.rb. To forward theshow request as described previously, change theshow method as follows: def show redirect_to topics_path(:forum_id = params:id) end Since this means that theapp/views/forums/show.rhtml view file will not be used, you can delete this file. Creating a New Forum When a moderator creates a new forum, we should redirect the moderator back to the list of forums rather than going to the topic list view that will be empty. To do this, alter thecreate method in theforums_controller.rb file, and change the redirect on a successful create action: def create ... if flash:notice = 'Forum was successfully created.' format.html redirect_to forums_path format.xml head :created, :location = forum_path(forum) else ... end The Forum New and Edit Pages Both thenew.rhtml andedit.rhtml pages can be viewed only by moderator users, and both of the pages contain almost the same form. We can place the common parts of the form into a partial and include that partial in both the new and edit pages. 130 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M To do this, first create a new partial fileapp/views/forums/_form.rhtml, and add the inner code for a form: pForum Name:br /%= f.text_field :name, :size = 40 -%/p pDescription:br /%= f.text_area :description, :rows = 4, :cols = 60 -%/p Next, edit theapp/views/forums/new.rhtml file, add the code to create the form, and render the form partial by creating a form_for block and passing theform object to the partial. The new.rhtml page is shown in Listing 5-7. Listing 5-7. The Forum New View h2New forum/h2 %= error_messages_for :forum % % form_for(:forum, :url = forums_path) do f % %= render :partial = 'form', :locals = :f = f -% %= submit_tag "Create" % % end % %= link_to 'Back', forums_path % Theedit.rhtml page is almost the same, using the same way of including the form partial, except theform_for block that is created must use HTTPPUT. This is how Rails differentiates this as an objectedit action rather than acreate action. The new edit view is shown in Listing 5-8. Listing 5-8. The Forum Edit View h2Edit forum/h2 %= error_messages_for :forum % % form_for(:forum, :url = forum_path(forum), :html = :method = :put ) do f % %= render :partial = 'form', :locals = :f = f -% %= submit_tag "Update" % % end % %= link_to 'Show', forum_path(forum) % %= link_to 'Back', forums_path % Creating forms using partials can save you a lot of time if you have large forms. Keeping the form in a separate file also means that if the form is changed, you only have to change one file rather than two. The Delete Forum Action If the moderator chooses to delete an entire forum, thedestroy action is invoked. Since the generated action does just this, we can leave it exactly as it is. CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 131 Manually Testing the Forums Controller You can now start the Rails server and try creating a new forum. Of course, you will have to log in as the Admin user to see the create, edit, and delete links. Remember that in the migrations we gave the Admin user the moderator role. After you have logged in as Admin, go tohttp://localhost:3000/forums/new. Try creating a new forum and saving it. You will be returned to the list of available forums, as shown in Figure 5-2. Figure 5-2. The forum index action showing the list of available forums The Topics Controller and Views We now need to develop the next level of the forum data—the topics. As we stated in the specifica- tions, each topic belongs to a specific Forum object. When a forum is selected, the topics in this forum are listed, and new topics can be created by users of the site. The Topic Index Action The generated topicsindex action currently just retrieves all topics. Since we want to see topics only for a given forum, we need to change this behavior. Also, since the number of topics is likely to be large, we should paginate the topic list. To do this, we will use the Rails paginator helper as we did in Chapter 4. 132 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M Open the topics controller file,app/controllers/topics_controller.rb, and change the index action as follows: def index forum = Forum.find(params:forum_id) topics_pages, topics = paginate(:topics, :include = :user, :conditions = 'forum_id = ?', forum, :order = 'topics.updated_at DESC') respond_to do format format.html index.rhtml format.xml render :xml = topics.to_xml end end As you can see, this first retrieves theforum object and uses thepaginate helper to create topics_pages andtopics, which will be passed to the view. The Topic Index Page The topic index is a list of topics for a given forum. This is similar in structure and markup to the forum index, except that obviously the table columns are different. The option to create a new topic is available to all logged-in users, not just moderators. If a user isn’t logged in, a link to the login page is shown instead of the Create New Topic link. Each topic in the table should also show edit and delete links for moderator users. The delete link should ask for the moderator to confirm that he or she wishes to delete the entire topic. Theapp/views/topics/index.rhtml page should be edited as shown in Listing 5-9. Listing 5-9. The Topic Index View h2Forum : %= -%/h2 h3Topics/h3 p % if is_logged_in? -% %= link_to 'Post New Topic', new_topic_path(:forum_id = forum) -% % else -% %= link_to 'Login to post a new topic', :controller = 'account', :action = 'login' -% % end -% /p % if topics_pages.page_count 1 % p class="pagination"Pages: strong %= pagination_links topics_pages, :params = params % /strong/p % end % CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 133 table id="topics" tr th class="name"Topics/th th class="reply"Posts/th th class="author"Author/th /tr % topics.each do topic -% tr class="%= cycle('odd', 'even') %" td class="name" %= link_to, posts_path(:forum_id = forum, :topic_id = topic) -% % if is_logged_in? and logged_in_user.has_role?('moderator') -% br / small %= link_to 'delete', topic_path(:forum_id = forum, :id = topic), :method = :delete, :confirm = 'Are you sure? This will delete this entire topic.' -% %= link_to 'edit', edit_topic_path(:forum_id = forum, :id = topic) -% /small % end -% /td td class="reply"%= topic.posts_count %/td td class="author"%= link_to topic.user.username, user_path(:id = topic.user) %/td /tr % end -% /table % if topics_pages.page_count 1 % p class="pagination"Pages: strong %= pagination_links topics_pages, :params = params % /strong/p % end % As you can see, each topic is linked to theshow action for a given topic. Theposts_count cache is used to display the number of posts without having to perform another database query. Creating a New Topic All logged-in users are allowed to create new topics. In our forum, a topic cannot exist without any posts belonging to it. Therefore, we must change the behavior of the topics’create action, so that creating a new topic creates a first post too. This stops empty topics appearing on the site and improves the usability—when the users create new topics, it’s almost certain that they will want to create posts about those topics. Take a look at the existing new andcreate actions in thetopics_controller.rb file. At the moment, they simply create a new topic without specifying a forum that the topic belongs to, and obviously,create does not create a post for the topic. 134 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M Change thenew method to instantiate apost object as well as atopic object: def new topic = post = end The view file for this method needs to allow the user to enter a topic name and a message. Edit theapp/views/topics/new.rhtml file as shown in Listing 5-10. Listing 5-10. The New Topic View h2New Topic/h2 % form_for :topic, :url = topics_path do f -% pSubject:br /%= f.text_field :name, :size = 40 -%/p pFirst Post:br /%= text_area :post, :body, :rows = 8, :cols = 60 -%/p %= submit_tag 'Save' % % end -% You will notice that the post body field is specified using a regulartext_area helper, rather than the form blockf. Even though the form is a new topic form and will be posted to the topic controller, we can still add fields for other objects. Now take a look at the followingcreate method that this form will be posted to. As discussed, this method creates not only a new topic but also a new post belonging to this topic. We also need to make sure that the topic is created with the relevantforum_id filled in from the parameter passed to the action via the URL. def create topic = = params:topic:name, :forum_id = params:forum_id, :user_id = post = = params:post:body, :topic_id =, :user_id = respond_to do format format.html redirect_to posts_path(:topic_id = topic, :forum_id = format.xml head :created, :location = topic_path(:id = topic, :forum_id = end rescue ActiveRecord::RecordInvalid respond_to do format format.html render :action = 'new' format.xml render :xml = post.errors.to_xml end end CH A PT E R 5 ■ B U I L DI N G A D IS CU S SI O N F OR U M 135 As you can see, the topic and post are both created with theuser_id of the logged-in user and the relevant forum and topicids. If either of the objects is reported as invalid by ActiveRecord, this will be caught by the rescue exception handler, and thenew view will be re-rendered with the appropriate error messages, or an XML error message will be returned. If there is no problem, the user is either redirected to the list of posts for this new topic or the relevant XML response is sent. Editing a Topic Editing a topic can be performed only by a moderator user and allows only the topic name to be changed. The generated code is almost what we need; we just need to change a few things for it to work exactly as we want. First of all, edit thetopics_controller.rb file to change the redirection on a successful topic update. Because we are using nested routes, we need to include the forumid as part of the redirection and change it to show the list of posts, as follows: def update topic = Topic.find(params:id) respond_to do format if topic.update_attributes(params:topic) flash:notice = 'Topic was successfully updated.' format.html redirect_to posts_path(:topic_id = topic, :forum_id = format.xml head :ok else format.html render :action = "edit" format.xml render :xml = topic.errors.to_xml end end end We also need to edit theapp/views/topics/edit.rhtml topic view file to add the field that we want to allow to be edited. Also, the URL that the edit form posts to needs to be amended to include the forumid along with the topicid that is being edited. Edit the form section of the file to match the following: h1Editing topic/h1 %= error_messages_for :topic % % form_for(:topic, :url = topic_path(:id = topic, :forum_id =, :html = :method = :put ) do f % pSubject:br /%= f.text_field :name, :size = 40 -%/p p %= submit_tag "Update" % /p % end % 136 CH AP T E R 5 ■ B U I L DI N G A D IS CU S SI ON FO R U M Deleting a Topic The autogenerateddestroy action simply destroys a given topic. We could just destroy the topic, which would automatically delete all the posts within this topic because of the dependency delete_all statement in the Topic model. However, since we are using a counter cache to keep track of the number of posts by a given user, we need to delete each post in turn. This isn’t the most efficient method, but since deleting a topic is something that is going to be performed seldom and only by a moderator, it is acceptable. Also, currently the method redirects totopics_path but does not specify the forumid. We need to change this and add theforum_id parameter to the redirection, as follows: def destroy topic = Topic.find(params:id) topic.posts.each post post.destroy topic.destroy respond_to do format format.html redirect_to topics_path( :forum_id = params:forum_id) format.xml head :ok end end The Topic Show Action We should also edit theshow action to redirect the browser to theindex action of the posts controller. Edit theshow action of thetopics_controller file as follows: def show redirect_to posts_path(:forum_id = params:forum_id, :topic_id = params:id) end The Posts Controller and Views All that is left is to adapt the posts controller code to work with the forum and topics. The Posts Index Page The postsindex method lists all of the posts in a given topic. This requires us to change the index action to find the current topic and then retrieve all the posts within this topic, along with paginating the posts. We will use the default value of ten posts per page. Edit theapp/controllers/posts_controller.rb file, changing theindex action as follows: def index topic = Topic.find(params:topic_id, :include = :forum) posts_pages, posts = paginate(:posts, :include = :user, :conditions = 'topic_id = ?', topic)

Advise: Why You Wasting Money in Costly SEO Tools, Use World's Best Free SEO Tool Ubersuggest.