Skip to content

Customizing Metadata (Sufia 6): nested attributes, part 2

Michael J. Giarlo edited this page Jul 1, 2016 · 2 revisions

Overview

This builds on Customizing Metadata (Sufia 6): nested attributes, part 1 -- this guide creates each nested attribute as a complete ActiveFedora:::Base object rather than as a blank node.

This example is for implementors who have a complex RDF data model that includes descriptive metadata from one resource referenced in another. As example, let's say you use Sufia's GenericFile, but with multiple authors, and each author is also its own RDF resource with a first and last name. We'll build the models and specs required to test them, as well as the controllers that will pass the attributes from the forms to the model, and the specs required to test them too.

This walkthrough assumes you have a blank Sufia 6.0 installation.

Test your GenericFile

First, write a test expressing what you want this to look like. If you haven't already, setup your rspec environment:

bundle exec rails g rspec:install

Write your test in a file called spec/models/generic_file_spec.rb

require 'rails_helper'

describe GenericFile do

  let(:file) do
    GenericFile.create do |f|
      f.apply_depositor_metadata "user"
    end
  end

  describe "setting the title" do
    before { file.title = ["My Favorite Things"] }
    subject { file.title}
    it { is_expected.to eql ["My Favorite Things"] }
  end

  describe "adding an author" do
    before { file.authors_attributes = [{first_name: "John", last_name: "Coltrane"}] }
    subject { file.authors.first }
    it { is_expected.to be_kind_of Author }
  end
end

Run the test and the first one should pass, but the second one will fail.

Build your models

Create app/models/generic_file.rb and build your model:

class GenericFile < ActiveFedora::Base
  include Sufia::GenericFile

  has_and_belongs_to_many :authors, predicate: ::RDF::DC.creator, class_name: "Author", inverse_of: :generic_files

  accepts_nested_attributes_for :authors
end

Create app/models/author.rb

class Author < ActiveFedora::Base
  type ::RDF::FOAF.Person

  has_many :generic_files, inverse_of: :authors, class_name: "GenericFile"

  property :first_name, predicate: ::RDF::FOAF.firstName, multiple: false do |index|
    index.as :stored_searchable
  end

  property :last_name, predicate: ::RDF::FOAF.lastName, multiple: false do |index|
    index.as :stored_searchable
  end
end

Run your test again and it should pass. At this point you can add additional nested terms and start expanding your model tests to cover additional needs, but you'll eventually have to wire up some controllers that will allow your forms to pass these nested attributes' values on to your models.

Build your controllers

First, we test, but in order to test controllers, we need to add a few extras to our testing suite. Since our controller needs to know who is doing these actions, we need to stub out a user. Sufia uses the FactoryGirl gem for this. Add it to your Gemfile and include it with other gems in your :development and :test blocks.

group :development, :test do
  # ...

  gem 'factory_girl_rails'
end

Update your gems:

bundle install

Next, edit spec/rails_helper.rb to include test helpers from the Devise gem as well as setup FactoryGirl. You'll want to add the two config lines below to your existing config block

RSpec.configure do |config|
  #...

  config.include Devise::TestHelpers, type: :controller
  config.include FactoryGirl::Syntax::Methods
end

Additionally, add these methods to the very end of spec/rails_helper.rb

FactoryGirl.define do
  factory :user do
    sequence(:email) { |n| "user#{n}@example.com" }
    password 'password'
  end
end

module FactoryGirl
  def self.find_or_create(handle, by=:email)
    tmpl = FactoryGirl.build(handle)
    tmpl.class.send("find_by_#{by}".to_sym, tmpl.send(by)) || FactoryGirl.create(handle)
  end
end

Now, let's write our controller test in spec/controllers/generic_files_controller_spec.rb

require 'rails_helper'

describe GenericFilesController do
  routes { Sufia::Engine.routes }
  let(:user) { FactoryGirl.find_or_create(:user) }
  before { sign_in user }

  describe "update" do
    let(:generic_file) do
      GenericFile.create do |gf|
        gf.apply_depositor_metadata(user)
      end
    end

    context "when adding a title" do
      let(:attributes) { { title: ['My Favorite Things'] } }
      before { post :update, id: generic_file, generic_file: attributes }
      subject do
        generic_file.reload.title
      end
      it { is_expected.to eq ["My Favorite Things"] }
    end

    context "when adding an author" do
      let(:attributes) do
        { 
          title: ['My Favorite Things'], 
          authors_attributes: [{ first_name: 'John', last_name: 'Coltrane' }],
          permissions_attributes: [{ type: 'person', name: 'archivist1', access: 'edit'}]
        }
      end

      before { post :update, id: generic_file, generic_file: attributes }
      subject { generic_file.reload }

      it "sets the values using the parameters hash" do
        expect(subject.authors.first.first_name).to eq "John"
        expect(subject.authors.first.last_name).to eq "Coltrane"
      end
    end
  end
end

The first test should pass, but the second will fail.

Build out your forms and presenters

Sufia builds its forms based on a Presenter. For GenericFile, this is the Sufia::GenericFilePresenter. We need to create our own presenter so we can include the Author class as an attribute.

First, create app/presenters/resource_presenter.rb

class ResourcePresenter < Sufia::GenericFilePresenter
  self.terms = [:title, :authors]
end

Next, create the edit and batch edit forms that will use our presenter:

app/forms/resource_edit_form.rb

class ResourceEditForm < ResourcePresenter
  include HydraEditor::Form
  include HydraEditor::Form::Permissions

  self.required_fields = [:title]

  protected
    def self.build_permitted_params
      permitted = super
      permitted.delete({ authors: [] })
      permitted << { authors_attributes: permitted_authors_params }
      permitted
    end

    def self.permitted_authors_params
      [ :id, :_destroy, :first_name, :last_name ]
    end
end

app/forms/resource_batch_edit_form.rb

class ResourceBatchEditForm < ResourceEditForm
end

We have to create the authors_attributes= method on the form so that Rails will build the nested forms. ActionView::Helpers is involved here

Last, but certainly not least, we have to tell our controller to use our new ResourcePresenter and forms. To do this, create app/controllers/generic_files_controller.rb with:

class GenericFilesController < ApplicationController
  include Sufia::Controller
  include Sufia::FilesControllerBehavior

  self.presenter_class = ResourcePresenter
  self.edit_form_class = ResourceEditForm
end

Now you should be able to rerun your controller spec and have both tests passing.