# Complex has-many-through relationships and models

**URL:** <https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755>\
**Category:** Rails\
**Created:** [March 14, 2007, 2:36am UTC](https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755 "2007-03-14T02:36:16Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![PeteSalty](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/p/bc79bd/32.png) [@PeteSalty](https://www.ruby-forum.com/u/PeteSalty)\
**Post date:** [March 14, 2007, 2:36am UTC](https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755/1 "2007-03-14T02:36:16Z")

</div>

I have some questions about Has Many Through and how to manage a  
couple of things that keep cropping up. I’m reasonably new to rails,  
having come from a C# background and I have to admit I’m having some  
trouble getting my head around models. Anyway this is my second  
project in rails and it’s a little more complex than the first. I’m  
trying to do things the ‘rails way’ but I’m running into a few  
problems.

So, let me start off with some background. We have a few tables that  
enjoy various has-many (and has-many-thorugh) relationships.  
To start off with we have an ‘owners’ table (these are essentially  
users but we call them owners for various reasons)

Owners have the usual stuff, username, password, first\_name,  
last\_name, etc.

We also have ‘notebooks’. These notebooks have little information,  
just a name and are used to hold notes (duh!), but more on that later.

Owners have many notebooks through a table called  
‘notebookshares’ (man I hate that name). Notebookshares carry  
information about the owners relationship to the notebook,  
so on top of notebook\_id and owner\_id the notebookshares table  
contains a field called ‘access’. This field can be one of three  
values; ‘readonly’, ‘full’, ‘owner’. If it’s ‘owner’ then that user  
created the notebook and owns it and everything in it. They can then  
share notebooks they own with others, which is where ‘readonly’ and  
‘full’ come in. If I have ‘readonly’ access to a notebook then I’d  
somewhat restricted on what I can do, if I have ‘full’ access then I  
can do everything to the notebook but share it with others.  
Finally notebooks have many notes, but notes only belong to one  
notebook. Notes simply contain some text, the date/time they were  
created and last updated.

Essentially it all looks like this:

class Owner \< ActiveRecord::Base

has\_many :notebookshares, :dependent =\> true  
has\_many :notebooks, :through =\> :notebookshares do  
def find\_editable()  
find(:all, :conditions =\> “notebookshares.access \<\>  
‘readonly’”)  
end

```
def by_share_type(share_type)
  find(:all, :conditions => ['notebookshares.access = ?',

```

share\_type])  
end  
end

etc…

end

class Notebookshare \< ActiveRecord::Base

belongs\_to :owner  
belongs\_to :notebook

end

class Notebook \< ActiveRecord::Base

has\_many :notes, :order =\> ‘position’, :dependent =\> :destroy

has\_many :notebookshares, :dependent =\> true  
has\_many :owners, :through =\> :notebookshares do  
def find\_editable()  
find(:all, :conditions =\> “notebookshares.access \<\>  
‘readonly’”)  
end

```
def by_share_type(share_type)
  find(:all, :conditions => ['notebookshares.access = ?',

```

share\_type])  
end  
end

etc…

end

class Note \< ActiveRecord::Base

belongs\_to :notebook

acts\_as\_list :scope =\> :notebook

etc…

end

So, if I have either ‘owner’ or ‘full’ access to a notebook (as  
defined by the relationship between my owner\_id and the specific  
notebook\_id) then I can edit any given note in that notebook. If I  
only have ‘readonly’ I can view the notes but can’t really manipulate  
them in any way.

One of the things that keeps coming up is the ‘access’ level of a  
particular note or notebook. For example, if I’m trying to delete a  
note I need to know if I have the right to do so.  
For example something like this would be great (which would be found  
in the notes controller):

def destroy

```
@owner = Owner.find(session[:owner_id])

@note = @owner.notes.find(params[:id])

@note.destroy unless @note.access == 'readonly'

```

end

Similarly this information would be fantastic for notebooks, so  
something like

#where @owner is an Owner object that was retrieved previously  
if @owner.notebooks.find(params[:id]).access == ‘readonly’

```
blah...blah...blah

```

end

Now, I think it would have to be owner driven since the access an  
owner has on a notebook is what defines what they can do to the  
notebook and subsequently to the notes in that notebook.  
I suspect that we could also have something like

Notebook.find(params[:id]).access(@owner) since we would then know the  
owner.id and the notebook.id but I kind of like the first way better.

So, I have a few questions:

1. I know it’s possible to do something like  
@owner.notebook.notes.find(params[:id]) but is it possible to do  
something like @owner.notes.find(params[:id]) ?  
If it is I certainly haven’t been able to work out how - perhaps  
I’m missing something.

2. If it is possible to do 1 (or even if it isn’t and you have to do  
the longer and less elegant @owner.notebook.notes.find(params[:id]))  
is it possible to add a function to the note model that would return  
the access for that given note, or even add an accessor that get  
populated after the note is retrieved.

I suspect that the difficulty would be in determining if the owner was  
present, because it would also be possible to do something like  
Notes.find(params[:id]) which means that we wouldn’t know who was  
looking for the note and therefore what kind of access level they had.  
Perhaps in this case the access function would return nil.

1. Similarly for 2 above how would you go about writing a function for  
Notebook that returned the access level for the supplied owner using  
something like

Notebook.find(params[:id]).access(@owner)

and similarly could something like

@owner.notebooks.find(:all)

prepopulate an access accessor for the notebook such that we could do  
something like

foreach @owner.notebook.find(:all) do |notebook|

```
 puts notebook.name unless notebook.access == 'readonly'

```

end

Thanks for any help on this subject - even a link to something similar  
would be appreciated.

---

<div class="post-metadata">

**Author:** ![PeteSalty](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/p/bc79bd/32.png) [@PeteSalty](https://www.ruby-forum.com/u/PeteSalty)\
**Post date:** [March 14, 2007, 8:57pm UTC](https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755/2 "2007-03-14T20:57:08Z")

</div>

Actually on further examination we can’t even do something like

@owner.notebooks.notes.find(params[:id])

is this right? Seems odd to me.

---

<div class="post-metadata">

**Author:** ![PeteSalty](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/p/bc79bd/32.png) [@PeteSalty](https://www.ruby-forum.com/u/PeteSalty)\
**Post date:** [March 14, 2007, 9:34pm UTC](https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755/3 "2007-03-14T21:34:14Z")

</div>

So, there’s a lot of questions here… I’ll actually address the last  
one  
first, because it’s kind of easy:

When you do @owner.notebooks, it returns a collection of Notebook  
objects.  
So, when you do @owner.notebooks.notes.find(:all) or whatever, you’re  
actually trying to call the notes function on the collection, as opposed  
to  
the underlying objects themselves. You’ll either have to iterate over  
them,  
or do something like  
class Owner \< ActiveRecord::Base  
def notes  
self.notebooks.collect(&:notes)  
end  
end

Then, @owner.notes will return all the notes for all notebooks.

Now, on to some of the other stuff:

For #1 in your first email, this (I think) does exactly what you want.  
For #2 in your first email, I going to make an assumption: I believe  
from  
what I read that notes don’t have individual access, they only have  
access  
based on the owners access to the notebook they are in.  
class Note \< ActiveRecord::Base  
def has\_access?(owner)  
self.notebook.has\_access?(owner)  
end  
end  
class Notebook \< ActiveRecord::Base  
def has\_access?(owner)  
return nil if owner.nil?  
begin  
return Notebookshare.find\_by\_owner\_id\_and\_notebook\_id(owner.id,  
self.id).access != ‘read-only’  
rescue ActiveRecord::RecordNotFound  
return nil  
end  
end  
end

Tell me if I’ve answered all your questions, I’m too lazy to read  
through  
the whole thing again and look 😛

---

<div class="post-metadata">

**Author:** ![PeteSalty](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/p/bc79bd/32.png) [@PeteSalty](https://www.ruby-forum.com/u/PeteSalty)\
**Post date:** [March 14, 2007, 10:14pm UTC](https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755/4 "2007-03-14T22:14:24Z")

</div>

Luke,

Thanks for the response. Super helpful, but I have some more questions  
(isn’t that always the way)

would

class Owner \< ActiveRecord::Base  
def notes  
self.notebooks.collect(&:notes)  
end  
end

only return notes for the notebooks that the owner has access to (I  
suspect that it would). I’m unfamiliar with the terminology of  
&:notes, although I get the idea of running collect on each elelment  
of the noebooks array.

The other 2 functions that you provided (has\_access? for both notes  
and notebooks) would certainly work but I was also wondering if there  
was some way of prepopulating an accessor when something like

@owner.notebooks.find(:all)

was run, so that rather than having to call has\_access on each  
notebook you could just call it’s ‘access’ accessor

perhaps something like:

class Notebook \< ActiveRecord::Base

after\_filter :set\_access

attr\_accessor :access

has\_many :notes, :order =\> ‘position’, :dependent =\> :destroy

has\_many :notebookshares, :dependent =\> true  
has\_many :owners, :through =\> :notebookshares do  
def find\_editable()  
find(:all, :conditions =\> “notebookshares.access \<\>  
‘readonly’”)  
end

```
def by_share_type(share_type)
  find(:all, :conditions => ['notebookshares.access = ?',

```

share\_type])  
end  
end

def set\_access

```
self.access = <this is where I get stuck>

```

end

etc…

end

where I get stuck is in set\_access. Is there a way to determine if the  
notebook has been retrieved using something like:

@owner.notebooks.find(:all)

and hence use the calling @owner object to determine the access. I  
realize that Notebook.find(1) would have to return nil in access  
because we wouldn’t know which owner we’re looking at the access for  
and then the has\_access? function would come into play.

Hope this isn’t too confusing.  
Once again, thanks for the help.

Dale

---

<div class="post-metadata">

**Author:** ![PeteSalty](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/p/bc79bd/32.png) [@PeteSalty](https://www.ruby-forum.com/u/PeteSalty)\
**Post date:** [March 16, 2007, 3:20pm UTC](https://www.ruby-forum.com/t/complex-has-many-through-relationships-and-models/88755/5 "2007-03-16T15:20:34Z")

</div>

> of the noebooks array.  
> What I wrote is exactly the same as self.notebooks.collect { |notebook|  
> notebook.notes }… basically, takes each notebook in turn and queries  
> for  
> their notes… this would only work for notebooks that have been  
> cross-referenced through notebookshares, whatever kind of access that  
> is  
> denoting (read-only, whatever). If you want to specify a certain type  
> of  
> access, you can do  
> self.notebooks.collect {|notebook| notebook.notes if  
> notebook.has\_access?  
> (self)}

More later… gotta run.
