Posts [ 11 ]

Topic: How to use dynamic finders?

I'm new to rails, but I'm building a baking journal app and having issue with my Ingredient model as whenever I input an ingredient on the entry form I'm creating a new ingredient each time. What I want to do is check if the ingredient table already has that ingredient, and if so to link it, if not, create it. So for example, I create a new Pizza entry, and I type "flour", "water", "salt" for the ingredients. All these records already exist in my ingredients table, so I want to just reference those instead of creating a new one. I heard that dynamic finders would do what I want, but I'm not able to get that to work as I don't know how to access it from the form... This is my entries controller new action where I define a dynamic finder:

Re: How to use dynamic finders?

I never tried this myself, so I'd appreciate you provide the running project so I could tinker with it to find out what exactly is going on (upload code to github or an archive of your folder somewhere)

You're JS triggers for "Add Ingredient" seem a bit unorthodox I'd rather use rails.js/UJS and `link_to :remote => true` to render ingredient list items. This way you could refactor the ingredient form fields to use a partial both in the initial form setup and when a new ingredient row is requested.

Re: How to use dynamic finders?

@DivineDominion Thanks! Yeah I know my JS is very unorthodox. I was trying to follow ryan bates railscast on nested forms and I couldn't get the add field to work because of some error, so i decided to roll my own for now.

I'll look into those auto-completion gems right now, but I'm new to all this programming and so I prefer not to use gems that do magic haha

The hidden field is set upon selecting an entry from the auto-complete widget.

This way, though, people could still enter an ingredient and _not_ select the suggestion from the list. If the auto-completion widget isn't used, the ID won't be set. To me it seems you'd fare better with a combined approach:

Optional: offer auto-completion on the form for user's convenience. Don't set a hidden ID field to build the association but simply insert a canonical ingredient name this way.

Re: How to use dynamic finders?

Thanks for looking so deep into this!

You're absolutely right, and I do want to eventually implement an auto-complete feature so that users can select from a pre-defined list of ingredients in addition to being able to create their own. However, I'm trying to learn this step by step, and for me the next part of this is to figure out how to stop duplicating the ingredients.

The table only has 'name' as an attribute, but whenever a form is submitted, a new ingredient is created. This is why I wanted to use dynamic finders, I was told it would be the way to stop creating ingredients on each form submit if the ingredient already existed as it would find the ingredient by name or if it returns nil it creates it. But I'm not sure how to use the dynamic finders... you actually pointed out my attempt at using it in your last comment about the entries#new action, I just put it there and didn't really know what to do with it haha. Where would you put this code? I don't have an ingredients controller and so thats why I was trying to find a place for it in the entries controller, but I'm starting to think the best place for it to go would be in the ingredients controller.

This is the logic that should happen:

User clicks to create a new entry

User types multiple ingredients into the entry form

On form submit, find if ingredients exist in table, the ones which do, assign their id to ingredient_id in entry_ingredients table. For the ones that don't, create them and assign their id to ingredient_id in entry_ingredients.

Re: How to use dynamic finders?

Okay, I assume your rationale was like this: in entries#new, I have to prepare an @entry=Entries.new object so the form knows where it belongs to. Therefore, I have to prepare an Ingredients object, too.

Thing is, you're going to create (and store in your database) at least one empty ingredient object this way, maybe only on your first request, but that's one object too much nevertheless. This it what the server log on the console reveals with a fresh database:

Bonus information: you should update your database schema to disallow NULL values on the name. In my opinion, the database schema should reflect whether a vlaue must be set or defaults to something.

Since I'm pretty much going to need a feature like this in my app soon, I'm interested in your progress Judging from the code, I suggest you add a lot more tests to automate finding out when the form does what you want. Use `autotest` or setup `guard` to have tests running in the background and shorten feedback loops on code changes.

Now add some functional/integration tests which exercise two scenarios from a user's perspective:

Visit the homepage, follow the link to the form or request the form directly. This shouldn't hit the database, i.e. it shouldn't `assert_difference` on Ingredient.count. Ensure the test database is cleaned before running the test suite, I don't remeber whether that's the default behaviour.

Prepare an ingredient, then request and fill in the form with something plus the ingredient you know exists. Ensure no Ingredient is created. This test will most likely fail until you're done, that's intended: you'll know when you're done with this task

In the same spirit, add some more controller or request tests in which you call `post :create, ...` to emulate form submission. Prepare an Ingredient object on test suite setup, request entries#create via post and some working parameters in which you include a way to identify the ingredient. Be creative: what do you want a working form (which doesn't exist yet) to tell the controller? Will `:ingredient => { :name => 'foo' }` do the job, or would you prefer the form submits an ID to distinguish existing and new ingredients?

And in controller tests, would you rather assert Ingredient.where(...).fine_or_create(...) is called or would you prefer your own interface and grow your app around your own expectations?

That's the cool part of Behavior- or Test-Driven Development: you decide how you'd like things to work and then figure out how on earth one could implement that stuff. The other way 'round, you'd always be limited by what you already know.

Re: How to use dynamic finders?

Thanks for all the pointers and for pointing out that insert statement in the server log, I hadn't noticed that and its clearly not what I'm going for!

My rationale for adding that piece of code to entries#new went more like this: "This probably needs to go in an ingredients controller, but let me see if I can find a place for it in the entries controller. Looks like entries#new or entries#create might be good candidates. Since whenever the new entry button is clicked, it goes to the controller action new, and a new entry is initialized each time this happens with @entry = Entry.new, this is probably the place for me to search for or create a new Ingredient." But now that I restate that and read over your explanation, I realize thats not right at all as it will do this logic whenever a new entry is initialized... What I want to do is on form submit, or on create but this should work for edits too. It should search the database for each ingredient that was submitted in the params[:entry][:entry_ingredients_attributes][:ingredient_attributes][:name], create it if it doesn't exist or assign the current one to the entry_ingredient it belongs to. Sounds simple enough but causing so much headache!

My initial attempt at solving this was a before_save on the model, but I didn't get far with the logic on that one, might go back to that though since I can't think where else to do the check for each ingredient thats being saved.

In regards to the tests, I've never created any tests before, that one you pointed out must have been created with the scaffolding I used when starting the project (never even ran the test once). I used scaffolding once just to see how it works, I should have deleted the app and started again from scratch since I'm learning. I do, however, want to learn testing as I think BDD is the best way to create applications because you also then have an easy way to find out if things broke and where they broke. Was looking into cucumber but it was over my head when I first started out, should give it another look now. Have a good read on how to get started?

Re: How to use dynamic finders?

I agree with you, @gih. Before you overload your ActiveRecord models with before_save statements, I think the way you learn the most and gain the most in the end is really this:

get confident with TDD/BDD,

test what's there already,

introduce a form handler model which pulls together ActiveRecord codes and which is decoupled from the database during testing. This should provide a unique and suitable domain specific language via custom methods you yourself invent. Backed by TDD I feel really comfortable doing stuff like that, while I used to cling to Rails' internals and wanted Rails to solve everything out of the box for me because I didn't dare to stray.

write tests on different levels to state what's missing one after another and make them pass

Maybe this takes an additional week or tws until you feel confident, but the benefits will be huge!

I got my hands on TDD with Rails via extensive web search and reading lots of StackOverflow questions. I really like the everydayrails.com series @anso pointed out. It got me started as well.