www.flickr.com
Michael Kovacs' photos More of Michael Kovacs' photos
Recommend Me Cable Car Software logo

Thursday, July 20, 2006

Rails realities part 16 (Finders and second order relations)

I recently needed to run a finder query and have the results sorted by a property on a second order relation. What the heck does that mean? This:

"Find me all Foos and sort the result by the 'name' property on the Baz object"


The documentation in rails shows you how to load a single related object but not how to load related objects of THAT object. A google of "rails find sort related" returned http://rails.techno-weenie.net/question/2006/1/27/dynamic_finders_sorting_by_a_related_object which is almost there.

The answer to the above problem is:

Foo.find(:all, :include => { :bar => :baz }, :order => 'baz.name')

The difference being that the :include parameter is a hash to a hash instead of just pointing to a single association.
Many thanks to technoweenie in #caboose. After getting the solution from it does appear somewhat obvious and the docs didn't appear to have anything, but upon further review there is something that shows an example of chaining includes on a finder query. In the "Table Aliasing" section of the ActiveRecord::Associations::ClassMethods API docs.

For extra credit technoweenie also pointed out that you could do the joins manually like so:

find :all, :joins => 'inner join bars on bars_id = bars.id inner join bazs on bazs.id = bars.bazs_id'


Note that you can also include more than one first order relation as follows:

Foo.find(:all, :include => [:bar, :biz], :order => 'biz.name')

And though I haven't tried it I suspect that from there you could include each of those relations' related objects in the same manner as described earlier.
Be warned that the queries produced are going to get ugly, but happy relation chaining :-)

Sunday, July 09, 2006

Bonfires on Ocean Beach

Today we take time out of our regular ruby on rails postings for a rant about bonfires on ocean beach.

For some reason the topic of bonfires on ocean beach has been brought to the forefront. My awareness began with this article in the Chronicle. After reading the article I was still not certain what position I held in the debate. I certainly am disgusted every time I head down to the beach and see the voluminous trash all over the beach, mostly as a result of bonfires the night before. On the other hand I've always enjoyed a good bonfire anywhere and especially at ocean beach on a nice calm evening. There's nothing like the intimacy you get being in such a beautiful environment and enjoying something as primal as sitting around the bonfire.

That said there's some level of respect and common sense required for this activity so as to not completely destroy the environment around you and allow others to enjoy the beach.

Today was a cleanup day on ocean beach sponsored by a local grocery store. Unfortunately it overlapped with the great world cup final but sometimes that's the price you pay for being a good samaritan. Anyway, we grabbed the dogs and headed over to where the group was meeting before heading down to ocean beach. Once down there we began picking up trash and doing our part to try and help keep ocean beach a nice place to enjoy.

I walked for about a half a mile picking up bottle caps, cigarette butts, and other debris with my trusty little trash picker upper. But then I finally got to the area of the beach where bonfires are allowed. It was right at the very edge of this area where I came upon my first bonfire remains. Now I've seen bonfire remains several times in the past, littered with broken bottles and what not and am always disgusted by the lack of respect, but today was quite different. Today I was actually there with a garbage bag, gloves, and trash picker upper thingy. It's a bit of a different mindset from just seeing it in passing as opposed to when you're there to actually clean stuff up.

I stood there and looked down at the pile of burnt wood, ashes, nails, and broken glass and saw a virtual "gold mine" of trash. Seeing as how my goal today was to cleanup trash I fancied myself being the "winner" of the day's "competition". Yes I know it wasn't a competition but in my mind I had just made it out to be. (See what happens when you don't get to watch the sporting event? You get competitive in other ways)

So I sat there for the next hour or so cleaning up the debris from just this single bonfire. Never moving to any of the others further down the beach. When I arrived at the bonfire site my bag was relatively light and empty as the other portion of the beach just didn't have much trash at all. But after cleaning up this section I ended up walking away with about 20lbs of trash. (Aimee saw how long I was at this spot and came back after awhile and was jealous of my "find" and declared that I was "winning". See, she's competitive too ;-)

Here are some pictures of the trash from just that bonfire that came from my bag.

IMG_1070.jpg

IMG_1071.jpg

IMG_1075.jpg


Now this is just the trash I was able to get. Aimee had some in her bag from this bonfire and I certainly didn't even come close to getting it all.

So can anyone guess where I might stand on the bonfire issue after getting up close and personal with the result of the activities? An outright ban may or may not be the solution but the jackasses that are dumb enough to burn palettes of wood with 100s of nails in them are ruining it for the folks that are conscious and mindful of not destroying the environment around them.

If some tenable solution isn't found due to budgetary restrictions then I'd be for an all out ban of this activity because the tradeoff of the debris in those photos is simply too high of a price to pay for such a non-essential activity.

From the article:


The Men's Circle has proposed selling clean firewood at the beach. The Park Service has available a stockpile of wood from nonnative trees cut down in the Marin Headlands as part of wildfire-prevention projects.

Others have suggested a permit system, but that would require additional personnel and resources -- something the Park Service doesn't have, Evenson said. One person even suggested that the Park Service offer a bounty for nails and staples picked out of the sand.

A ban against all fires at Ocean Beach, however, isn't the way to go, said Brian Burt of the Men's Circle.

"We understand that there are issues," he said. But when it comes to a bonfire, he said, "there's something sacred about it."


After reading that I'm wondering if I shouldn't save my 15-20 lbs of nails I collected today from that single bonfire and wait for the bounty :-) And speaking of sacred does Brian Burt really believe that the fires are more sacred than the beach? I hope not.

So if a permit system is too much of a strain on resources perhaps fire pits can be installed? I'm not sure if that would work either with the tide coming in and out but you might be able to go and just clean those pits up instead of having random spots around the beach with debris leftover from fires. As it is right now they bulldoze the beach every so often which ends up just stirring up all the broken glass, nails, and other debris.

In fact after thinking about it further if there isn't some permit system put in place that can hold someone responsible for the result of a bonfire there simply is no way to ensure that people won't continue to burn nails with wood and break glass in bonfires.

Also from the article, there's also a movement afoot to "save" ocean beach it is said. One where they vehemently oppose banning this activity.

More from the article:

"I'm a fifth-generation San Franciscan, and I feel that this is probably one of the best things about living in San Francisco, in the Bay Area," said Aaron Pava.

Pava, who runs an Internet consulting company, started a Web site two weeks ago -- www.saveoceanbeach.org -- to attract attention to the ban proposal. Since then, he's received more than 600 responses from 151 ZIP codes -- all opposed, he said.

Devotees of Burning Man, the annual weeklong festival of art, partying and chaos now held in Nevada's Black Rock Desert, received a "call to arms" edition of the project's Jack Rabbit Speaks newsletter Thursday soliciting comments against the ban.

"It's San Francisco's great fortune to have the prospect of the great Pacific before it," said Larry Harvey, Burning Man founder and the event's executive director. "To gather 'round the fire speaks of a communal impulse that's nothing but good."

Bonfires also are a part of some religious ceremonies. Reclaiming, a local pagan group, holds beach bonfires to celebrate the winter and summer solstices.

If bonfires are banned at Ocean Beach, group members are ready to protest the decision on First Amendment grounds, said member Susan "Kala" Levin.


So I don't know Aaron Pava (The irony of "saveoceanbeach.org" isn't lost on me) or Susan "Kala" Levin, but to them I say, look at the pictures I've posted and tell me that you think the activity of bonfires is more important than keeping that dangerous trash off of the beach. How is being a flat out proponent of bonfires at all costs going to "save" ocean beach exactly? Susan, why don't you tell beach goers that it's because of your "First amendment rights" that they, and everyone cannot safely walk barefoot on this beach without risking injury. To stand behind the first amendment is a crock. This has nothing to do with free speech. How about taking a look further down the bill of rights at the 9th amendment? Are you of the mindset that any law imposed upon you that's for the public welfare is a violation of the first amendment? Let's have a look at what the amendment says....

The 9th amendment

The enumeration in the Constitution, of certain rights, shall not be construed to deny or disparage others retained by the people.

The 9th amendment says basically this: "It makes the drafter's intent clear that the enumeration of certain rights in the body of the Constitution or in the Amendments thereto shall not be considered to be exhaustive or all inclusive, but rather illustrative."

So you see the rights of myself and others to walk on the beach without a nail sticking into my foot is not to be disparaged by your perceived right to go and litter the beach with boxes of nails and broken glass as a means of expressing yourself. In fact I'm pretty sure we don't even need the constitution to cite littering as being against the law. If you feel the need to express yourself in this way feel free to go down to the local hardware store, buy a box of nails, take some broken glass, and scatter that around your house or apartment. There, now you're exercising your first amendment rights without impugning upon mine or anyone else's inherent right to safely walk on the beach. And make no mistake about it, while not directly outlined in the constitution, we all certainly retain that right. I hope that we can all agree that the welfare of the people overrides freedom of speech. With that argument settled I'm dying to hear how it is that without a realistic solution anyone can be for allowing this activity to continue unabated as it does today....

Sunday, July 02, 2006

Rails realities part 15 (AJAX modal dialog)

I spent some time this past week looking at the various solutions out there for creating a modal dialog and while I was nearly able to make each of them work there were varying degrees of ugliness required.


First off my requirements:


I need to have a page containing a form that also contains a few different lists of user created items. Now I know that some will think that this might be a usability nightmare but in the flow of the user it makes sense and works. So imagine if you will:

< form>
(text fields, area)
list1
< a href>Add new thing to list1< /a>
list2
< a href>Add new thing to list2< /a>
list3
< a href>Add new thing to list3< /a>
(text fields)
submit
< /form>

Now there are some problems with the above requirement. Namely how do I get items into the list without taking the user to another page or doing a full page refresh? I can't create inline popup divs for each list to allow users to add new entries to each list because that requires a form tag and you can't have a form that lives inside of a form.

< form>
(text fields, area)
list1
< a href>Add new thing to list1
< div id='list1_dialog' style='display: none;'>
< ajax remote form id='dialog_form'>
fields...
submit button
< /form>
< /div>
...
< /form>


That leaves the options of having a div that lives outside of the form and is shown to the user for adding/editing items in the list.

Believe it or not accomplishing that is not an easy feat mainly because I want a modal dialog that will allow for adding/editing these lists without doing a page refresh. Luckily I was able to figure out a solution and while I'm sure all of the following modal dialog implementations will work I only got my solution working with just one, no doubt due to my own javascript deficiencies.

The 3 modal dialog contenders were:
  1. Submodal
  2. Prototype window class
  3. Lighbox gone wild

All of these implementations are just fine and I was able to get a modal dialog working with each of them in fairly short order.

That said, the implementation that I ended up using was lightbox gone wild.
(If you'd like to play along at home I'd suggest going to download LGW and follow along with the posting as it will make things clearer. Just put lightbox.js and lightbox.css in your /javascripts and /stylesheets directory of your rails app, refer to them in your .rhtml page where you'd like to pop the dialog from and you'll be set.)

LGW uses prototype in its implementation just like the prototype window class implementation, but it was considerably cleaner, smaller, and easier to understand. The whole implementation itself is just over 200 lines of javascript code and one stylesheet.

In fact if you don't need anything special you don't even need to write a lick of code to get the thing working.
The following is the minimum required HTML to get a modal dialog working with LGW.

< link rel="stylesheet" href="css/lightbox.css" type="text/css" />
< script type="text/javascript" src="scripts/lightbox.js">< /script>
< a href="text.html" class="lbOn">About Lightbox< /a>

This code creates a link to text.html. When the link is clicked lightbox will create a modal dialog that contains the contents of text.html

Inside of text.html you create a link to close the dialog...

< a href="#" class="lbAction" rel="deactivate">close< /a>

That's it!

Under the covers there's some cool prototype javascript hacks going on. Namely there are event listeners being setup for all of the elements that are tagged with 'lbAction' and 'lbOn'.

For items tagged with 'lbOn', when they are clicked a modal dialog is created and shown to the user.

For items tagged with 'lbAction' an event listener is created for click events on those items. The callback that is associated with the click is specified by the 'rel' attribute on the element. So in our example above the close link will have an eventlistener created on it that calls back to 'deactivate'.

Seems simple enough and elegant so I should be all set right? Unfortunately no. Now I may very well be missing something but I couldn't get things to work properly, in IE of course, without some trickery that I'll go over.

First my link in 'page.rhtml':

< %=link_to 'Add contact', {:action => 'add_contact_dialog'}, :class => 'lbOn'%>

That link triggers my dialog. In my dialog screen (dialog.rhtml) I have:

< %=form_remote_tag :html => {:id => 'add_contact_form', :name => 'add_contact_form'}, :url => {:action => 'add_contact'}%>
< %=submit_tag button%>< a href="#" class="lbAction" rel="deactivate">cancel< /a>
< %=end_form_tag%>

Submitting this form to 'add_contact' renders my RJS template (add_contact.rjs):

page.insert_html :bottom, 'ContactList', :partial => 'list_entry', :object => @contact, :locals => {:hide => true}
page.visual_effect :blind_down, @html_id
page.call 'add_lightbox', 'edit'+@html_id

What this template does is
  1. take the contact I created, create a list element for it with "edit" and "delete" links. The element is not shown to start because we...
  2. create a visual effect to make the element appear that indicates to the user that a new item was added to the list.
  3. create a lightbox listener for the "edit" link I created so clicking the link pops the dialog and loads this contact's information for editing.
The page.call is calling a javascript function that I created (add_lightbox) and added to lightbox.js, that takes the given DOM id and creates a listener in addition to some other things...

function add_lightbox(id) {
dismiss();
value = new lightbox($(id));
}

function dismiss() {
lightbox.prototype.deactivate();
if(browser == 'Internet Explorer') {
// this is here because lightbox doesn't scroll IE after dismissing after we post forms
window.scrollTo(0, lightbox.prototype.getYPos());
}
}

As you can see there's some extra stuff going on besides just adding a new lightbox to the element that was passed in. This is the critical point in the implementation where I could only get things working by doing things in this particular order.

There are three important points here:
  1. deactivate() must be called from the RJS template (In this instance it is called by dismiss()). It will *NOT* work in IE6 if you try to use a callback with the remote_form_tag, eg:

    < %=form_remote_tag :html => {:id => 'add_ref_form', :name => 'add_ref_form'}, :url => url, :complete => 'lightbox.prototype.deactivate();'%>

    Why? I have no idea. I tried other options as well, :loaded, :loading, nothing worked. Of course it works fine with Firefox and Safari. I didn't check IE7 because quite frankly it doesn't matter yet. I need IE6 to work.
  2. You must scroll IE after dismissing the dialog.
  3. You can only create the lightbox for the newly inserted item after the request is complete because the unique identifier for the edit link isn't available until after it's created. To be fair you could come up with another convention but why? It makes sense to use the ID of the thing you're editing.
IE and scrolling:
If you read the tutorial on the LGW page you'll see that they talk about the problem with IE not scrolling back to where it was and that they have code that fixes it. Unfortunately I couldn't get it to work whenever I performed an AJAX form submission. What I ended up doing was creating a class level variable in the lightbox implementation that I use to store the Y-axis position of the scrollbar (I don't care about the X-axis position because my page will never scroll horizontally). I doubt this is the optimal way to accomplish this but hey, it works.

Anyway, the modification of the lightbox.js code is as follows...
Add the function definition 'getYPos' to the prototype object in the lightbox.prototype declaration (hint: put this after the last function in the prototype declaration and put a ','
after the last function definition before pasting in the following code.)

getYPos: function() {
return lightbox.yPos;
}

Then modify the getScroll function as follows:

getScroll: function(){
if (self.pageYOffset) {
this.yPos = self.pageYOffset;
} else if (document.documentElement && document.documentElement.scrollTop){
this.yPos = document.documentElement.scrollTop;
} else if (document.body) {
this.yPos = document.body.scrollTop;
}
lightbox.yPos = this.yPos; <-- set the class level property
},

This modification sets the class variable yPos on lightbox whenever the dialog is activated and adds an accessor method to get the value.

So going back to the flow of the add_lightbox called from our RJS template we..
  1. dimiss the dialog
  2. scroll the window to the previous position if IE
  3. create a new lightbox listener for the newly added DOM element
That's it! (heh, not quite as simple as the first example but still not bad now that I've already suffered through the pain of figuring it all out)

The code to edit is very similar except that we don't add the lightbox listener we simply call our dismiss() function after making the edit and HTML update in our RJS template (edit_contact.rjs) as follow:

page.replace_html @html_id, @contact.to_s
page.call 'dismiss'
page.visual_effect :highlight, @html_id, :duration => 3


I hope this is clear to folks. I was thinking of either coming up with a patch, plugin, or submit this to the folks over at LGW and see if they can improve upon it. But for people that need a modal dialog that needs to contain a form and that will dismiss when you click "OK" as well as submit the form, this solution is pretty easy and will suit your needs.

Enjoy!

P.S. - If you liked this post how about a Digg?

Saturday, July 01, 2006

Rails realities part 14 (RTFM and don't work too late)

Sometimes I just don't learn that you can only be productive so many hours in a day. I wasted a good hour or two last night around 1AM simply because I worked for too long and didn't pay attention. If you don't want to read the entire post, the executive summary of today's post is this:

If you want your object deletion to cascade down your relationship hierarchy don't use :dependent => :delete_all on your has_many relations. And oh yeah, RTFM



I was running into behavior where objects that were more than one level deep in a relationship hierarchy weren't being destroyed when I destroyed the top level object. Turns out the option to :dependent I was using was the culprit but I didn't even think about it and ASSumed that it should cascade delete operations.

If I had paid attention to the API docs I would've seen:


:dependent - if set to :destroy all the associated objects are destroyed alongside this object by calling their destroy method. If set to :delete_all all associated objects are deleted without calling their destroy method. If set to :nullify all associated objects’ foreign keys are set to NULL without calling their save callbacks. NOTE: :dependent => true is deprecated and has been replaced with :dependent => :destroy. May not be set if :exclusively_dependent is also set.


I missed this and unfortunately my has_many relations specified the :delete_all flag since all instances are indeed dependent upon the parent object. I ended up creating a temporary workaround for a short period of time just to make sure that my objects would actually get deleted. Then I dug into the AR code to see what actually happens with the association definition with :dependent and sure enough at line 1000 of associations.rb in AR 1.14.2 I see...

case reflection.options[:dependent]
when :destroy, true
module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
when :delete_all
module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }"

This is where the callback is inserted into the parent class. The :delete_all option that I specified does just that, calls delete_all on the parent class. Reading the API docs for delete_all yields...


Deletes all the records that match the condition without instantiating the objects first (and hence not calling the destroy method).


Of course it was during when I started typing this entry the next morning that I actually saw the API doc for :delete_all, and I know that I've read it before but for some reason after a long day yesterday I just sort of zoned out.

Oh well, hopefully this post may help someone else who makes the same error with their has_many relations :-)

This page is powered by Blogger. Isn't yours?