current_user w modelu

Aplikacja, którą akurat tworzę wymaga aby logi zapisywane były w bazie danych. Stworzyłem zatem model Log i w nim metodę Log#create. I ten element działa. Działa pod warunkiem że w wywołaniu tej metody podam obiekt klasy User jako “wykonawcę” logowanej czynności. Wykonanie tego z poziomu kontrolera nie stanowi problemu np.

class ArticlesController < ApplicationController (...) def create (...) Log.create({who: current_user, action: :create, element: @article, description: "(#{history})"}) (...) end (...) end
Problem w tym, że niektóre operacje wykonywane na artykułach nie przechodzą przez kontroler. Dobrze by było aby Log#create wywoływane było w callback-u after_save modelu Article. Tyle że model nic nie wie o sesjach i nie zna metody current_user.

Jak podejść do rozwiązania tego problemu?

może napisz metodę klasową dla Usera która będzie pobierała current_user i zapisywała do loga, i tę metodę wywołuj callbackiem?

Dodaj to do ApplicationController:

[code] before_filter :set_current_user

def set_current_user
User.current = current_user
end[/code]
w modelu user dodajesz:

[code] def self.current
Thread.current[:user]
end

def self.current=(user)
Thread.current[:user] = user
end[/code]
i w modelach mozesz juz sie odwolywac do obecnego usera np. tak:

User.current.id

Używanie current_user w modelach to droga do otchłani piekielnych. Ustawianie User.current jako globalnej zmiennej w ApplicationController to tylko proteza pozwalająca Ci szybciej tą drogą schodzić.

Wyobraź sobie, że dopisujesz sobie jakiś rake task, który działa na modelach. Wtedy User.current nie będzie ustawione, bo żaden controller się nie odpala (i nie ma żadnej sesji) i wszędzie gdzie chciałeś mieć użytkownika masz nile.

Co do Twojego konkretnego przypadku,to jest trywialny. Rozumiem, że chcesz zalogować stworzenie jakiegoś Article. Możesz wtedy z łatwości wykonać Log.create na after_save i przekazać tam who: self.created_by.

[quote=mits] Thread.current[:user]
[/quote]
O wiele bezpieczniej jest używać https://github.com/steveklabnik/request_store, a pozatym to tak jak @sharnik pisał

Zgadzam się z tym i dlatego pytam jak ten problem rozwiązać.

Jakoś nie widzę tej trywialności. Skąd metoda #created_by bo przeszukałem wszystkie gemy z Rails 4 i nie znalazłem takiej metody. Article.methods ani Article.first.methods również nie pokazują takiej metody. Oczywiście mogę napisać taką metodę zakładając, że obiekt jest w jakiś sposób powiązany z jego twórcą np. Article posiada atrybut user_id.
Zresztą to i tak nie rozwiązuje problemu bo przecież artykuł może być aktualizowany nie tylko przez jego twórcę (np. “moderator” poprawia “redaktora”) i taką aktualizację również muszę “zalogować”.

W ilu miejscach robisz update artykułu wykonany przez usera? Podejrzewam, że w maksymalnie kilku, dlatego lepiej zrób to w kontrolerze.

Albo w kotrolerze

a = Article.new a.editor = current_user a.save
A w modelu

[code] attr_accessor :editor

after_create :log_costam

private

def log_costam
Log.create({who: editor, action: :create, element: self, description: “(#{history})”})
end[/code]

Ja tam jakoś nie uważam żeby to było złom jak nóż, można uzyć do przygotowania jedzenia, albo kogoś zadźgać :smiley:

No więc najprostszym rozwiązaniem jest:

[code]class ApplicationController
before_filter :assign_user

def assign_user
Thread.current[:user] = get_user_from_session_or_something
end
end[/code]
w modelu np.

class Post def before_update self.modified_by = Thread.current[:user] end end
W przypadku rake tasków moja ulubiona metoda to stworzyć użytkownika o loginie “robot” - czasami kilka takich w zależności od funkcji wtedy:

[code]task :set_user do
Thread.current[:user] = find_a_robot
end

task :do_something => [:environment, :set_user] do
end[/code]
Takie podejście pozwala mocno wysuszyć (DRY) wiele rzeczy związanych z audytem (logowaniem). W każdej wersji Ruby od 2.0 do 4.0 uzycie Thread.current jako “request store” działa i raczej działać będzie nadal biorąc pod uwagę jak wiele gemów z tego korzysta.

A motyw ze specjalnymi uzytkownikami to jest warjacja wzorca “Null Object” - http://sourcemaking.com/refactoring/introduce-null-object znacznie upraszcza kod, bo uzytkownik zawsze jest - nawet jeśli przedstawia się jako “anonymous”, albo “robot”, oszczędza się na ifach.

Ps. Lol. Nawet przywołany gem używa Thread current - https://github.com/steveklabnik/request_store/blob/master/lib/request_store.rb

Tak, ale czyści się co request więc jest bezpieczniejszy.

W ilu miejscach robisz update artykułu wykonany przez usera? Podejrzewam, że w maksymalnie kilku, dlatego lepiej zrób to w kontrolerze.[/quote]
W opisie problemu podałem Article bo najbardziej oczywisty przypadek ale dotyczy to również innych modeli: Folder, Category, Auction, Gallery. O ile artykuły, foldery, kategorie i galerie stanowią pewną całość to przetargi już nie. Na przetarg składają się dane takie jak typ czy cena ale również dokumenty którymi są artykuły. Modyfikacje przetargu wprowadzają również zmiany do artykułów. A w planach są jeszcze inne “obiekty” korzystające z artykułów. Jak widać miejsc w których zapisywane są logi jest całkiem sporo i będzie jeszcze więcej. Dlatego zapis logów chciałem przenieść do modelu tym bardziej, że logi dotyczą właśnie modeli.

Metoda z wykorzystaniem Thread.current wydaje się całkiem ciekawa zwłaszcza, że w aplikacji wykorzystuję paper_trail i tam właśnie w ten sposób określany jest wykonawca zmian w dokumencie. Zastanawiam się również nad zapisaniem b/p w klasie Log nazwy użytkownika z poziomu ApplicationController-a. Trochę obawiam się co się będzie działo pomiędzy kolejnymi requestami i oczywiście problem wywołań przez rake.

Pojawił się jednak kolejny problem. W logach powinien się znajdować link do “rejestru zmian”. W tej chwili wygląda to tak:

history = view_context.link_to(t("common.History"),article_version_path(@article.versions.last)) Log.create({who: current_user, action: :update, element: @article, description: "(#{history})"})
Kolejny element, którego nie zrealizuję w modelu.

Cały czas mam wrażenie, że zabieram się do tego problemu ze złej strony.

[quote=Tuptus]Jakoś nie widzę tej trywialności. Skąd metoda #created_by bo przeszukałem wszystkie gemy z Rails 4 i nie znalazłem takiej metody. Article.methods ani Article.first.methods również nie pokazują takiej metody. Oczywiście mogę napisać taką metodę zakładając, że obiekt jest w jakiś sposób powiązany z jego twórcą np. Article posiada atrybut user_id.
Zresztą to i tak nie rozwiązuje problemu bo przecież artykuł może być aktualizowany nie tylko przez jego twórcę (np. “moderator” poprawia “redaktora”) i taką aktualizację również muszę “zalogować”.[/quote]
Masz rację, to created_by to było uproszczenie. Zakładałem, że możesz wykonać taką metodę article.created_by i zwróci autora.

Jak pisałem wcześniej, trzymanie się w ogólnym rozwiązaniu jakiejś zmiennej globalnej current_user (niezależnie jak ustawiana) to jest kiepski pomysł. Po drugie, logowanie to nie jest odpowiedzialność modeli, bo one odpowiadają za zapisywanie w bazie danych, a nie za logikę biznesową.
Moim zdaniem osiągasz poziom skomplikowania aplikacji, w którym warto wprowadzić services, które będą zajmować się właśnie logiką biznesową. Czyli tworzysz artykuł wywołaniem jakiegoś ArticleService gdzie explicite przekazujesz użytkownika i on już wywołuje odpowiednie metody na Article, żeby ten artykuł stworzyć i również ten service wywołuje Log.create z tym samym użytkownikiem explicite przekazanym.

Zarówno rozwiązanie z RequestStore, jak i Thread.current będą działać, ale mają one sens tylko przy nieskomplikowanej aplikacji. Jeśli będzie ona dalej rozwijana i zależy Ci na jakości kodu, to będziesz musiał rozdzielić logikę biznesową od modeli.

Wyglada dla mnie ze chcesz tak na prawde miec wersjonowanie, a wiec https://www.ruby-toolbox.com/categories/Active_Record_Versioning, albo jak uzywam MongoID to masz to wbudowane http://mongoid.org/en/mongoid/docs/extras.html#versioning

Wersjonowanie to inna sprawa i w zasadzie jestem zadowolony z efektu - korzystam z paper_trail i zmodyfikowanej wersji htmldiff. Jednak nie wszystko zapisuję w wersjonowaniu. Przykładowo akceptacja artykułu przez moderatora nie modyfikuje treści więc nie zapisuję tego w “rejestrze zmian” ale w logach muszę uwzględnić.

Też o tym myślałem. Takie rozwiązanie poznałem “studiując” ZF+Doctrine i prawdę mówiąc przeraziła mnie ilość dodatkowej pracy. Moja aplikacja nie jest aż tak ogromna. Ale idąc tym tropem zastanawiam się nad nadpisaniem w modelach metod #save! i #update np:

class Article < ActiveRecord::Base (...) def save!(current_user,description,*args) super(args) Log.create ... end end
To jest oczywiście skrót bo logowanie powinno nastąpić tylko przy powodzeniu zapisu. Może nawet te dwie metody zapisać do jakiegoś modułu i potem go tylko includować do modeli. Co myślicie o takim rozwiązaniu?

A nie można po prostu tego Log.create wrzucić do obserwera żeby nie komplikować modelu ? Stworzyć sobie np LoggableObserver i w nim observe :article, :post itd… jeśli jest więcej tych modeli do logowania

Rails 4 nie ma observerów i niech tak zostanie.

Abstrahując od observerów railsowych, których istotnie już nie ma defaultowo, to wzorzec projektowy Observer jak najbardziej mógłby służyć uporządkowaniu kodu w tym przypadku.

Rails 4 nie ma observerów i niech tak zostanie.[/quote]
Rails 4 nie ma też warstwy services ani np. presenterów. To powód, żeby z tego nie korzystać?

Ale observers zaciemniają kod, rozpraszają logikę po aplikacji i ogólnie są narzędziem Szatana. Taka różnica.

Bez popadania w przesadę. Akurat do logowania czy logiki zupełnie oddzielnej Observery są jak znalazł.