{"id":344,"date":"2012-11-20T14:22:39","date_gmt":"2012-11-20T14:22:39","guid":{"rendered":"http:\/\/nicholshayes.co.uk\/blog\/?p=344"},"modified":"2013-10-31T15:56:11","modified_gmt":"2013-10-31T15:56:11","slug":"sorting-with-has_many-through-and-acts_as_list","status":"publish","type":"post","link":"http:\/\/nicholshayes.co.uk\/blog\/?p=344","title":{"rendered":"Sorting with has_many :through and acts_as_list"},"content":{"rendered":"<p>I&#8217;m currently working on a questionnaire system which has two main objects: Question and Questionnaire. I had been associating them using has_and_belongs_to_many, but a new requirement means that I now need to be able to define a question order within each questionnaire. <\/p>\n<p><a href=\"https:\/\/github.com\/swanandp\/acts_as_list\">act_as_list<\/a> gives me much of the functionality I want. However, I could not just add that to the Question model, because I may need a question to be at position 1 in one questionnaire, and position 4 within another.<\/p>\n<p>The solution was to refactor the has_and_belongs_to_many association to use has_many :through. To do this I had to create a joining class. I called this new class QuestionnairesQuestion. Using a plural on the first word of the class name allowed me to use the has_and_belongs_to_many join table, so I wouldn&#8217;t lose the existing relationships.<\/p>\n<p>The required migration took a little work. First I had to add in an id and make it a primary key (the existing table only had question_id and questionnaire_id fields). Then I also needed to generate some initial values for the new fields I was adding. This was my solution:<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"ruby codecolorer\"><span class=\"kw1\">class<\/span> AddIdsToQuestionnairesQuestions <span class=\"sy0\">&lt;<\/span> <span class=\"re2\">ActiveRecord::Migration<\/span><br \/>\n&nbsp; <span class=\"kw1\">def<\/span> up<br \/>\n&nbsp; &nbsp; add_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:id<\/span>, <span class=\"re3\">:primary_key<\/span><br \/>\n&nbsp; &nbsp; add_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:position<\/span>, :<span class=\"kw3\">integer<\/span><br \/>\n&nbsp; &nbsp; add_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:created_at<\/span>, <span class=\"re3\">:datetime<\/span><br \/>\n&nbsp; &nbsp; add_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:updated_at<\/span>, <span class=\"re3\">:datetime<\/span><br \/>\n&nbsp; &nbsp; <br \/>\n&nbsp; &nbsp; execute <span class=\"st0\">&quot;UPDATE questionnaires_questions SET position = id, created_at = now(), updated_at = now()&quot;<\/span><br \/>\n&nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; <span class=\"kw1\">def<\/span> down<br \/>\n&nbsp; &nbsp; remove_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:id<\/span><br \/>\n&nbsp; &nbsp; remove_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:position<\/span><br \/>\n&nbsp; &nbsp; remove_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:created_at<\/span><br \/>\n&nbsp; &nbsp; remove_column <span class=\"re3\">:questionnaires_questions<\/span>, <span class=\"re3\">:updated_at<\/span><br \/>\n&nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; <br \/>\n<span class=\"kw1\">end<\/span><\/div><\/div>\n<p>I then needed to add the acts_as_list declaration to QuestionnairresQuestion with a scope option so that sorting was per questionnaire. These are the associations that worked for me:<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;height:300px;\"><div class=\"ruby codecolorer\"><span class=\"kw1\">class<\/span> Question <span class=\"sy0\">&lt;<\/span> <span class=\"re2\">ActiveRecord::Base<\/span><br \/>\n&nbsp;<br \/>\n&nbsp; has_many <span class=\"re3\">:questionnaires_questions<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; has_many<span class=\"br0\">&#40;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"re3\">:questionnaires<\/span>,<br \/>\n&nbsp; &nbsp; <span class=\"re3\">:through<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"re3\">:questionnaires_questions<\/span>, <br \/>\n&nbsp; &nbsp; <span class=\"re3\">:uniq<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"kw2\">true<\/span><br \/>\n&nbsp; <span class=\"br0\">&#41;<\/span><br \/>\n<span class=\"kw1\">end<\/span><br \/>\n<br \/>\n<span class=\"kw1\">class<\/span> Questionnaire <span class=\"sy0\">&lt;<\/span> <span class=\"re2\">ActiveRecord::Base<\/span><br \/>\n<br \/>\n&nbsp; has_many<span class=\"br0\">&#40;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"re3\">:questionnaires_questions<\/span>, <br \/>\n&nbsp; &nbsp; <span class=\"re3\">:order<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"st0\">'position'<\/span><br \/>\n&nbsp; <span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; has_many<span class=\"br0\">&#40;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"re3\">:questions<\/span>,<br \/>\n&nbsp; &nbsp; <span class=\"re3\">:uniq<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"kw2\">true<\/span>,<br \/>\n&nbsp; &nbsp; <span class=\"re3\">:through<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"re3\">:questionnaires_questions<\/span>,<br \/>\n&nbsp; &nbsp; <span class=\"re3\">:order<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"st0\">'position'<\/span><br \/>\n&nbsp; <span class=\"br0\">&#41;<\/span><br \/>\n<span class=\"kw1\">end<\/span><br \/>\n<br \/>\n<span class=\"kw1\">class<\/span> QuestionnairesQuestion <span class=\"sy0\">&lt;<\/span> <span class=\"re2\">ActiveRecord::Base<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; belongs_to <span class=\"re3\">:questionnaire<\/span><br \/>\n&nbsp; belongs_to <span class=\"re3\">:question<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; acts_as_list <span class=\"re3\">:scope<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"re3\">:questionnaire<\/span><br \/>\n<span class=\"kw1\">end<\/span><\/div><\/div>\n<p>This worked, but I did have an issue with determining which question was top within each scope. The problem was that act_as_list assumes the top item will have the position &#8216;1&#8217;. However, I&#8217;d made the position match the id, which meant that at best only one item would have a position of &#8216;1&#8217;. To fix this I created this rake task:<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"ruby codecolorer\">namespace <span class=\"re3\">:data<\/span> <span class=\"kw1\">do<\/span><br \/>\n<br \/>\n&nbsp; <span class=\"co1\"># Usage: rake data:reset_positions RAILS_ENV=production<\/span><br \/>\n&nbsp; desc <span class=\"st0\">&quot;Goes through each of the acts_as_list objects and resets the positions based on order they were added to the database&quot;<\/span><br \/>\n&nbsp; task <span class=\"re3\">:reset_positions<\/span> <span class=\"sy0\">=&gt;<\/span> <span class=\"re3\">:environment<\/span> <span class=\"kw1\">do<\/span><br \/>\n&nbsp; &nbsp; <span class=\"re2\">ActiveRecord::Base<\/span>.<span class=\"me1\">connection<\/span>.<span class=\"me1\">execute<\/span> <span class=\"st0\">&quot;START TRANSACTION;&quot;<\/span><br \/>\n&nbsp; &nbsp; Questionnaire.<span class=\"me1\">all<\/span>.<span class=\"me1\">each<\/span> <span class=\"kw1\">do<\/span> <span class=\"sy0\">|<\/span>questionnaire<span class=\"sy0\">|<\/span><br \/>\n&nbsp; &nbsp; &nbsp; first_id = questionnaire.<span class=\"me1\">questionnaires_questions<\/span>.<span class=\"me1\">minimum<\/span><span class=\"br0\">&#40;<\/span><span class=\"re3\">:id<\/span><span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"kw1\">if<\/span> first_id &nbsp; <span class=\"co1\"># nil if questionnaire has no questions<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; sql = <span class=\"st0\">&quot;UPDATE questionnaires_questions SET position = (1 + id - #{first_id}) WHERE questionnaire_id = #{questionnaire.id};&quot;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; &nbsp; <span class=\"re2\">ActiveRecord::Base<\/span>.<span class=\"me1\">connection<\/span>.<span class=\"me1\">execute<\/span> sql<br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; &nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; &nbsp; <span class=\"re2\">ActiveRecord::Base<\/span>.<span class=\"me1\">connection<\/span>.<span class=\"me1\">execute<\/span> <span class=\"st0\">&quot;COMMIT;&quot;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"kw3\">puts<\/span> <span class=\"st0\">&quot;Positions reset&quot;<\/span><br \/>\n&nbsp; <span class=\"kw1\">end<\/span><br \/>\n<span class=\"kw1\">end<\/span><\/div><\/div>\n<p>Running that got the associations working. There is probably a better way of doing this, but this will do for now.<\/p>\n<p>My next problem was how to access the act_as_list functionality. <\/p>\n<p>If questions were the act_as_list object, you could do things like this<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"ruby codecolorer\">questionnaire.<span class=\"me1\">questions<\/span>.<span class=\"me1\">last<\/span>.<span class=\"me1\">move_higher<\/span><\/div><\/div>\n<p>However, as questions are used on multiple questionnaires and they need to be independently sortable within each questionnaire, it is the through table model QuestionnairesQuestion that acts_as_list. To change position the change must be made in the context of the questionnaire.<\/p>\n<p>My solution was to add these methods to Questionnaire:<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"ruby codecolorer\">&nbsp; private<br \/>\n&nbsp; <span class=\"kw1\">def<\/span> method_missing<span class=\"br0\">&#40;<\/span>symbol, <span class=\"sy0\">*<\/span>args, <span class=\"sy0\">&amp;<\/span>block<span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"kw1\">if<\/span> acts_as_list_method?<span class=\"br0\">&#40;<\/span>symbol<span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; &nbsp; pass_method_to_questionnaires_question<span class=\"br0\">&#40;<\/span>symbol, args.<span class=\"me1\">first<\/span><span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"kw1\">else<\/span><br \/>\n&nbsp; &nbsp; &nbsp; <span class=\"kw1\">super<\/span><br \/>\n&nbsp; &nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; <span class=\"kw1\">def<\/span> pass_method_to_questionnaires_question<span class=\"br0\">&#40;<\/span>symbol, question<span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"kw3\">raise<\/span> <span class=\"st0\">&quot;A Question is needed to identify QuestionnairesQuestion&quot;<\/span> <span class=\"kw1\">unless<\/span> question.<span class=\"me1\">kind_of<\/span>? Question<br \/>\n&nbsp; &nbsp; questionnaires_question = questionnaires_questions.<span class=\"me1\">where<\/span><span class=\"br0\">&#40;<\/span><span class=\"re3\">:question_id<\/span> <span class=\"sy0\">=&gt;<\/span> question.<span class=\"me1\">id<\/span><span class=\"br0\">&#41;<\/span>.<span class=\"me1\">first<\/span><br \/>\n&nbsp; &nbsp; questionnaires_question.<span class=\"me1\">send<\/span><span class=\"br0\">&#40;<\/span>symbol<span class=\"br0\">&#41;<\/span> <span class=\"kw1\">if<\/span> questionnaires_question<br \/>\n&nbsp; <span class=\"kw1\">end<\/span><br \/>\n&nbsp; <br \/>\n&nbsp; <span class=\"kw1\">def<\/span> acts_as_list_method?<span class=\"br0\">&#40;<\/span>symbol<span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; &nbsp; <span class=\"re2\">ActiveRecord::Acts::List::InstanceMethods<\/span>.<span class=\"me1\">instance_methods<\/span>.<span class=\"kw1\">include<\/span>?<span class=\"br0\">&#40;<\/span>symbol.<span class=\"me1\">to_sym<\/span><span class=\"br0\">&#41;<\/span><br \/>\n&nbsp; <span class=\"kw1\">end<\/span><\/div><\/div>\n<p><em>pass_method_to_questionnaires_question<\/em> in combination with <em>method_missing<\/em>, allows you to pass to a questionnaire the acts_as_list method together with the question it needs to effect. The equivalent move_higher call then becomes:<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"ruby codecolorer\">questionnaire.<span class=\"me1\">move_higher<\/span><span class=\"br0\">&#40;<\/span>questionnaire.<span class=\"me1\">questions<\/span>.<span class=\"me1\">last<\/span><span class=\"br0\">&#41;<\/span><\/div><\/div>\n<p>You can now also do:<\/p>\n<div class=\"codecolorer-container ruby default\" style=\"overflow:auto;white-space:nowrap;\"><div class=\"ruby codecolorer\">questionnaire.<span class=\"me1\">move_to_top<\/span><span class=\"br0\">&#40;<\/span>question<span class=\"br0\">&#41;<\/span><br \/>\nquestionnaire.<span class=\"me1\">last<\/span>?<span class=\"br0\">&#40;<\/span>question<span class=\"br0\">&#41;<\/span><\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;m currently working on a questionnaire system which has two main objects: Question and Questionnaire. I had been associating them using has_and_belongs_to_many, but a new requirement means that I now need to be able to define a question order within &hellip; <a href=\"http:\/\/nicholshayes.co.uk\/blog\/?p=344\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[3],"tags":[],"_links":{"self":[{"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/344"}],"collection":[{"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=344"}],"version-history":[{"count":17,"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/344\/revisions"}],"predecessor-version":[{"id":459,"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/344\/revisions\/459"}],"wp:attachment":[{"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=344"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=344"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/nicholshayes.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=344"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}