Search code examples
ruby-on-railsadministrate

Show Administrate Attribute in One Area But Not In Another


I'm creating a Learning Management System. I've got Users enrolled in Courses. I'd like to show when a User has completed a course on the Users Show page. Like this:

enter image description here

If I add the complete Attribute in app/dashboards/course_dashboard.rb under COLLECTION_ATTRIBUTES, it shows in the Course Index page also, which I don't want:

enter image description here

How can I add this complete attribute to the user's course information on the User Show page (as in the first image) but not add it to the Course Index page (as in the second image)?


Solution

  • Unfortunately, Administrate doesn't support this out of the box. However it's possible with a little bit of Ruby trickery. This is unofficial and the exact implementation may change with new versions of Administrate. It should work with Administrate 0.15, and probably other versions.

    The key is this template in Administrate's code: https://github.com/thoughtbot/administrate/blob/c16b8d1ee3a5ef1d622f9470738e89d73dbb8f1b/app/views/administrate/application/_collection.html.erb

    There are two lines that are important here. The first one lists the table headers <th>, and it is this one:

    <% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
    

    The second one lists the data columns <td> for each record, and it looks like this:

    <% collection_presenter.attributes_for(resource).each do |attribute| %>
    

    The link is to the template that Administrate uses to render collections. This can be an index page, or a list of records in a HasMany field. In each of the lines above, it iterates through the collection attributes defined for the dashboard, as returned by collection_presenter.attribute_types and collection_presenter.attributes_for(...), depending on the case.

    In order to achieve your desired effect, you need those lists to be different when rendering an index page or when rendering the HasMany list. Currently there isn't an option for HasMany fields to dictate that this list has to be any different in their case.

    Fortunately we can hack something together here.

    First, remove :complete from CourseDashboard::COLLECTION_ATTRIBUTES. You don't want it listed in the index page, so it shouldn't appear there. Do not remove it from CourseDashboard::ATTRIBUTE_TYPES, as we still need to define it so that we can use it elsewhere.

    Second, create a new field. I'm going to call it CustomHasMany, but it could be anything:

    $ ./bin/rails g administrate:field custom_has_many
          create  app/fields/custom_has_many_field.rb
          create  app/views/fields/custom_has_many_field/_show.html.erb
          create  app/views/fields/custom_has_many_field/_index.html.erb
          create  app/views/fields/custom_has_many_field/_form.html.erb
    

    We'll use this field for your courses attribute in UserDashboard:

    require "administrate/base_dashboard"
    
    class UserDashboard < Administrate::BaseDashboard
      ATTRIBUTE_TYPES = {
        # ...
        courses: CustomHasManyField
        # ...
      }
    
    

    This is not going to work initially, as the field is new. The first thing it needs is to copy the behaviour of the existing HasMany field. We can do this with class inheritance:

    class CustomHasManyField < Administrate::Field::HasMany
    end
    

    This won't quite mimic the HasMany field because it's using the basic templates provided by the generator. Let's tell it to use the has_many templates instead:

    class CustomHasManyField < Administrate::Field::HasMany
      def to_partial_path
        "/fields/has_many/#{page}"
      end
    end
    

    OK, so now it should be the same as a HasMany field. So far this has been using public interfaces and "official" Administrate stuff. I have to admit that to_partial_path is not well documented, but I think it's stable enough.

    So now we have to tell it to add complete to the list of fields... This is where the hack comes in play.

    If you read the source code of Administrate, you'll find that collection_presenter above is provided by the upper-level template. In turn this is defined as field.associated_collection(order), where field is the field object, which in our case is an instance of CustomHasManyField.

    So if we can hack CustomHasManyField#associated_collection to return a collection whose attribute_types and attributes_for include :complete... we should be ok?

    Looking at the code for collections, we can see that both lists are in turn based on the result of another method attribute_names, which is the source of truth as to which attributes should be rendered: https://github.com/thoughtbot/administrate/blob/c16b8d1ee3a5ef1d622f9470738e89d73dbb8f1b/lib/administrate/page/collection.rb If we modify this attribute_names method, the rest should follow suit.

    Monkeypatching to the rescue:

    class CustomHasManyField < Administrate::Field::HasMany
      def associated_collection(*)
        collection = super
    
        def collection.attribute_names
          [:complete] + super
        end
    
        collection
      end
    
      def to_partial_path
        "/fields/has_many/#{page}"
      end
    end
    

    That looks like it works in my computer. Does it work for you?

    As for doing this in a more official manner... Do you feel like creating a PR for the project?