has_many :through - zmagania początkującego

Witam ponownie :wink:
Cały czas walczę ze swoją aplikacją, starając się samemu dojść do rozwiązań, ale nie zawsze wychodzi :wink: Tym razem chciałem się upewnić czy mój tok rozumowania jest dobry, i zapytać jak rozwiązać jeden problem.
W mojej aplikacji (ma to być prosta aplikacja do zarządzania projektami)
jest tak:

[code=ruby]class Project < ActiveRecord::Base
has_many :tasks
end

class User < ActiveRecord::Base
acts_as_authentic
belongs_to :role
has_many :todos
has_many :obligations
has_many :tasks, :through => :obligations
end

class Task < ActiveRecord::Base
belongs_to :project
has_many :obligations
has_many :users, :through => :obligations
end

class Obligation < ActiveRecord::Base
belongs_to :user
belongs_to :task
end[/code]
czyli każdy projekt ma wiele zadań, a każde zadanie może posiadać wielu użytkowników. Zastanawiam się cały czas czy dobrze użyłem do tego zadania has_many :through. Generalnie zamysł jest taki, że po założeniu nowego projektu, user z uprawnieniami menadżera dzieli projekt na mniejsze zadania, i każde z nich przypisuje jednemu, lub większej ilości użytkowników. Do tego później Ci użytkownicy mogą opisać swoje zadanie, dlatego użyłem has_many :through zamiast HABTM, aby mieć większe możliwości i móc dodać do Obligations(lepszego określenia nie wymyśliłem na tą relację) więcej danych.

Problem mój w tym, że o ile google tonie w tutorialach i opisach mówiących jak stworzyć relacje has_many :through, tak nie wiele jest o tym jak z niej później korzystać.
Na razie wydaje mi się że robię to dosyć na około.
Moje listowanie zadań i przypisanych im użytkowników woła pewnie o pomstę do nieba :wink:

[code=ruby] <% @obligation.each do |ob| %>


<%= Task.find(ob.task_id).title %>
<%= User.find(ob.user_id).username %> <% end %>[/code] Problem w tym że staram się listować wszystkie zadania należące do danego projektu i mające przypisanego użytkownika, a kod powyższy oczywiście wypisuje wszystko co się da, a nie wiem jak ugryźć to, żeby Task było ograniczone tylko do tych które należą do aktualnie przeglądanego projektu.

Przy okazji, jak później sobie radzić z tworzeniem takich przypisań, kiedy będę mógł wybrać wielu użytkowników do jednego zadania. Nie mam nawet pomysłu teraz jak to ugryźć
Za wszelką pomoc z góry serdecznie dziękuje.
pozdrawiam

<% @obligation.each do |ob| %>
  <%= ob.user.username %>
  <%= ob.task.title %>
<% end %>
<% @project.tasks.select {|task| task.users != [] }.each do |task| %>
  <%= task.name %>
<% end %>

Pisane z palca, więc może być coś nie tak.

dzięki za uwagę odnośnie pierwszego kodu :wink:
drugi niestety nie zwraca nic :wink: i nie bardzo go rozumiem, w każdym razie przedstawię jak to według mnie powinno być.
Wydaję mi się, że najpierw powinienem pobrać id tasków które należą do aktualnie przeglądanego projektu, a następnie wypisać wszystkich userów przypisanych do tych zadań przez tablice obligations.
problem w tym że nie umiem tego poprawnie przetłumaczyć na railsy :wink:

Spróbuj tak:

<% @project.tasks.each do |task| %>
  <%= task.name %>
  <% task.users.each do |user| %>
    <%= user.username %>
  <% end %>
<% end %>

dzięki :slight_smile: już załapałem dzięki Tobie jak to działa :wink:

pozwolę sobie odświeżyć temat, bo moje pytanie jest niejako kontynuacją.

Na screenie jest widok pojedynczego projektu, gdzie wylistowane są taski i przypisani do nich użytkownicy. Co chciałbym osiągnąć, to dopisanie kolejnego rekordu w Obligations, po wybraniu z dropdown użytkownika i klinięciu Dodaj. Chciałbym żeby po zapisaniu aplikacja powróciła do tej samej strony projektu.
I nie wiem, czy mogę napisać po prostu kod który zapisze mi ID taska i ID usera jako nowy rekord w Obligations, czy stworzyć jakiś kontroler osobny i akcje do tego, czy może są jakieś inne magiczne metody…

@task.users << User.find(params[:user_id])
redirect_to tasks_url

Coś w tym guście

dzięki, udało mi się to zrobić, tylko teraz mam kolejne wyzwanie które mnie przerosło, mianowicie z dropdowna z listą użytkowników chciałby pozbyć się tych którzy już zostali dopisani do danego zadania.
Select mam tak:

<%= select_tag :user_id, options_from_collection_for_select(@user, 'id', 'username') %>

próbowałem w też w konsoli jakoś to wykombinować używająć :conditions ale poległem.
Byłbym wdzięczny za jakąś wskazówkę.

Zamiast próbować pozbyć się przypisanych już użytkowników z , wypróbuj inne podejście - pobierz z bazy danych tylko tych użytkowników, którzy nie są jeszcze przypisani do tego zadania. Jeśli wiesz jak to zrobić w SQLu to nie powinieneś mieć już problemu z odpowiednim użyciem User.find. LEFT OUTER JOIN będzie bardzo pomocny w napisaniu tego zapytania.

udało się :wink: dzięki

<%= select_tag :user_id, options_from_collection_for_select(User.find_by_sql("SELECT * FROM users LEFT OUTER JOIN obligations ON users.id = obligations.user_id WHERE obligations.task_id != '#{task.id}' "), 'id', 'username') %>

EDIT:
chyba za wcześnie się pochwaliłem…

Po bliższym przyjrzeniu się problemowi - LEFT JOIN nie wystarczy. Potrzebujesz podzapytania:

[code Ruby]# kontroler (warto jednak przenieść do modelu Task - ułatwi pisanie testów)
@users = User.all(:conditions => [‘NOT EXISTS (SELECT * FROM obligations
WHERE obligations.user_id = users.id AND obligations.task_id = ?)’, task.id])

widok

<%= select_tag :user_id, options_from_collection_for_select(@users), ‘id’, ‘username’) %>[/code]

Dzięki wielkie! :slight_smile:

Albo:

# model
def self.not_assigned(task)
  User.all(:include => :tasks).reject{ |user| user.tasks.include?(task) }
end

# kontroler
@task = Task.find(params[:task_id])
@users = User.not_assigned(@task)

# widok się nie zmienia

Pisane z palca, więc coś może nie grać. Pewnie wolniejsze niż rozwiązanie hekto5, ale jak ktoś się boi sqla, to może czegoś takiego użyć (chociaż sqlowe rozwianie nie jest trudne w zrozumieniu).