1. State machine w Railsach
  2. Gem: Mailcatcher
  3. „Małe, a cieszy”, czyli różne praktyczne Railsowo-Ruby’owe tricki

1. State machine w Railsach

  1. Vaidehi: A Machine State of Mind, Part 1
  2. Vaidehi: A Machine State of Mind, Part 2
  3. Sitepoint: State Machines in Ruby
  4. Github AASM
  5. Github: State Machine

Czym jest maszyna stanów?

*…a dokładniej skończona maszyna stanów

Zanim zastanowimy się jak się nimi posługiwać – musimy zrozumieć czym są i czemu służą.

Działanie maszyny stanów jest bardzo często porównywane do schematów blokowych. Obserwujemy przechodzenie po kolejnych etapach (stanach) w zależności od zdarzeń jakie mają miejsce, czasami pod pewnymi warunkami.

Wyobraźmy to sobie na najprostszym przykładzie procesu zamawiania.

  1. Składamy zamówienie (zakładamy, że to jest ten „zobowiązujący” moment zakupu)
  2. Wypełniamy formularz dostawy
  3. Dokonujemy płatności
  4. Płatność zostaje potwierdzona
  5. Wysyłamy przedmiot LUB zamawiający rezygnuje z zamówienia
  6. Przesyłka zostaje dostarczona

Każde z tych wydarzeń powoduje, że zamówienie przechodzi do innego stanu

Wiemy z jakiego stanu możemy przejść do jakiego – ze stanu potwierdzona_płatność możemy pod wpływem wydarzenia wysyłka przejść do stanu wysłane

Nie ma jednak możliwości, żebyśmy ze stanu wypełniony_formularz nagle przeskoczyli do wysłane

Pamiętajmy też, że przejścia pomiędzy stanami nie muszą być jednokierunkowe, to znaczy pod wpływem jakiejś akcji, np przy negatywnym potwierdzeniu płatności – z powrotem przeniesiemy zamówienie do stanu, w którym oczekuje płatności

Mamy więc jakiś skończony zbiór stanów w jakich nasz obiekt może się znajdywać, oraz wydarzenia które powodują przejście z jednego stanu w inny.

State machine w Railsach

Jak moglibyśmy zaimplementować maszynę stanów w Railsach?

Wydaje się, że powinniśmy stworzyć kolumnę state (String) na naszym obiekcie klasy Order, nadać jej defaultową wartość, a następnie stworzyć metody instancyjne, których wywołanie zmienia wartość pola state.

Po pierwsze – dużo kodu w modelu (fat models?! tylko nie to, walczymy z tym już od kilku postów!)
Po drugie – każdy warunek dotyczący tranzycji, callbacki… – coraz więcej kodu.
…i jeszcze więcej jeżeli chcielibyśmy mieć metody sprawdzające czy zamówienie jest w danym stanie, np .sent?

Na szczęście – z pomocą przychodzą gemy: AASM ( acts_as_state_machine ) oraz  state_machine plugin

Biorąc pod uwagę, że…

state_machine Github screen:

state_machine

aasm Github screen:

aasm

Przyjrzę się uważniej AASM

Implementacja AASM w Railsach

  • Zaczynamy tradycyjnie od dodania odpowiedniego gemu do pliku Gemfile
  • Następnie, dla modelu, którego obiekty maja być powiązane z maszyną stanów – musimy dodać kolumnę, w której będziemy przechowywali (w postaci stringaobecny stan – domyślnie jest to kolumna aasm_state
rails g migration AddAasmStateToOrder aasm_state:string
rake db:migrate

 

  • Kolejnym krokiem jest załączenie AASM do odpowiedniego modelu
class Order
  include AASM
end

I już? tak, i już! możemy korzystać w modelu z maszyny stanów.

  • Zacznijmy od zdefiniowania listy stanów w jakich nasz obiekt może się znajdować, wskazując, który ze stanów jest startowy
class Order
  include AASM

  aasm do
    state :placed, initial: true
    state :form_filled
    state :paid
    state :payment_confirmed
    state :sent
    state :received
    state :canceled
  end

end

Tworząc teraz nowy Order możemy sprawdzić w jakim znajduje się stanie

order = Order.create({...})
order.aasm.current_state
#=> :placed

 

  • Teraz musimy zdefiniować tranzycje, czyli przejścia pomiędzy stanami oraz wydarzenia, które te zmiany powodują
class Order
  include AASM

  aasm do
    state :placed, initial: true
    #...

    event :fill_form do
      transitions from: :placed, to: :form_filled
    end

    event :pay do
      transitions from: :form_filled, to: :paid
    end
  end

end

Sprawdźmy co nam to daje:

order.fill_form
#=> true
order.aasm.current_state
#=> :form_filled
order.form_filled?
#=> true
order.paid?
#=> false
order2 = Order.new(...)
order2.pay
#=> AASM::InvalidTransition: AASM::InvalidTransition
orde2.may_pay?

Jak widać dzięki AASM dysponujemy metodami, które wywołane na obiekcie powodują zmianę jego stanu : eventy

W każdej chwili możemy nie tylko sprawdzić obecny stan, ale też sprawdzić czy obiekt znajduje się w danym stanie

Co się natomiast stało w ostatniej linijce?
Próbowaliśmy „zapłacić” za zamówienie, którego formularza jeszcze nie wypełniliśmy – wykonać zdarzenie pay na obiekcie który jest w stanie placed

Efekt? Maszyna stanów nas zatrzymała ponieważ to wydarzenie powoduje przejście jedynie ze stanu form_filled do paid!

Co więcej w każdej chwili możemy sprawdzić czy na danym obiekcie może zostać wywołane wydarzenie

  • Co jeszcze podrzuca nam AASM?

Znamy już najbardziej podstawowe działanie maszyny stanów. Jakie „triki” warto jeszcze znać?

Event może dotyczyć kilku stanów

aasm do
  #...

  event :cancel do
    transition from: [:placed, :form_filled, :paid], to: :cancelled
  end
end

 

Tranzycje mogą być warunkowe i to nie tylko w zależności od stanu w jakim znajduje się obiekt: guard, guards, if, unless

class Order
  include AASM

  aasm do
    #...

    event :send do
      transition from: :payment_confirmed, to: :sent, guard: :in_stock?
    end
    # OR
    event :send do
      transition from: :payment_confirmed, to: :sent, if: :in_stock?
    end
    # OR
    event :send do
      transition from: :payment_confirmed, to: :sent, unless: :out_of_stock?
    end

  end

  def in_stock?
    #check if product is in stock
  end

  def out_of_stock?
    #check if product is out of stock
  end
end

Oprócz tego AASM umożliwia nam ustawienie callbacków, a więc funkcji, które mają zostać wykonane przed/po tranzycji i róże inne udogodnienia, takie jak np. false zamiast wyjątku przy nieprawidłowej tranzycji, czy zmiana domyślnej nazwy kolumny, co może się przydać, jeżeli jak dotąd posługiwaliśmy się np. kolumną status do przechowywania informacji o stanie obiektu.

Podsumowując – AASM jest bardzo prostym narzędziem, które pozwala na implementację funkcjonalności, która wydaje się niezbędna dla większości aplikacji.

 

2. Gem: Mailcatcher

  1. MailCatcher – oficjalna strona
  2. RichOnRails: Debugging emails with Mailcatcher

Gem bardzo prostu, ale jakże ułatwiający życie!

MailCatcher pozwala na „przechwytywanie” wysyłanych maili w trybie development, a tym samym kontrolę tego
– czy zostały wysłane
– jak są sformatowanie
– jak wyglądają ich nagłówki, tematy odbiorcy

…jednym słowem – na pełnowymiarowe testowanie mailingu naszej aplikacji.

Tak naprawdę jest to program – serwer SMTP, który oferuje nam fajny interfejs do przeglądania wysyłanych z aplikacji wiadomości.

Korzystanie z niego i konfiguracja są bardzo proste:

  • dodajemy odpowiedni gem do Gemfile’a + bundle install
  • odpalamy deamona (zadanie działające w tle) MailCatchera
    bundle exec mailcatcher
    

    W tym momencie – mamy dostępny podgląd naszego serwera mailowego pod adresem
    localhost:1080

     

  • Ostatnią rzeczą jaka jest konieczna, aby móc korzystać z MailCatchera jest konfiguracja mailingu w ramach naszej railsowej apki
    #config/environments/development.rb:
    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
    

Voila!

mail_catcher.png

 

3. „Małe, a cieszy”, czyli różne praktyczne Railsowo-Ruby’owe tricki

Na koniec chciałabym napisać o kilku ciekawostkach związanych z RoR, na które udało mi się trafić ostatnimi czasy

  • cachowanie w zmiennej instancyjnej
  • def my_var
      @my_var ||= ...
    end
    

     
    Nie! To jeszcze nie ciekawostka🙂 Większość z nas zna ten „myk”

    „Sprawdź czy @my_var ma ustawioną wartość. Jeżeli nie – podstaw prawą stronę równania. Jeżeli tak – zwróć jej obecną wartość”

    Zawsze wydawało mi się to oczywiste.

    Dopiero niedawno zdałam sobie sprawę, że tak jak działa
     

    x += 1
    #to to samo co
    x = x + 1
    

    Tak tutaj
     

    x ||= func()
    #to to samo co
    x = x || func()
    

    Wydawałoby się, że to co piszę jest kompletnie nie-odkrywcze. Ale warto zwrócić uwagę, że oznacza to, że nie cachujemy w takim razie wartości nil ani false!

  • .tap(&:save)
  • Bardziej Railsowo: w sytuacji, kiedy potrzebujemy zapisać obiekt ale zamiast „wrócić” z wartością true wolelibyśmy dostać utworzony obiekt

    my_var = my_object.save
    #=> true
    my_var = my_object.tap(&:save)
    #=> MyObject...
    

     

  • define_method
  •  

    Wyobraźmy sobie, że mamy obiekt w bazie, które może mieć jeden z pięciu typów: a, b, c, d lub e.
    To jakiego jest typu – gdzieś w kodzie -ma znaczenie.
    Fajnie byłoby mieć metodę

    my_object_instance.a?
    #=> true/false
    my_object_instance.e?
    #=> true/false
    

    Jednak napisanie w modelu 5 metod właściwie identycznych… jest bez sensu!
    Spójrzmy co daje nam define_method

    class MyClass
      KINDS = [:a, :b, :c, :d, :e]
    
      KINDS.each do |kind|
        define_method "#{kind}?" do
          self.kind == kind
        end
      end
    end
    
     

  • aliasy w form objectach
  •  

    Wykorzystując gem Virtus do tworzenia obiektów formularza możemy skorzystać z aliasów do pól

    Posługując się aliasami możemy (zachowując „bazowe” nazewnictwo pól dla atrubytów) odwoływać się do tychże inną nazwą, na przykład

    class MyNewForm < ::BaseForm
    
      attribute :my_attr, String
     
      alias :moj_atrybut :my_attr
      alias :moj_atrybut= :my_attr=
    end
    
    f = MyNewForm.new
    f.attributes
    #=> {:my_attr => nil}
    f.moj_atrybut = "cos"
    f.attributes
    #=> {:my_attr => "cos"}
    

    Ok. Co nam to może dać…?

    Ciekawym przypadkiem jest sytuacja, w której mamy 2 tabele o podobnym polu, jednak przy jednej z nich posłużyliśmy się trochę inną nazwą niż w drugiej, chociaż tak naprawdę chodzi o to samo i chcielibyśmy wykorzystać ten sam partial w formie, czyli w jednym i drugim przypadku

    <%= f.text_field :moj_atrybut %>
    

    Ktoś mógłby zadać bardzo przytomne pytanie – dlaczego nie użyć od razu

    class MyNewForm < ::BaseForm
    
      attribute :moj_atrybut, String
    
    end
    

    Faktycznie byłoby to rozwiązanie. Ale wtedy lekko skomplikowałoby nam to życie przy tworzeniu obiektu na podstawie atrybutów z forma

    MyObject.new(my_form.attributes)
    #=> ActiveRecord::UnknownAttributeError: unknown attribute: moj_atrybut
    

    Jeżeli natomiast posłużymy się aliasami – w widoku formularza możemy używać .moj_atrybut, wspólnie dla obu tabel, a potem spokojnie stworzyć obiekt

    MyObject.new(my_form.attributes)
    
  • .try(:method)!
  • Ostatnim z moich małych odkryć jest metoda try

    Jej działanie jest identyczne z

    
    my_object.my_method if my_object
    
    # <=>
    
    my_object.try(:my_method)
    

    Oznacza ni mniej ni więcej jeżeli dany obiekt istnieje – wywołaj na nim metodę. Jeżeli nie istnieje – zwróć nil-a

    Pamiętajmy, że gdyby my_object nie istniał, a wywołalibyśmy na nim metodę – dostalibyśmy błąd!

    Trzeba też mieć w pamięci, że try ma inne działanie niż .respond_to? – jeżeli dany obiekt istnieje, ale nie można na nim wywołać wskazanej metody – try wyrzuci błąd!

  1. Value Objects
  2. Wzorzec projektowy: Template Method
  3. Dodatkowe pole form object

1. Value Objects

  1. 7 patterns to refactor fat ActiveRecord models (a jakże😉 )
  2. Sitepoint: value objects explained with Ruby
  3. Rozdział z książki Rails 4 way
  4. Grok: value objects in Ruby

Kolejny krok z cyklu Xxx Objects, czyli refaktoringu polegającego na wydzielaniu specyficznych rodzajów obiektów, które (konwencyjnie) realizują jakieś założenia.

Attributes equality

Najważniejsze: value objects są to obiekty, których wzajemna relacja równości zależy od ich wartości, a nie identyczności obiektu.

objects whose equality is based on their internal fields rather than their identity

Przez wartości rozumiemy wszystkie atrybuty danego obiektu klasy. Pokażmy to może na przykładzie

class Address
  attr_reader :street, :number

  def initialize(street, number)
    @street = street
    @number = number
  end
end

adr1 = Address.new("dolna", 6)
adr2 = Address.new("dolna", 6)
adr1 == adr2
=> false

Co się stało? Dostajemy false ponieważ stworzone obiekty, mimo faktu, że mają te same wartości atrybutów – są dwoma osobnymi, różnymi obiektami klasy Address

Nie zgadza się to jednak  z naszymi założeniami dot. value objects

Aby nasza klasa je spełniała – musimy zdefiniować relację równości w ramach naszej klasy

 

class Address
  ...
  def ==(other)
    street == other.street && number == other.number
  end
  alias :eql? :==
end

adr1 = Address.new("dolna", 6)
adr2 = Address.new("dolna", 6)
adr1 == adr2
=> true

Super, podstawowe założenie Value objects : spełnione!
Dwa obiekty, reprezentujące te same wartości – są sobie równe

immutable

Kolejnym, bardzo ważnym założeniem, jest niezmienność wartości obiektów

Attributes should be immutable throughout its life cycle

Oznacza to ni mniej ni więcej, że nie powinniśmy mieć możliwości wykonania

adr1.street = "nowa"

W tym momencie, nasza klasa ma zablokowaną taką możliwość (posłużyliśmy się jedynie akcesorem attr_reader). Ciekawe rozwiązanie podpowiada sitepoint – umożliwmy „podstawianie wartości”, ale poprzez tworzenie nowego obiektu naszej klasy

 

class Address
  ...
  def street=(new_street)
    Address.new(new_street, number)
  end

  def number=(new_number)
    Address.new(street, new_number)
  end
end

adr1 = Address.new("dolna", 6)
adr2 = (adr1.street="nowa")

 

Struct.new

Sporym ułatwieniem może okazać się Struct.new, o którym wcześniej wspominaliśmy.

Dziedzicząc ze Struct.new nie musimy ani pisać konstruktora, ani też definiować funkcji porównującej!

class Address < Struct.new(:street, :new) 
end 
adr1 = Address.new("dolna", 6) 
adr2 = Address.new("dolna", 6) 
adr1 == adr2 
=> true

Niestety w tym rozwiązaniu aktualny pozostaje problem nadpisywania wartości atrybutów obiektu

Propozycją rozwiązania może być gem Valuektóry jak sam wskazuje – rozwiązuje dwa problemy:
– Constructors require expected arguments
– Instances are immutable

W ten sposób dziedzicząc po Value.new(:street, :number) zamiast po Struct – nie musimy martwić się nadwyżkowymi akcesorami

Inne korzyści z Value objects?

 

Muszę przyznać, że z początku nie widziałam „aż takiego” zastosowania obiektów Value objects

Należy jednak pamiętać, że na równości atrybutów – rola Value objects wcale nie musi się kończyć

Fajnym i dającym do myślenia przykładem jest klasa Rating prezentowana w poście na stronie Code Climate

class Rating
  def self.from_cost(cost)
    if cost <= 2
      new("A")
    elsif cost <= 4
      new("B")
    elsif cost <= 8
      new("C")
    elsif cost <= 16
      new("D")
    else
      new("F")
    end
  end
end

2. Wzorzec projektowy template method

Na podstawie 3go rozdziału Ruby: Wzorce projektowe – Urozmaicanie algorytmów za pomocą wzorca projektowego Template Method

słowo wstępne😉

Książka, chociaż wydana w 2008, pod niektórymi względami może być uznana za ponadczasową, dlatego myślę, że mimo pozornej nieaktualności warto poświęcić jej odrobinę uwagi.

Wzorzec projektowy – w ogromnym uproszczeniu, są to proponowane rozwiązania jakiegoś popularnego problemu, identyfikacja wspólnych rozwiązań.
Cel – maksymalne uelastycznienie systemów, ograniczenie kosztów wprowadzania potencjalnych zmian

Książka napisana jest w oparciu o tzw. Bandę Czworga (Gang of four) i prezentuje kilkanaście wybranych wzorców projektowych na przykładzie Ruby’ego

Na początku książki znajdziemy 4 podstawowe reguły rządzące wzorcami projektowymi (i ogólnie „dobrym” projektowaniem aplikacji), o których warto wspomnieć

  • Oddzielanie elementów podatnych na zmiany od tych niezmiennych
  • Programowanie aplikacji pod kątem interfejsu, tzn. operującej na możliwie najogólniejszym (abstrakcyjnym) typie danych
  • Posługiwanie się kompozycją zamiast dziedziczeniem (zbyt ścisłe powiązanie, dostęp do wszystkich metod nadklasy)
  • Delegacja – przenoszenie odpowiedzialności na inne obiekty

Wzorzec template method

W jakiej sytuacji?

Wzorzec ten przydaje się w sytuacji, kiedy mamy mechanizm, którego działanie jest podobne w różnych przypadkach, jednak na przykład jeden krok w ramach tego mechanizmu jest różny w zależności od przypadku.

„Abstrakcyjna” klasa bazowa
Definiujemy klasę, która będzie stanowiła klasę bazową naszego mechanizmu.

Metoda szkieletowa, szablonowa
W ramach klasy bazowej definiujemy metodę (zwaną szkieletową lub szablonową), która realizuje kolejne kroki naszego mechanizmu

Metody abstrakcyjne, metody zaczepienia
Metody zdefiniowane w ramach klasy bazowej, które są wywoływane w metodzie szkieletowej (szablonowej)

Możemy je definiować z różnym założeniem
– nie określając sposobu ich działania, oczekując, że zostaną osobno „przykryte” w konkretnych przypadkach (w klasie bazowej możemy zgłaszać wyjątki w ramach tych metod, żeby mieć pewność, że zostaną przykryte) => metody abstrakcyjne
– wstępnie określając ich działani (może być „pustym” wywołaniem!), zakładając, że mogą zostać przykryte, ale nie muszą => metody zaczepienia

Podklasy

Żeby dokończyć dzieła musimy w takim razie stworzyć podklasy, czyli te przypadki użycia
Podklasy będą wyglądały fragmentarycznie, bo będą zbierały metody… wydawałoby się nigdzie nie wywołane – ich wywołanie znajduje się w metodzie szablonowej!

Nie przykrywają one metody szablonowej, a jedynie metody abstrakcyjne (obowiązkowo) i metody zaczepienia.

+++

Na koniec warto zwrócić uwagę na dwie rzeczy

  • nie należy stosować wzorca template method w przypadku ogromnej liczby niezrozumiałych metod, które będą wymagały przykrycia – staramy się raczej ograniczyć jego stosowanie do mało rozbudowanych mechanizmów
  • należy pamiętać o tzw. taktyce ewolucyjnej, czyli założeniu, że najpierw poszukujemy możliwie prostego rozwiązania problemu i dopiero przy dalszym rozwoju aplikacji wprowadzamy rozwiązania takie jak metoda szablonowa – nie komplikujmy prostych sytuacji🙂

+++

Podsumowanie
Wspominany wzorzec polega na umieszczeniu w klasie bazowej niezmiennych elementów i przenoszeniu tych bardziej wyspecjalizowanych do zaimplementowania w klasach potomnych.

Implementacje mogą być konieczne (musimy przykryć metody niezdefiniowane w klasie bazowej) lub nieobowiązkowe (kiedy metody te są w jakiś sposób zdefiniowane w klasie bazowej).

Efekt?
Przejrzysty kod, elastyczne projekty

 

3. Dodatkowe pole w form object

Było sporo „teorii”, dlatego na koniec chciałabym podrzucić mały „railsowy trick”, który poznałam stosunkowo niedawno.

Wyobraźmy sobie, że korzystamy z jakiegoś form objecta – formularz zamawiania napojów

#app/forms/baverages_form.rb
class BaveragesForm < BaseForm
  attribute :name, String
  attribute :count, Integer
  attribute :alcohol, Boolean, default: false

  validates :name, presence: true
  validates :count, presence: true
end

#app/controllers/order_controller.rb
class OrderController < ApplicationController
  def new
    @form = BaveragesForm.new
  end

  def create
    @form = BaveragesForm.new(params)
    if @form.valid?
      #save order
    else
      #render form again
    end
  end
end

#app/views/orders/new.html.erb
<%= form_for(@form, url: orders_path) do |f| %>
  Nazwa: <%= f.text_field :name %>
  Ilość: <%= f.text_field :count %>
  Alkohol? <%= f.checkbox :alcohol %>
  <%= f.submit %>
<% end %>

Wydawałoby się, że wszystko wygląda świetnie.

Ale… przydałaby się walidacja pola alcohol w zależności użytkownika, który zamawia!

Jeżeli nie jest zalogowany – zwracamy błąd, że checkbox „alcohol” nie może być zaznaczony. Podobnie – jeżeli jest w wieku < 18 lat (przy założeniu, że w bazie przechowuje informację na temat daty urodzenia zarejestrowanych użytkowników)

Teoretycznie – oba te przypadki możemy obsłużyć „jakoś” w kontrolerze – sprawdzając co przyszło w params i w zależności od usera – zwracać błąd lub dopiero walidować obiekt form

Tak naprawdę jednak – chcielibyśmy dalej trzymać walidację w ramach stworzonego form object (w końcu po to go stworzyliśmy, żeby trzymać tę logikę w jednym miejscu)

class BaveragesForm < BaseForm 
  attribute :name, String 
  attribute :count, Integer 
  attribute :alcohol, Boolean, default: false 

  validates :name, presence: true 
  validates :count, presence: true 
  validate :user_age 

  def user_age 
    return true unless alcohol 
    if !current_user || current_user.date_of_birth > (Date.today - 18.years)
      errors.add(:alcohol, :not_allowed)
    end
  end
end

Byłoby idealnie, gdyby… gdyby nie fakt, że z poziomu tego obiektu nie mamy dostępu do current_user!

Nie chcemy robić z tego atrybutu formularza i jakoś go „dopychać”, bo użytkownik sam w sobie wcale nie jest związany z tym formularzem – jest nam „tylko potrzebny do walidacji”

Tu z pomocą przychodzi nasz „chwyt” i metoda .tap

class BaveragesForm < BaseForm 
  attr_accessor :user 

  attribute :name, String 
  attribute :count, Integer 
  attribute :alcohol, Boolean, default: false 

  validates :name, presence: true 
  validates :count, presence: true 
  validate :user_age 

  def user_age 
    return true unless alcohol 
    if !user || user.date_of_birth > (Date.today - 18.years)
      errors.add(:alcohol, :not_allowed)
    end
  end
end

class OrderController < ApplicationController
  def new
    @form = BaveragesForm.new
  end

  def create
    @form = BaveragesForm.new(params).tap do |form|
      form.user = current_user
    end
    if @form.valid?
      #save order
    else
      #render form again
    end
  end
end

Kluczowe są linijki: 2 oraz 26-28, w których dodajemy do obiektu formularza informację na temat użytkownika oraz akcesor, który pozwala na posłużenie się nią w walidacji.

Pokazany przykład jest być może trywialny, ale pokazuje w jaki sposób (elegancko), można zachować walidację w form object nawet jeżeli jest związana z obiektem nie będącym elementem formualrza

Noworoczny Ruby, odchudzanie modeli ciąg dalszy i gem Pundit

  1. Ruby 2.3
  2. Query Objects
  3. Pundit [gem]

1. Ruby 2.3 has been released!

…przecież na taką informację, nie możemy pozostać obojętni😉

Zrzut ekranu 2015-12-25 o 14.24.46.png

Czego możemy się spodziewać w Rubym 2.3?

  1. Zimowo: Frozen string literals

W Rubym wszystkie łańcuchy znaków podlegają zmianie

Zrzut ekranu 2015-12-25 o 14.52.09

Jeżeli chcieliśmy uniemożliwić ich edycję (przekształcić w immutable objects) – mogliśmy posłużyć się metodą .freeze

freeze

Freeze, jak sama nazwa wskazuje – powodował zamrożenie łańcucha znaków. Próba jego edycji kończyła się błędem.

W Rubym 2.3 mamy możliwość określenia wszystkich stringów jako domyślnie zablokowanych – możemy to zrobić za pomocą linijki na początku każdego pliku .rb

# frozen_string_literal: true

…lub korzystając z gema Magic frozen string litteral

Efekt:

freeze23

Dlaczego? Używanie frozen strings jest zdecydowanie bardziej wydajne.
Ponadto – warto się do tego przyzwyczajać. Ruby 3.0 będzie domyślnie traktował łańcuchy znaków jako frozen

2. Safe navigation operator – operator bezpiecznego wywołania

&.

Jest to operator który pozwala na skrócenie kodu

if var && var.method
# do postaci
if var&.method

Operator &. najpierw sprawdza czy obiekt, na którym chcemy wywołać metodę nie jest nil-em i jeżeli nie jest – wywołuje na nim odpowiednią metodę

W Railsach jak dotąd w takich sytuacjach posługiwaliśmy się .try(:method)

Różnica &.method – .try(:method)

Warto zwrócić uwagę, że &. sprawdza jedynie istnienie obiektu na którym chcemy wywołać metodę (czy nie jest nil-em). Jeżeli obiekt istnieje – próbuje wykonać na nim metodę.

.try natomiast sprawdza czy nie jest nil-em a następnie, jeżeli nie, sprawdza, czy na danym obiekcie można wywołać metodę czy też nie

3. Przekopywanie tablic i hashy : .dig(key1, key2…)

 

Metoda .dig pozwala na dostanie się do wewnętrznego elementu zagnieżdżonej tablicy za pomocą jednej metody. Jako parametry podajemy kolejne klucze po których jesteśmy w stanie wydobyć element.

Zrzut ekranu 2015-12-25 o 18.03.36

4. Na pohybel literówkom: did you mean…?

Najnowszy Ruby ma wbudowany gem Did you mean? który w przypadku NameError / NoMethodError – podpowiada „o co mogło nam chodzić”

did_you.png

5. Porównywanie Hashy

Tak! ale z rozwagą🙂

W Ruby 2.3 możemy używać metod >=, <=, <, >, == w stosunku do hashy, jednak należy bardzo dobrze rozumieć w jaki sposób działają!

Na przykład niezbyt jasne może wydawać się:

Zrzut ekranu 2015-12-25 o 20.46.29

Żeby to zrozumieć należy patrzeć na porównywanie hashy jak na sprawdzanie zawierania się pary klucz-wartość

Innymi słowy – jeżeli w porównywanych hashach nie znajduje się (znajdują się) dokładnie te same pary – na pewno otrzymamy false

Zrzut ekranu 2015-12-25 o 20.56.40

  • Przez równość dwóch hashy  rozumiem dokładnie te same klucze, z takimi samymi wartościami
  • Uznajemy, że jeden hash jest większy od drugiego jeżeli zawiera te same klucze z takimi samymi wartościami a ponadto posiada jakiegoś dodatkowe klucze

Dociekliwym polecam artykuł: Olivier Lacan: Hash Comparison in Ruby 2.3

6. Sprawdzanie czy wartość jest dodatnia/ujemna – .positive? / .negative?

 

2.positive? #=> true
2.negative? #=> false
-2.negative? #=> true

 

7. Bardziej restrykcyjne wydobywanie wartości spod kluczy: .fetch_values(:key1, :key2)

 

h = {a: 2, b: 3}
h.values_at(:a, :b, :c) #=> [2, 3, nil]
h.fetch_values(:a, :b, :c) #=> KeyError: key not found: :c

 

8. Odwrotność Enumerable#grep: #grep_v

..czyli metoda, pozwalająca na wybranie elementów nie pasujących do wzorca

names = %w(Krystyna Jan Basia Piotr)
women = names.grep(/a$/) #=> ["Krystyna", "Basia"]
men = names.grep_v(/a$/) #=> ["Jan", "Piotr"]

 

2. Query objects

 

Ostatnio pisałam o jednym z pomysłów na refaktoring – wyodrębnieniu tzw. Policy Objects. 

Dziś spojrzymy na kolejną propozycję z tego cyklu: Query Objects

Query objects, podobnie jak policy objects służą do odczytu danych (nie zaś operacji zapisu) i służą przede wszystkim „wyszczupleniu” fat models

Co je charakteryzuje?

Tak naprawdę będziemy korzystali z query objects aby wydzielić złożone zapytania.

Co za tym idzie, tak jak policy objects zwracają odpowiedź tak/nie – tak query objects będą zwracały kolekcję obiektów wyciągniętą z bazy danych na podstawie zapytania

Wyobraźmy sobie, że mamy w modelu rozbudowany scope (ten jest jeszcze  całkiem łagodnym przypadkiem)

app/models/post.rb

Zrzut ekranu 2015-12-26 o 01.13.18.png

Spróbujmy stworzyć query object dla tego zapytania

Zrzut ekranu 2015-12-26 o 01.11.57.png

Wówczas w kodzie

#zamiast
Post.most_commented
#wywołamy
Post::MostCommentedQuery.new.most_commented

Zwróćmy uwagę, że przy takim rozwiązaniu, moglibyśmy również…

author_posts = @author.posts
Post::MostCommentedQuery.new(author_posts).most_commented

Sprawa wydaje się banalnie prosta, jednak na ciekawą rzecz zwrócił uwagę autor jednego z wyżej wymienionych artykułów: co jeżeli mamy już istniejącą sporą aplikację i chcielibyśmy skorzystać z tego rozwiązania? Nieco uciążliwe byłoby wyszukanie i zmiana wszystkich wywołań scope’ów

Poza tym obecne wywołanie… nie wygląda fajnie! Zdecydowanie lepiej wyglądała wersja ze scope’m..

Żeby spróbować temu zaradzić należy spojrzeć jak działa Railsowy scope.

Spoglądając w kod źródłowy widzimy, że możemy przekazać dowolny obiekt (body) pod warunkiem, że będzie on odpowiadał na metodę call, bo to właśnie wywołanie metody call ( all.scoping { body.call(*args) } ) będzie wpływało na wynik.

=> wniosek? zmieńmy nazwę metody most_commented w naszej klasie na call i stwórzmy obiekt naszej klasy w modelu

Zrzut ekranu 2015-12-26 o 16.32.57.png

Moim zdaniem – takie rozwiązanie jest idealne.

  • Nadal zachowujemy tradycyjną konwencję Post.most_commented
  • Nie musimy martwić się dotychczasowym wykorzystaniem danego scope’a – będzie nadal działał
  • Znacznie odciążamy modele
  • Ktoś przychodząc z zewnątrz, szukając tradycyjnie w modelu jak zbudowany jest dany scope – nie ma problemu z identyfikacją

*możemy się jeszcze „pozbyć” słówka new delegując call do konstruktora

class << self
  delegate :call, to: :new
end

3. Pundit [gem]

Omawiając w poprzednim poście istotę Policy objects wspominałam, że następnym razem spojrzymy na gem oparty o ideę policies – Pundit

Pundit jest gemem, który bardzo ułatwia budowanie systemu autoryzacji w ramach aplikacji, to znaczy pozwala na określanie uprawnień do wykonywania akcji/przeglądania treści.

Zarówno na stronie gemu jak i w artykule New Relic znajdziecie dokładnie opisane krok po kroku (wraz z przykładami) wdrożenie go do aplikacji, dlatego tutaj opiszę ogólnie w jaki sposób można z niego korzystać, jak jest skonstruowany.

  • gem oczywiście wymaga instalacji a następnie dodania go za pomocą include do kontrolera aplikacji (application_controller.rb)
  • opcjonalnie można wygenerować „bazową” application_policy.rb
  • generalnie wszystkie klasy, które będą służyły do autoryzacji za pomocą Pundit-a trzymamy w folderze app/policies
  • Idea jest następująca – tworzymy klasę o takiej nazwie jak nazwa modelu z którym dana autoryzacja ma być związana, dodając słowo kluczowe Policy
    #załóżmy, że chcemy ustawić uprawnienia do edycji postów
    class PostPolicy
    end
    
  • konstruktor w takiej klasie zawsze będzie brał jako parametry user-a (domyślnie brany przez Pundit-a : current_userNie wymaga podania expicite) oraz drugi argument – najczęściej obiekt danego modelu (ale tak naprawdę może to być dowolny obiekt)
    class PostPolicy
      attr_reader :user, :post
      def initialize(user, post)
        @user = user
        @post = post
      end
    end
    
  • Pozostałe metody (jeżeli chcemy trzymać się konwencji i korzystać w pełni z „usprawnień” ze strony Pundit-a) mają takie nazwy jak nazwy metod w kontrolerze (PostsController), które chcemy opatrzyć autoryzacją + znak zapytania
    class PostPolicy
      #...
      def update?
        user.admin? || post.author == user
      end
    end
    
  • teraz, w danej metodzie kontrolera wystarczy umieścić kluczowe słowo authorize oraz obiekt, który ma „wpaść” pod post.Tak jak wspomniałam – nie musimy wcale podawac usera! domyślnie przekazany zostanie current_user
    class PostsController
      #...
      def update
        @post = Post.find(params[:id])
        authorize @post
        if @post.update(...)
        #...
        end
      end
    end
    

    Co tu się stało?
    wywołaliśmy authorize w ramach PostsControllerw akcji update =>
    gem wie, że ma wyszukać klasę PostPolicy, stworzyć obiekt posługując się current_userem i wywołać metodę update? na tym obiekcie

    class PostsController
      #...
      def update
        @post = Post.find(params[:id])
        authorize @post
        #...
      end
    end
    #to tak naprawdę...
    PostPolicy.new(current_user, @post).update?
    
  • widoku możemy skorzystać z helpera dostarczanego przez Pundit-a
    <% if policy(@post).update? %>
    ...
    <% end %>
    
  • Dostosowanie gemu do swoich potrzeb
    • można przekazywać nazwę metody klasy policies jeżeli jest inna niż nazwa metody z poziomu której wywołujemy authorize
      authorize @post, :other_method?
      
    • można przekazać nazwę klasy zamiast konkretnego obiektu, jeżeli działanie autoryzacji nie jest związane z konkretnym obiektem
      authorize Post
      
    • Można wreszcie na własny sposób określić skutki braku autoryzacji wychwytując wyjątek generowany przez Pundit
      rescue_from Pundit::NotAuthorizedError, with: :my_method
      
  • Pundit umożliwia również tworzenie scope’ów związanych z autoryzacją
  • Jest jeszcze kilka ciekawych zastosowań Pundita (np. co jeżeli nie mamy związanego z daną autoryzacją modelu? ) dlatego bardziej dociekliwym – polecam lekturę dokumentacji

Podsumowując – już z samej konwencji nazewniczej wiemy, że faktycznie gem ten związany jest z ideą policies
Ponadto – faktycznie metody w ramach klas Pundita zwracają wartości true/false

Sam gem, przynajmniej w większości „typowych” przypadków wydaje się łatwy i przyjemny w użyciu dając mimo wszystko pole do popisu do pewnych customizacji


Święta, koniec roku i czas postanowień noworocznych. Z radością mogę pokazać co spotkało mnie pod choinką i co w związku z tym stanie się moim wyzwaniem na 2016🙂

WP_20151225_14_11_47_Pro

 

Noworoczny Ruby, odchudzanie modeli ciąg dalszy i gem Pundit

Refactor cd., odrobina zabawy JS-em i pomocna wbudowana klasa Rails

  1. Czy… ? – Policy Objects
  2. Przeładowanie fragmentu strony z JS-em
  3. Struct.new 

1. Policy Objects

  1. [nieśmiertelne] Codeclimate – 7  Patterns to Refactor Fat ActiveRecord Models
  2. Grouper Engineering Blog
  3. Crushlovely  (Policy Object w JS-ie, ale zamysł ten sam😉 )

Wyodrębnienie tzw. Policy Objects to jedno z założeń refaktorowania kodu (przede wszystkim modeli)

Czym są?

Przede wszystkim: odpowiadają na pytania TAK/NIE

exclusively return boolean values, describing whether the policy passes or doesn’t pass the object

Wyobraźmy sobie, ze chcemy sprawdzic, czy dany użytkownik może zobaczyć post?
Sporo różnych przypadków:
– jeżeli jest adminem
– jeżeli jest autorem
– jeżeli post jest opublikowany i publiczny
– jeżeli post jest opublikowany  i prywatny, ale user ma do niego dostęp

Kontroler? Nie…

Wyobraźmy sobie, że umieszczamy to w kontrolerze, jako before_filter 
zaśmiecamy kontroler, o którego skinny przecież tak dbamy
– ograniczamy reużywalność tego sprawdzania ograniczeń
– utrudniamy testowanie

Model? Nie…

Podobnie błędem byłaby próba umieszczania tego w modelu – od zasady fat models skinny controllers odchodzimy już od dawna, poza tym sprawdzamy tu nie tylko atrybuty jednego modelu

Service Objects? Nie do końca…

Faktycznie – moglibyśmy wziąć pod uwagę wydzielenie to do jakiegoś serwisu, ale raczej przyjmujemy:

‘Service Object’ for write operations and ‚Policy Object’ for reads

…a przecież faktycznie – tutaj jedynie odczytujemy informacje, nie wykonujemy żadnych operacji zapisu na obiektach.

==> Policy Objects

Dlatego tego typu informacje postaramy się wyodrębnić do obiektów typu Policy Objects.
Zacznijmy od najprostszej implementacji – tworzymy odpowiednią klasę w folderze policy_objects

app/policy_objects/post_preview_available_policy.rb
+ app/controllers/posts_controller.rb

Zrzut ekranu 2015-12-20 o 16.49.03

Możemy pokusić się o jakiś mały refaktor, żeby wywołanie tego Policy wyglądało schludniej, wyciągając do nadrzędnej klasy inicjalizację

Na przykład

Zrzut ekranu 2015-12-20 o 17.22.44.png

Można również rzucić okiem na  gem, który jednak nie jest już rozwijany od dwóch lat, dlatego myślę, że można na niego spojrzeć raczej żeby zastanowić się nad pewnymi koncepcjami, które możnaby przenieść na swój teren:

Gem Policy (tomblomfield)

Za to w przyszłym tygodniu chciałabym spojrze na ciekawy gem służący jako system autoryzacji, oparty na idei policies: Pundit

2. Przeładowanie fragmentu strony: JS + Rails

Wyobraźmy sobie, że mamy na stronie dwie zakładki i w zależności od wybranej zakładki – jakiś fragment strony zawiera inną treść.

Moglibyśmy osiągnąć to odpowiednio nadając hidden jakiejś treści, ale wtedy w źródle strony będzie się ona cały czas znajdywała.

Poza tym dzięki naszemu rozwiązaniu może sprawiać wrażenia SPA😉

Oczywiście to co chcę tu pokazać to jedynie propozycja jak można to osiągnąć.

Widok – wydzielenie ważnych elementów

app/views/pages/show.html.erb

Zrzut ekranu 2015-12-20 o 20.21.11

W ten sposób
– wydzieliliśmy klasą js-tabs które elementy są klikalne
– wskazaliśmy skąd będziemy pobierali nową treść (data-url)
– wreszcie – wydzieliliśmy element do przeładowania js-fragment

Teraz w pliku .js chcemy obsłużyć wywołanie AJAX-owego zapytania w odpowiednie miejsce po kliknięciu w którąś z zakładek

Zrzut ekranu 2015-12-20 o 20.07.51

Ok! mamy ideę przeładowania – teraz musimy stworzyć ścieżkę, odesłać do odpowiedniej akcji w kontrolerzi stworzyć widok tej akcji, który przeładuje sekcję

Zrzut ekranu 2015-12-20 o 20.23.34

3. Struct.new

Ostatnie, o czym chciałabym napisać w tym poście to konstrukcja, z którą spotkałam się czytając różne artykuły na temat Policy Objects.

[printscreen ze strony: http://www.elabs.se/blog/52]

Zrzut ekranu 2015-12-20 o 20.40.24.png

Czym jest tajemnicze Struct.new po którym zdają się dziedziczyć definiowane klasy?

  1. Ruby doc
  2. Blog Ruby Learning
  3. Stephanie writes code
  4. „krytyka” Struct: The Pug Automatic

Struct jest to wbudowana klasa Ruby’ego która nie wymaga od nas tworzenia nowej klasy z inicjalizatorem i akcesorami.

W praktyce

Zrzut ekranu 2015-12-21 o 00.00.16

Jak widać nie musieliśmy tworzyć ani metody initialize, żeby pobrać nowe argumenty, ani ustawiać akcesorów do odczytu i zapisu

Na tym etapie… przypomina to jednak bardziej tworzenie hasha z atrybutami.

Co jednak ciekawe – do atrybutów możemy odwoływać się na kilka sposobów

p.name
p[:name]
p[&quot;name&quot;]
# a nawet, choć to może mało przydatne...
p[0]

Struct jest jednak klasą generującą podklasy  – to oznacza, że możemy definiować też metody

Zrzut ekranu 2015-12-21 o 20.04.44

Struct może nam służyć przy definiowaniu klasy, wykorzystując dziedziczenie

 class Person &lt; Struct.new(:name, :age, :sex)
end 

Dlaczego możemy dziedziczyć po konstruktorze?
Bo Struct.new, jak wspomnieliśmy tworzy klasę – de facto dziedziczymy po klasie utworzonej przez Struct, a to wydaje się już całkiem logiczne

Dzięki temu dziedziczeniu – nie musimy pisać ani metody initialize, ani martwić się o akcesory

Struct można również wykorzystać wewnątrz klas – przykład takiego zastosowania można zobaczyć na wskazanym przeze mnie blogu – Stephanie writes code

Wady!

Ze Structem należy jednak uważać…

  • w przeciwieństwie do „prawdziwego” initializeargumenty przekazywane do Struct nie są obowiązkowe

    Zrzut ekranu 2015-12-21 o 20.48.07Na ogół nie jest to pożądane zachowanie
  • Obiekty o tych samych wartościach atrybutów będą uważane za sobie równe
    Zrzut ekranu 2015-12-21 o 20.52.02
  • Akcesory, które najczęściej używamy jako prywatne – w przypadku dziedziczenia po Struct.new są publiczne – zarówno odczyt jak i zapis
Refactor cd., odrobina zabawy JS-em i pomocna wbudowana klasa Rails

Wszystko albo nic, jak odnaleźć drogę i praca za kulisami

PLAN:

  1. Transakcje
  2. Routing w Railsach – sposoby, chwyty, ciekawostki
  3. Zadania w tlebackground workers

1. Transakcje

Transakcje obejmują blok kodu, w ramach którego zostaną wykonane z sukcesem WSZYSTKIE zapytania do bazy lub ŻADNE z nich.

Najbardziej klasycznym i wskazywanym zawsze przykładem transakcji jest operacja przelewu.
Musimy mieć pewność, że sukcesem zakończy się zarówno pomniejszenie jednego konta o daną kwotę jak i powiększenie stanu docelowego konta.

Jeżeli nie uda nam powiększyć stanu konta odbiorcy – nie chcemy przecież, żeby stan konta nadawcy został obciążony!

Jakikolwiek wyjątek w ramach transkacji wymusi ROLLBACK, który przywróci stan bazy danych sprzed transakcji

Różne sposoby tworzenia transkacji

  • ActiveRecord::Base.transaction
    jako metoda nie wywołana na konkretnej klasie

    ActiveRecord::Base.transaction do
      david.withdrawal(100)
      mary.deposit(100)
    end
  • ModelName.transaction
    jako metoda klasowa wywołana na modelu

    Account.transaction do
      balance.save!
      account.save!
    end
  • class_object.transaction
    jako metoda wywołana na obiekcie danej klasy

    balance.transaction do
      balance.save!
      account.save!
    end

na co uważać?

  • w transakcji musi zostać wychwycony wyjątek, żeby Railsy wykonały rollback
    mogłoby wydawać się to oczywiste ale!
    np. update_attribute(attr, val) nie wyrzuca wyjątku w przypadku porażki, a zwraca false
    warto o tym pamiętać i używać np. update_attributes!(attr: val)Podobnie należy uważać na find_by_attr_name, które zwraca nil jeżeli nie znajdzie rekordu, ale nie wyrzuca wyjątku (w przeciwieństwie do find)
    Można to obejść samodzielnie wyrzucając wyjątek w przypadku nil

    ActiveRecord::Base.transaction do
      account = Accoun.find_by_name
      raise ActiveRecord::RecordNotFound if account.nil?
      ....
  • „wypływające w górę wyjątki”
    Wyjątki, które pojawią się wewnątrz transkacji, po przywróceniu stanu bazy danych – zostaną przekazane wyżej poza transakcję – musimy pamiętać o ich obsłużeniu!
    Nie dotyczy: ActiveRecord::Rollback – jest to specjalny wyjątek który „unieważni” transakcję, ale nie zostanie przekazany wyżej
  • nested transaction
    Z zagnieżdżoych transakcji powinniśmy korzystać, kiedy odwołujemy się do więcej niż jednej bazy danych
    Transakcja w ramach pojedynczego połączenia z bazą.
  • dwa dodatkowe callbacki związane z transakcjami
    • after_commit
      -wykonanie, jeżeli transakcja się powiedzie
    • after_rollback
      – wykonanie, jeżeli transakcja się nie powiedzie

 O callbackach w kontekście transakcji – jeszcze kilka słów niżej.

Masowe dodawanie danych w ramach transakcji

Innym wg mnie b. ważnym aspektem transakcji, jest szybkość wykonywania operacji wewnątrz bloku.

Najlepszym przykładem „kiedy warto” jest sytuacja, w której potrzebujemy za jednym razem dodać ogromną ilość rekordów do bazy.

ActiveRecord::Base.transaction do
  1000.times { Model.create(options) }
end

Oczywiście im więcej zapisywanych rekordów – tym „zysk czasowy” wyższy.

Built-in transaction – transakcje wbudowane

…czyli kolejny przykład, że Railsy czasami myślą za nas😉

Używając relacyjnych baz danych, mamy często do czynienia z parent table i jej child tables

Dokonywanie operacji na rekordzie z tabeli parent save oraz destroy – wpływa na odpowiadające mu rekordy tabeli child

Przykład:
trans_parent_child

Stworzymy produkt, stworzymy koszyk, przypiszemy produkt do koszyka.
Zapisanie koszyka do bazy => zapisanie produktu

Gdyby nasz produkt nie miał jednak podanej nazwy – nie dojdzie do zapisu ANI tego produktu, ANI całego koszyka

I tym właśnie są built-in transaction w Railsach!

Callbacks + transakcje

Skoro wspomnieliśmy, że save oraz destroy są już Railsowo opakowane w transkacje – trzeba pamiętać o tym, że ma to też znaczenie dla callback-ów z nimi związanych”

callbacks takie jak before_save, after_save stanowią część transkacji!
oznacza to, że w przypadku niepowodzenia transkacji – rollback będzie dotyczył również ich

Experimenting Built-in Transactions – Ruby & Rails For Dummies
Kilka słów: API 


2. Routing w Railsach – sposoby, chwyty, ciekawostki

Temat budowanie ścieżek w Railsach (routingu) jest tematem bardzo obszernym i co prawda nie uda mi się go wyczerpać, jednak postaram się zawrzeć jak najwięcej przykładów i ich „efektów”


resources

resources :products

resources tworzy defaultowe ścieżki dla podstawowych akcji:
index, show, new, edit, create, update, destroy

możliwość zawężenia przy pomocy resources :products, only: [:show, :edit, :update]

dla resources można określić kontroler
resources :products, controller: „admin/solds”

efekt:

routes_resources

jedynie część:
resources :products, only: [:index, :new, :create]


NAmespace

namespace :group do
    …
end

namespace służy do grupowania kontrolerów i odpowiedniego ustawiania ścieżek

Najłatwiej zrozumieć to na przykładzie.
Wyobraźmy sobie, że chcemy zgrupować kontrolery związane z panelem admina
uzyskujemy strukturę
app/
—-controllers/
——————admin/
————————–products_controller.rb
————————–promotions_controller.rb

namespace :admin do
resources :products
get 'promotion/new' =&gt; 'promotions#new', as: :new_promotion
end

efekt:
routes_namespace_1

dzięki namespace

  • pomimo użycia jedynie „promotions#new” – Railsy wiedzą, że mają wejść w folder admin i tam szukać promotions_controller
  • URL zamiast localhost:3000/promotion/new ma postać localhost:3000/admin/promotion/new
  • zmieniają się ścieżki, którymi możemy się posługiwać, tutaj należy uważnie się im przyjrzeć!
    zamiast new_promotion_path – admin_new_promotion_path
    zamias edit_product_path(p.id) – (nieco inaczej) edit_admin_product_path(p.id)

Scope module

scope module: ‚group’ do
    ….
end

resources :products, module: ‚group’

scope module: ‚group’ uzyjemy kiedy chcemy zgrupować kontrolery wewnątrz group ale nie wpływać na ścieżki/URL

scope module: 'admin' do
resources :products
get 'promotion/new' =&gt; 'promotions#new', as: :new_promotion
end

efekt:

routes_module_1

Jak widać – admin został jedynie przy wskazywaniu na kontroler


scope ‚/prefix’

scope ‚/prefix’ do

end

scope ‚/nazwa’ służy odwrotnemu celowi – jeżeli chcemy uzyskać prefix dla URL, ale nie chcemy grupować kontrolerów

scope '/admin' do
resources :products
get 'promotion/new' =&gt; 'promotions#new', as: :new_promotion
end

efekt

routes_scope2

Widzimy, że teraz admin pojawił się jedynie jako fragment URL, nie wpływając ani na ścieżki, ani na położenie kontrolerów


namespace + path

namespace :group, path: „prefix” do

end

to dziwaczne połączenie pozwoli nam na zebranie kontrolerów w group, ścieżki z group oraz ustawienie prefixu URL na prefix

namespace :admin, path: 'panel-admina' do
nbsp; resources :products
get 'promotion/new' =&gt; 'promotions#new', as: :new_promotion
end

efekt

routes_namespace_path


scope, Path, As

scope path: ‚url-prefix’, as: ‚path_prefix’ do
 ….
end

pozwala ustawienie prefixu URL na url-prefix oraz osobno prefixu ścieżek na path_prefix

scope path: "panel-admina", as: 'admin' do
resources :products
get 'promotion/new' =&gt; 'promotions#new', as: :new_promotion
end

efekt

routes_5

Member, Collection

resources :products do
get 'preview', on: :member
get 'search', on: :collection
end
resources :products do
member do
get 'preview'
end
collection do
get 'search'
end
end

 

Pozwala na dodanie własnych akcji do defaultowych z resources i korzystanie zbudowanych w ten sposób ścieżek

akcje możemy dodawać do konkretnego obiektu (member), lub dla całej kolekcji (collection)
Tutaj chcemy mieć akcję preview dla pojedynczego obiektu i ogólną akcję search
 URL: /products/2/preview , /products/search
– Railsy wiedzą, że chodzi o metody preview i search w kontrolerze products_controller

Pierwsze rozwiązanie – kiedy chcemy tylko jedną akcję
Drugie – kiedy takich akcji będzie więcej

resources :products do
member do
get 'preview'
end
get 'search', on: :collection
end

efekt

routes_member_collection

„Zmienne” w ścieżkach – dynamic segments

wszystko, co w routesach podamy po dwukropku (patrz – :id we wszystkich dotychczasowych przykładach) będzie dostępne wewnątrz metody pod params[:nazwa]

dwa specjalne:
:controller
:action

get ‚:controller/:action/:query’

spowoduje, że /products/search/cheapest
przejdzie do metody search w kontrolerze products, a pod params[:query] – znajdzie sie „cheapest”

Constraints

możemy nakładać ograniczenia na wszystkie dynamiczne segmenty (te po dwukropku)

w przypadku ograniczeń na jeden „parametr”

get ":controller/:action/:id/:query", id: /[A-Z]\d{5}/

wiecej ograniczen:

get ":controller/:action/:id/:query", constraints: { id: /[A-Z]\d{5}/, query : /.../}

Wieloznaczne segmenty, Przekierowania

wildcard segments, czyli wieloznaczne segmenty, są to segmenty,w ramach których pozwalamy na dowolną „podścieżkę”

get '/products/*whatever/:title' =&gt; "products#custom"

do tego wzorca będzie pasował np url:
/products/sth-12/sth-else/123/Harry_potter
Z poziomu kontrolera – params[:whatever] => sth-12/sth-else/123, params[:title] => Harry_potter

Przekierowania

get '/promotions', to: redirect('/sales')

w przekierowaniu można posłużyć się parametrami z wpisanej ścieżki

get '/promotions/show/:id', to: redirect('/sales/show/%{id}')

Zadania w tlebackground workers

Czym są najogólniej zadania w tle? To tak naprawdę wszystko to, na czego wykonanie użytkownik nie czeka, siedząc na naszej stronie.

Słowo klucz: kolejka, kolejkowanie zadań

Kolejka = FIFO – first in first out 
Oznacza, że gromadzimy po kolei zadania do wykonania i wykonujemy je w takiej kolejności w jakiej zostały dodane.

Odwrotnością jest stos – LIFO – last in first out – gdzie jako pierwsze zostaje wykonane zadanie, które ostatnio wpadło na stos

kiedy stawiamy na zadania w tle?

3 najczęstsze sytuacje:

  1. Wykonanie zadania może zająć sporo czasu
  2. Zadanie jest zależne od komunikacji z innymi serwerami
  3. Wykonanie zadania jest niezależne od działania użytkownika (zadania cykliczne, czasowe)

Ad 1.
Wyobraźmy sobie, że mamy funkcjonalność, która przelicza jakieś skomplikowane statystyki działania danego użytkownika. Nie chcemy, żeby użytkownik klikając na „przelicz statystyki” musiał czekać na naszej stronie, aż wszystko przeliczymy. Szczególnie, że może się zdarzyć, że kilku innych użytkowników w tym samym czasie może zrobić to samo.

Dużo lepszym rozwiązaniem byłoby, gdybyśmy poinformowali go:
Po wygenerowaniu podsumowania będzie dostępne w zakładce „statystyki”

My kolejkujemy zadanie, użytkownik sobie śmiga po naszej aplikacji od czasu do czasu sprawdzając czy już jest, czy może jeszcze nie.

Ad 2.

Jeżeli działanie naszej apki jest zależne od innych aplikacji – warto również potraktować je jako zadanie w tle na wypadek gdyby „ta druga zaniemogła” i zablokowała nasze działania.

Ad 3.

W tym przypadku raczej będziemy posługiwali się innymi narzędziami, ale i zadania w tle mogą się przydać, np. w sytuacji, kiedy chcemy „po tygodniu od założenia konta” wysłać użytkownikowi maila. Nie będzie to więc reakcja na jakieś konkretne działanie usera.

Czego używać do zadań w tle

Na podstawie: Sitepoint: Comparing Ruby Background Processing Libraries

1. Delayed Job

Jumpstartlab tutorials
Strona gemu
Sitepoint
Samurails

Stworzone przez Shopify, która to firma udostępniła kod i dorzuciła doc-i, już dobrą chwilę temu (~2009?)

W przeciwieństwie do dwóch pozostałych – nie potrzebuje instalacji niczego innego, co miałoby posłużyć do kolejkowania (jak Redis, o czym przekonacie się później)

Kolejkowanie w Delayed::Job opiera się o railsowy ActiveRecord – de facto tworzymy tabelę w BD, która będzie przechowywała listę zadań do wykonania

Gemfile:

gem 'delayed_job_active_record'

…dodaliśmy gem, teraz posłużymy się odpowiednim generatorem, który m. in. przygotuje dla nas migrację dla wspomnianej tabeli

rails g delayed_job:active_record

Spoglądając w utworzony plik migracyjny możemy dowiedzieć się już całkiem sporo na temat tego, jak będzie działał Delayed::Job (dobrze okomentowany!)


delay / handle_asynchronously

Dodajmy do modelu User metodę np. „send_welcome_email”, która ma być zakolejkowana w momencie rejestracji.

Aby została wykonana asynchronicznie mamy dwie możliwości:

  • dodanie delay przed wywołaniem metody
    user.delay.send_welcome_email
  • określenie danej metody jako trwale asynchoniczną
    handle_asynchronously :send_welcome_email
    

    Wówczas samo user.send_welcome_email zostanie zakolejkowane

Możemy za jednym razem odpalić wszystkie zakolejkowane zadania:

rake jobs:work

Lub też poprosić Railsy, żeby wykonywały zakolejkowane zadania na bieżąco

RAILS_ENV=development bin/delayed_job start


Wady Delayed::Job?
Nie jest najszybszy – kiepska wydajność przy ogromnej ilości zadań ze względu na oparcie się o BD
Lepszym pomysłem jest oparcie się o tzw. bazy NO SQL – np. Redis…

Kilka rad o tym „najlepiej korzystać” z Delayed::Job – Sitepoint: Delayed Job Best Practices

2. Rescue

Strona gemu
JumpstartLab
Sitepoint
Samurails

2.a REDIS

Rescue w przeciwieństwie do Delayed Job potrzebuje „czegoś więcej” – swoje działanie opiera o Redisa, czyli bazę danych typu klucz-wartośc.

Aby móc korzystać z Rescue musimy najpierw zainstalować Redisa i uruchomić jego serwer.

2.b Gem

Następnie – instalujemy gem resque

gem 'delayed_job_active_record'

2.c Konfiguracja

Nasz zadania będziemy trzymali w osobnym folderze:

app/workers

Aby nasi pracownicy byli powiązani z naszą aplikacją musimy stworzyć plik lib/tasks/resque.rake

require 'resque/tasks'

task "resque:setup" =&amp;gt; :environment

(druga linijka jest konieczna, jeżeli nasze zadania w tle będą potrzebowały dostępu do modeli

2.d Worker

Stwórzmy nasz pierwszy worker

app/workers/email_worker.rb


class EmailWorker

@queue = :email

def self.perform(email)

email.deliver

end

end

Z reguły klasa naszego zadania powinna zawierać słowo Worker w nazwie.
Następnie określamy w jakiej kolejce będą umieszczone jego zadania
Ostatnia rzecz to samo wykonanie zadania – metoda klasowa perform (zawsze klasowa, zawsze perform😉 )

2.e Zakolejkowanie zadania

Aby zamówić wykonanie zadania…


Resque.enqueue(EmailWorker, params[:email])

Linijka ta może pojawić się w metodzie kontrolera czy w jakiegoś serwisu.

Wady?

Brak możliwości priorytetowania zadań

3. Sidekiq

Główna strona Sidekiq’a
Gem
Sidekiq’owe wiki
RailsCasts

Sidekiq podobnie jak Resque wymaga instalacji i uruchomienia Redisa

Następnie instalujemy sam gem

gem 'sidekiq'

Aby skorzystać z Sidekiq’a nie musimy grzebać w folderze lib/tasks

Podobnie jak w przypadku Resque – będziemy umieszczali zadania w app/workers

app/workers/email_worker.rb


class EmailWorker

include Sidekiq::Worker

def perform(email)

email.deliver

end

end

Różnice?

  • nie tworzymy kolejki
  • załączamy za to Sidekiq’owa klasę
  • obowiązkowa metoda perform nie jest tu metodą klasową

A teraz wywołajmy gdzieś nasze zadanie


EmailWorker.perform_async(email)

Nasze zadanie zostało zakolejkowane.
Aby odpalić zakolejkowane zadania –


bundle exec sidekiq

Background Workers: podsumowanie

 

Delayed Job Rescue Sidekiq
Wady Działa oparte o ActiveRecord = wolniej niż w oparciu o Redisa Niebezpieczeństwo przeplatania wywołań a- i synchronicznych Nieco trudniejszy do odpalenia od DJ Uwaga na thread-safe!
Zalety Łatwy „na start” W oparciu o Redisa = szybkość Wydzielenie kodu Jeszcze lepsza wydajność Wydzielenie kodu
+ web dashboard
Wszystko albo nic, jak odnaleźć drogę i praca za kulisami

statyczne modele + R5 + feed

Tydzień V : 19.09-26.09.2015

PLAN:

  1. Static model
  2. Co nowego w Rails 5
  3. Cd. AWD with Rails 4, 158 – 231

1. Static model

…chociaż to nie najlepszy tytuł rozdziału.

O czym chciałam napisać?

O „alternatywie” dla przechowywania *niektórych danych tabelach bazy danych.
*niektórych, bo to o jakich danych mówimy jest bardzo ważne
*niektóre = dane statyczne, read-only, które nie ulegają zmianie, ale które potrzebujemy przechowywać i np łączyć w relacje z danymi w bazie

Przykład:
Wyobraźmy sobie, że z przechowujemy dane o nieruchomościach. Każda nieruchomość znajduje się w jakimś mieście.
Okazuje się, że z jakiegoś powodu, potrzebujemy też danych na temat tego miasta, np. dł. i szer. geograficzną, czy herb.

Potrzebujemy stworzyć relację 1:n pomiędzy miastem a nieruchomością
Każde miasto mogłoby więc być rekordem w bazie danych.

ALE!
Dane na temat miasta nie będą ulegały zmianie! Po co więc trzymać je w bazie i tracić na wydajności…?
A jeżeli nie w bazie, to gdzie, żeby móc zachować normalne „railsowe relacje” typu
Nieruchomosc.find(id).miasto.herb_url ?

Z pomocą przychodzą 2 gemy: Static Model & Active Hash

1. Static Model

Oficjalna strona projektu

  • instalacja :
    gem install static_model / gem install quirkey-static_model -s http://gems.github.com
  • Pozwala na tworzenie modeli i zachowanie == ActiveRecord, ale przechowuje dane w statycznych plikach YAML
  • Tworzymy model w app/models tak jak normalnie tworzymy modele ActiveRecord, z tą różnicą, że dziedziczymy pod StaticModel::Base
    # app/models/city.rb
    class City < StaticModel::Base
    
    end
  • Stworzenie takiego modelu bez żadnych dodatkowych informacji „zakłada”, że mamy plik YAML config/data/cities.yml
    *uwaga: zwróć uwagę, że zakładamy istnienie pliku o nazwie takiej, jak tworzone są nazwy tabeli – np dla modelu PaymentInfomation – config/data/payment_informations.yml
  • pliki można umieszczać w innych lokalizacjach, wtedy w ramach klasy
  • set_data_file '/sciezka/do_pliku/nazwa.yml'
  • format takiego pliku – tablica hashy o kluczach-atrybutach
    konieczne: podanie id
  • ---
    - id: 1
      name: Kraków
      longitude: 19.9372
      latitude: 50.0617
      herb_url: krakow.jpg
    - id: 2
      name: Warszawa
      longitude: 21.0211
      latitude: 52.2605
      herb_url: warszawa.jpg
  • Możemy tworzyć takie same zapytania jak dla ActiveRecord
      City.find(2),  City.first.longitude, City.all
  • „Jak na razie” (more coming) StaticModel wspiera jedynie relacje has_many oraz belongs_to pomiędzy SM-AR, SM-SM
    class Property < ActiveRecord::Base
      belongs_to :city, :foreign_key => 'city_id'
    end
  • class City < StaticModel::Base
      has_many :properties
    end
  • Tworzenie
  • >> p = Property.create(address: 'Lubicz 5', city_id: 1)
    => #<Property>
    >> p.city
    => #<City:0x1706fd8 @attributes={name: "Kraków",...

2. Active Hash

  • Strona gemu
    Blog Pivotal
  • Podobny choć bardziej rozbudowany
  • Jak można się domyślić: pozwala na przechowywanie danych w postaci hasha, gdzie nazwy atrybutów = klucze
    obowiązkowy: klucz id
  • tworzymy model, który dziedziczy po ActiveHash::Base
  • 3 sposoby tworzenia obiektów w takiego modelu, wewnątrz klasyactive_hash_create
  • Tworzenie obiektów w osobnym pliku (nie zwalnia nas ze stworzenia modelu!)
    # config/initializers/data.rb
    Rails.application.config.to_prepare do
      City.data = [
          {id: 1, name: "Kraków"},
          {id: 2, name: "Warszawa"}
      ]
    end
  • Domyślne wartości pól
    field   :city_rights, default: false
  • Metody klasowe
    all, count, first, last, find(id)/([ids]), find_by_id()
    find_by_fieldname, find_all_by_fieldname,
  • Tworzenie relacji  – zależne od wersji ActiveRecord (do przeczytania)
    wymaga extend ActiveHash::Associations::ActiveRecordExtensions

    class Property < ActiveRecord::Base
      extend ActiveHash::Associations::ActiveRecordExtensions
      belongs_to :city
    end
  • Skróty – ułatwiające powiązanie obiektów
    class Property < ActiveRecord::Base
      extend ActiveHash::Associations::ActiveRecordExtensions
      belongs_to :city, :shortcuts => [:name]
    end
    
    home = Property.new
    home.city_name = "Kraków"
    # Równoznaczne z : home.city = City.find_by_name("Kraków")
  • ActiveYaml – możliwość przechowywania danych w pliku .yml
    w formacie tablicowym/hashowym

    • domyślnie : będzie szukał w tym samym katalogu pliku cities.yml
      class City < ActiveYaml::Base
      end
    • możemy jednak określić katalog i/lub nazwę pliku
      class City < ActiveYaml::Base
        set_root_path "my/dir"
        set_filename "my_cities"
      end
    • Pliki .yml w dwóch stylach: tablicowy (patrz; static model), hashowy
      krk:
        id: 1
        name: Kraków
        longitude: ...
      war:
        id ...
  • ActiveJSON – możliwość przechowywania danych w pliku .json
    • lokalizacja pliku – jak wyżej, ActiveYaml
    • array-style 
      [{
          "id": 1,
          "name": "Kraków",
          ...
        },
        {
          "id": 2,
          ...
        }]
    • hash-style
    • {
        { "krk":
          {
            "id": 1,
            "name": "Kraków",
            ...
          }
        },
        { "war":
          {
            "id": 2,
            ...
          }
        }
      }
  • bardziej skomplikowana zabawa: ActiveFile

2. Co nowego w Rails 5*?

*Niestety muszę przyznać, że po części wracam na tarczy. Na tak wysokim poziomie rozwoju framework’a, niektóre zmiany, pojawiające się w wersji piątej, dotyczą na tyle zaawansowanych jego elementów, że nie wszystkie rozumiem. Ale – to oczywiście jedynie kwestia czasu😉
Elementy, których nie zrozumiałam pozwolę sobie oznaczyć *** gwiazdkami – w ten sposób zasygnalizuję ich obecnośc w Rails 5, ale nie będę realizowała zasady „nie znam się, to się wypowiem”

Na starcie chciałabym polecić „copiątkowy” newsletter zmian w Railsach: Rails weekly

Ze zmian, przede wszystkim: two key components: TURBOLINKS 3  && ActionCable

Turbolinks 3

Nie są może „rewolucją”, ale kolejnym krokiem naprzód.
Na szczęście zainteresowałam się nimi wcześniej, dlatego teraz możemy jedynie skupić sie na rzeczonym „kroku”

Turbolinks 3 w porównaniu do Turbolinks będą szybsze
Ponadto pozwolą na określenie, który element strony ma zostać przeładowany (jak dotąd: przeładowanie title i body) – partial updates
– Używamy HTML5 data- atrybutu dla oznaczania elementów DOM, które moga zostać przeładowane
– ***Rack middleware
– w kontrolerze: render :index, update: :our_element

***Action Cable

ActionCable na githubie

Słowo klucz: REALTIME
– 
realtime features, realtime messages, realtime functionality

– dzięki integracji websocket do naszej aplikacji, możemy korzystać z funkcjonalności w czasie rzeczywistym  – mamy tu na myśli np. czaty, kanały informacyjne
Mówimy tutaj o tzw. połączenia dwukierunkowych

Jak dotąd – ActionCable stanowił gem i był odpalany jako osobny proces (przykład czat-aplikacji z wykorzystaniem gemu)

Rails 5 Preview: Action Cable Part I  (tu również przykładowa aplikacja) :

Action Cable holds open a socket connection within your Rails app and allows you to define a channel (i.e. that socket), stream or post data to that channel and get or subscribe to data from that channel

Pozostałe zmiany

  • Rails 5 will only work on Ruby 2.2.1 and above.

  • ***Rails Api
    W Rails 5 wdrożony zostanie gem rails-api
    Ułatwi to tworzenie aplikacji Railsowych, API-only Rails applications, jako backendu dla aplikacji SPA
    Więcej info:  How to Build a Rails 5 API Only and Backbone Application …

    rails new <application-name> --api
  • rake => rails
    Zamiast używać koment rake db:migrate – w R5 będziemy posługiwali się rails db:migrate
  • relacja belongs_to, defaultowo stanie się obowiązkowa
    class Pet < ActiveRecord::Base
      belongs_to :owner
    end

    Jeżeli teraz będziemy chcieli stworzyć nowy obiekt Pet.new – musimy określić ownera
    W przeciwnym razie – błąd
    Możliwość „wyłączenia” : belongs_to :owner, optional: true

  • dodanie metody where.or dla ActiveRecord
    Post.where('id = 1').or(Post.where('id = 2'))
    # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
  • has_secure_token
    Super sprawa do dodania w modelu.
    – Wymaga utworzenia w migracji odpowiednich pól (bez żadnych dodatkowych specyfikacji – token)
    – przy tworzeniu nowego obiektu User.new() – automatycznie generuje token
    – + regenerate_token!rails5_token
  • *** zamiast alias_method_chain => Module#prepand
  • możliwość renderowania widoków „znieważnekąd” (nie tylko z poziomu kontrollera)ApplicationController.render _render_options_
    + assigns, które pozwoli na przekazywanie zmiennych instancyjnych do renderowanych widoków
  • rake restart  – restart serwera
  • zmiany związane z testami
    • zmiana syntaksy (zarówno RSpec jak Minitest)
      post :create, params: { y: x }, session: { a: ‘b’ }
      get :view, params: { id: 1 }
      get :view, params: { id: 1 }, format: :json
    • nowy test runner, który podobnie jak dla Rspec-a pozwoli na określenie pliku oraz linijki, której test chcemy odpalić
    • zmiany w testach kontrolerów
      assert_template : niezalecane
      assert_select : nadal wspierane do sprawdzania obecności elementów DOM
      assigns : również odpada

A skąd to wszystko wiem…?😉


3. AWD with Rails 4

Po raz kolejny natknęłam się dosyć szybko na „sporą rzecz”, o której wcześniej nie miałam pojęcia…

Web feed, news feed, atom, rss

Aż do dzisiaj, jak chodzi o IT, słowo feed kojarzyło mi się jedynie z feedback’iem …

Web Feed, news feed

…czyli po polsku kanał informacyjny (internetowy) – umożliwia dotarczanie użytkownikowi informacji na temat często aktualizowanych danych.

W pewnym zakresie, kanały informacyjne możnaby porównać do newslettera, jako źródła informacji „o nowościach”. Newsletter jest jednak np. codzienny, informacje na kanałach są dostarczane natomiast na bieżąco, w miarę aktualizacji.
Ponadto informacje z kanałów infomacyjnych dostarczane są bez treści reklamowych, zbędnych ozdobników – są to „suche informacje” oparte na formacie XML.

Śledzenie kanałów umożliwiają nam tzw. czytniki kanałów (np Atom Reader, czy RSS reader zależnie od formatu, często programy pocztowe).
Czytniki pozwalają nam na śledzenie interesujących nas informacji, w jednym miejscu, z przeróżnych źródeł.
Dostajemy listę z : tytułem, krótkim opisem i linkiem do pełnej treści

Ale ale… skąd my się tu wzięliśmy?

Bo nasza aplikacja może chcieć udostępniać użytkownikowi dane za pomocą kanału informacyjnego!
Polega to na generacji odpowiednich plików w chwili aktualizacji danych.
Pliki takie muszą jednak mieć bardzo konkretny format, aby moć zostać odczytane przez czytniki kanałów informacyjnych.

Atom, rss

To dwa standardy kanałów informacyjnych, oba oparte na XML.

Bardziej znanym jest standard RSS, jednak ze względu na pewne swoje niedoskonałości – jest stopniowo wypierany przez format Atom

Atom: rozszerzania .atom, .xml
RSS: .rss, .xml

Więcej na temat różnic Atom – RSS można przeczytać tutaj czy na Wikipedii

Ok. Skoro już wiemy, że Atom jest tym „postępowym” formatem – zastanówmy się jak w naszej aplikacji możeli generować pliki .atom, aby umożliwić użytkownikowi śledzenie zmian w naszej aplikacji

generowanie atom feeds

Szukając informacji na temat generowania rzeczonych plików, tradycyjnie, otworzyłam w kolejnych kartach kilka pierwszych wyników. [linki poniżej]

Żadna z odpowiedzi nie przekroczyła 3 scrolli myszką. To był dobry znak🙂

Potrzebujemy 3 rzeczy, żeby nasz builder zadziałał, w tym 1 nowego pliku i 2 „edycji”

  1. Element dzięki któremu przeglądarki/czytniki będą mogły automatycznie wykryć istnienie kanału informacyjnego (widok)
  2. Renderowanie dla odpowiedniego formatu (kontroler)
  3. Plik .atom.builder, który będzie generował feedy

Załóżmy, że chcemy dostarczać informacji na temat naszych produktów (mają nazwę, cenę i opis)

1. auto_discovery_link_tag

%= auto_discovery_link_tag(:atom, products_url(format: „atom”),  {title: „Przykład Atom”}) %

:atom – jest to wartość atrybutu type – domyślnie :rss
products_url – ścieżka, która doprowadzi nas do generowania feed
format: „atom” – dla ścisłości określamy format

Jeżeli np z naszej strony będzie generowany tylko jeden kanał informacyjny, taki element można umieścić w head
Generowany element jest niewidoczny, nawet jeżeli jest w treści strony

Wygenerowano : link rel=”alternate” type=”application/atom+xml” title=”Przykład Atom” href=”http://localhost:3000/products.atom&#8221;

Dzięki temu, po wejściu na naszą stronę np Feed Reader Chrome’a pokaże nam mały plusik przy ikonie a po kliknięciu zaproponuje śledzenie kanału

alink

2. „reakcja na format”

# app/controllers/products_controller.rb

def index
   @products = Product.all
   respond to |format|
     format.html
     format.atom
   end
end

3. app/views/products/index.atom.builder

Do stworzenia pliku używamy helpera atom_feed
Mamy kilka opcji „konfiguracyjnych”, do przejrzenia w dokumentacji helpera, m.in, language

elementy obowiązkowe to
atom_feed (options) do |feed|
feed.title
feed.updated
feed.entry … do |entry|

Działanie tego generatora najłatwiej zrozumieć obserwując plik i to co generuje:

atom2

Linki związane z generowaniem pliku feed w Railsach:


Pozostałe rzeczy, które zwróciły moją uwagę

  • Railsowe testy

    rozróżnienie:
    unit testing of models
    functional testing of controllers
    integration testing – 
    testowanie „user stories”Dwa pierwsze typy są generowane przy używaniu rails generate kontrolerów czy modeli
    Integration – nie są generowane automatycznie, ale mogą być również stworzone rails g integration_test user_stories
    > class UserStoriesTest < ActionDispatch::IntegrationTest
    symulowanie zapytania AJAX w teście:
    xml_http_request :post ‚/products’, product_id: 5

    testowanie mailingu
    nie wysyłamy „naprawdę”, posiłkujemy się ActionMailer::Base.deliveries

  • Autoryzacja użytkowników bez gem-a 

    Było to dla mnie o tyle ciekawe, że jak dotąd „ułatwiałam sobie pracę” gemem np. devisenie zastanawiając się czy można to robić inaczej.
    Autor książki co prawda sam wspomina, że warto jednak polegać na gotowych rozwiązaniach jako bezpieczenijszych i biorące pod uwagę wszystkie możliwe „pułapki” – nie zaszkodzi jednak odrobina świadomości😉

    • password:digest
      (przy tworzeniu migracji)
      digest – typ danych, który pozwala na przechowywanie zakodowanych haseł
    • has_secure_password
      (w modelu)
      obsługuje kodowanie hasła z „normalnego” na „zakodowane”
      umożliwia posługiwanie się password i password_confirmation
    • gem ‚bcrypt-ruby’
    • user = User.find_by(name: params[:name])
      user.authenticate(params[:password])
  • testy
    • assert_redirected_to
    • w fixtures: możliwość posłużenia się
      %= BCrypt::Password.create(‚haslo’) %
  • before_action vs before_filter
    w Rails 4. wszystkie callbacki *_filter zostały zastąpione przez *_action
    z tym, że _filter nadal działa i nie jest deprecated 
  • i18n = i-nternationalizatio-n!
    czyli aplikacja w kilku językach

    • config/initializers/i18n.rb
      I18n.default_locale = :en
      LANGUAGES = [ [‚English’, ‚en’], [‚Polish’, ‚pl’]]
    • dodanie wyboru języka do ścieżki
      scope ‚(:locale)’ do
      + odpowiednie callbacki  w application controller, które, wychwytując z URL-a scope z okresleniem języka ustawiają:
      I18n.locale = params[:locale]tm
    • helper t
      plik: app/views/products/show.html.erb
      t(‚.title’)
      poszuka w config/locales/xx.yml
      (gdzie xx to kod języka wychwycony z URL)

      #config/locales/xx.yml
      xx:
      
         products:
            show:
               title: "Przetłumaczona wersja"
    • tłumaczenia błędów walidacji

      #config/locales/xx.yml
      xx:
      
         activerecord:
            errors:
               messages:
                  blank: "nie może być puste"
    • przekazywanie parametrów, element nie „w zależności od template”
      %= t('errors.template.header', count: @order.errors.count, model: ...)
      
      #config/locales/xx.yml
      xx:
      
         errors:
            template:
                header: "wystąpiło %{count} błędów
               messages:
                  blank: "nie może być puste"
    • flash[:notice]

      #controller
      redirect_to index_path, notice: I18n.t(‚.sorry’)

      #xx.yml
      xx:
      sorry: „Przepraszamy, coś poszło nie tak”

 


Pomysły na przyszły tydzień:

  1. AWD with Rails 4 cd…
  2. Transakcje
  3. Config/routes.rb – opcje, triki, sposoby

 

 

 

statyczne modele + R5 + feed

W pogoni za object-oriented

Tydzień IV : 12.09-19.09.2015

PLAN:

  1. Powrót do AWD with Rails 4, 107-158
  2. Prezentery, Dekoratory
  3. Gem Draper

Szybki komentarz do planu. Uległ pewnej zmianie.
Spodziewałam się, że Prezenter i Dekorator to dwa zupełnie osobne tematy. Internet jednak przekonał mnie, że… nie do końca.
Stąd „prezentery i dekoratory” + gratisowy gem Draper, który jest bardzo mocno związany z tym tematem.

1. Powrót do AWD with Rails 4



…po czym zatrzymałam się już na 2 stronie kolejnego rozdziału (108)

Concerns

  • Concerns are modules that can be mixed into your models and controllers to share code between them
  • folder concerns automatycznie tworzony przez Rails 4 w app/controllers, app/models
  • jak wyżej – concerns są to moduły, stanowiące rozszerzenie ActiveSupport::Concern
    1_extend
  • pozwają na wyodrębnienie kodu, który może być wpółdzielony przez kilka kontrolerów/modeli
  • żeby skorzystać w kontrolerze/modelu: include MyModule
    2_include
  • included, ClassMethods, pozostałe
    – included : to co znajduje się w tym bloku zostanie wykonane w ramach kontekstu klasy, która includuje nasz concern
    – module ClassMethods .. end – metody zdefiniowane w tym bloku zostaną dodane do klasy, która includuje nasz concern
    – metody spoza bloku included / ClassMethods: wykonywane jako metody instancyjne
  • co może się znaleźć w takim module (modele – M, kontrolery – K):
    • relacje, asocjacje, readery: M
    • scopy : M
    • filtry : M, K
    • metody klasowe, instancyjne, prywatne
  • Krótki tutorial na RichOnRails [concerns+models]
  • Put chubby models on a diet with concerns [models]
  • Elegant Brew: przykład Concerns dla kontrolera
  • Fajny artykuł, po francusku (polecam nawet dla osób nie znających francuskieg- warto spojrzeć na kod

Dalsze czytanie AWD…

  • hook method (metoda automatycznie odpalana przez Railsy w danym „momencie życia obiektu”
    Użycie w modelu
    before_destroy :ensure_somethig
    – pozwoli na wykonanie metody ensure_something w momencie kiedy bedziemy chcieli usunąć jakiś element, ale jeszcze zanim do tego dojdzie.
    – Przydatne, kiedy na przykład chcemy się upewnić, że usuwamy produktu już zamówionego przez kogoś
    – Jeżeli metoda zwróci true – Railsy będą kontynuowały usuwanie
  • dodawanie błędów errors.add(:base, „Treść błędu”) – to samo miejsce, gdzie idą błędy z validates
    tutaj – nie wiążemy błędu z żadnym atrybutem, tylko z obiektem jako takim
  • button_to -pozwala na wykonanie zapytania POST za pomocą buttona: tworzy mini form html
  • build
    Przy założeniu, że mamy: artykuł i użytkownika
    User – has_many :articles
    Articles – belongs_to :user

    @article = user.articles.build(:title => „MainTitle”)
    @article.save


    Build pozwala na stworzenie od razu relacji

  • Dodawanie nowych migracji
    Migracja nie zawsze musi być związana z tworzeniem/usuwaniem nowych kolumn tabeli, edycją typów danych – ogólnie – nie musi być edycją „struktury” bazy danych

    Migracja może być również „zbiorową edycją danych w bazie”
    ***przykład z książki***
    Najpier stworzyliśmy koszyk, który mógł posiadać produkty. Taką „pozycję koszyka” określiliśmy jako LineItem
    Po dodaniu kilku produktów do różnych koszyków zorientowaliśmy się, że zamiast dodawać 3 razy jeden produkt – fajnie byłoby zwiększać jego ilość
    Dodaliśmy pole quantity do tabeli line_items

    3_add_quantity

    Super, możemy nawet obsłużyć, że teraz dodawanie produktu może zmieniać quantity
    Ale warto też zmienić dotychczasowe rekordy – usunac powtorzenia i zamienic je na ilosci – tutaj możemy również posłużyć się migracją, musimy jednak pamiętać, że każda migracja powinna być możliwa do cofnięcia – musimy więc stworzyć zarówno metodę up jak down

  • rescue_from … , with: …, logowanie własnej treści do LOGÓW
    Selection_003

    – Każdy kontroler ma atrybut logger
    – wyższość redirect_to nad render : zmienimy URL, w tym wypadku User nie „odświeży” błędu
    flash – dane z flash są przechowywane w sesji i dzięki temu dostępne między requestami – nie tracimy ich przy redirect

  • params => require, permit
    – params – coś a la Hash, obiekt klasy ActionController::Parameters
    – require, permit – metody
    – require – upewnia się o obecności danego klucza; jeżeli istnieje: zwraca jego zwartość; w przeciwnym razie: ActionController::ParameterMissing error
    – permit – ogranicza obiekt klasy Parameters to określonych kluczy, zwraca
    4_params
  • _path vs _url
    _url zwraca pełną ścieżkę (np http://localhost:300/articles/2)
  • partials + iteracje po kolekcji
    zamiast
    4_line_items_each
    możemy posłużyć się partialami
    5_line_items_partial

    – możemy przekazać kolekcję do metody, która renderuje
    – kiedy metoda render() dostaje kolekcję – domyślnie iteruje po jej elementach i renderuje partial dla każdego z elementów
    – wewnątrz partiala – mamy dostęp do zmiennej, która jest tym pojedynczym elementem, pod taką nazwą jak nazwa szablonu

  • trochę Ajaxa
    remote: true dodane do formularza, link_to, button_to – wysylanie pod dany adres zapytania AJAX wysylane POST-em
    – obsługa takiego zapytania format.js {}
    – tworzymy template w odpowiednim folderze views controller_action.js.erb
    – ramach .js.erb mozemy wyrenderowac jakis partial (z książki)
    js_erb
  • metoda helpera z blokiem
    6_helper_layout
    7_helper_methos

2. Prezentery i Dekoratory



Rozróżnienie pomiedzy tymi dwoma okazało się bardzo trudne. Terminy te są używane niekiedy naprzemiennie, a kiedyś już wydawało mi się, że zrozumiałam różnicę – trafiałam na przykład, który z powrotem sprowadzał mnie na ziemię.
Pozwolę sobie skwitować moje działania cytatem z bloga Robert Murray

Strictly speaking Decorators and Presenters are subtlety different and often open to interpretation but here we are using the decorator pattern to create a presenter

Dwa bardzo ważne pojęcia:

SimpleDelegator

  • Jest to klasa, której używamy do dziedziczenia
  • Dziedzicząc po SimpleDelegator, będziemy mieli dostęp do metod obiektu

Decorator pattern, Decorator design pattern

Prezentery

  • Czemu służą?
    • Usunięcie logiki z widoków
    • Usunięcie metod związanych z prezentowaniem z modeli
    • Ucieczka od fat helpers
  • do dzieła! przykłady z Railscasts
  • konfiguracja, czyli jak się do tego zabrać
    • tworzymy klasę naszego prezentera: app/presenters/user_presenter.rb
    • użycie – dwa sposoby: w kontrolerze albo w widoku
    • w kontrolerze
      1_presenter_config_controller

      Jedyną rzeczą, która może budzić tutaj wątpliwości to @template

      Do czego nam ta zmienna?
      Prezenter jest „naszym tworem” – nie posiada typowych dla widoków metod helpera, takich jak link_to czy image_tag
      W tym przykładzie nie wykonywaliśmy niczego „sensownego”, ale docelowo, skoro prezenter ma nam pomóc w warstwie widokowej – na pewno będą nam one potrzebne. Stąd potrzebujemy view_context
      Zastosowanie – w dalszym przykładzie.

    • w widoku – konieczna metoda w helperze
      1_presenter_config_apphelp

      Metoda w ApplicationHelper wymaga komentarza
      Po pierwsze – tworzymy ją w ApplicationHelper aby móc używać jej przy wszystkich prezenterach
      Skoro przy wszystkich, to musimy jakoś „uniwersalnie” wydobywać nazwę prezentera – tutaj kłania się konwencja. Warto nazywać prezentery od nazwy klasy obiektu.
      Do stworzenia prezentera używamy obiektu i szablonu (aby móc korzystać z helper methods) – będziemy przekazywali do metody present obiekt, natomiast view_context w tym wypadku to po prostu self – szablon z poziomu którego wywołaliśmy metodę helpera
      + yield presenter if block_given? – nad yieldem zawsze warto się zastanowić😉
      yield = ~wywołanie metody, która wywołuje blok powiązany z metodą wywołująca yield (tutaj: present)
      tutaj: ten blok to

      … do |user_presenter|
      user_presenter.mood
      end

      yield presenter wywoła ten blok, w miejsce user_presenter wstawiając nasz stworzony prezenter

  • Przykład: jeżeli mamy url avatara użytkownika – wyświetlamy jego zdjęcie jako link. Jeżeli nie wyświetlamy „default.jpg”
    2_presenter_example

    Po co metoda h zwracająca @template?
    Ponieważa bardzo często będziemy odwoływali się do @template, żeby mieć dostęp do metod.
    Przyjęło się, żę używamy h, żeby ułatwić sobie życie

  • refaktor
    Nie trudno sobie wyobrazić, że initialize i h, będą metodami, którymi będziemy posługiwali się w każdym prezenterze – warto je wydzielić do osobnego prezentera z którego inne będą dziedziczyły
    3_presenter_refactor

    Jakie presents? Jakie define method? Przecież miało być prościej…
    Wydzielając initialize do prezentera-rodzica, uogólniamy zmienną @object
    To oznacza, że zmiast @user.url.present? musielibyśmy użyć @object.url.present?
    Tragedii nie ma, da się zrobić, ale bardzo traci to na czytelności.
    dzięki self.presents(name) i wywołaniu presents :user wewnątrz każdego prezentera tworzona metoda o danej nazwie [tu: user], która zwraca @object i dzięki temu w prezenterze do danego obiektu będziemy mogli się odwoływać user.url.present?
    Innymi słowy – wywołanie w danym prezenterze presents :user robi to samo co dodanie metody

    def user
    @object
    end
  • linki linki

3. Gem Draper


    Do dzieła!

  • To gem, więc grzechem byłoby nie skorzystać z jego generatora:
    rails generate decorator article
    Tworzy: app/decorators/article_decorator.rb + spec/decorators/article_decorator_spec.rb
  • w pliku app/decorators/article_decorator.rb
    mamy już „szkielet klasy”

    draper_1

  • Z Draperem, dekorujemy obiekty w kontrolerze
    3 sposoby dekorowania

    • article = Article.find(params[:id])
      @article = ArticleDecorator.new(article)
    • @article= Article.find(params[:id]).decorate
    • @article = ArticleDecorator.find(params[:id])
      uwaga: wymaga decorates_finders w dekoratorze


  • b. ważne: Draper VS Helper ==> Obiektowo, a nie Proceduralnie!
    W helperze, chcąc sformatować wyświetlanie daty użylibyśmy
    %= format_data(@article)
    …a przecież tak bardzo chcemy być obiektowi!
    Z użyciem dekoratora będzie to wyglądało
    %= @article.format_data

  • 3 sposoby odwołania się do obiektu w dekoratorze Drapera: object – model – article
    draper_obj_mod_art

    Trzeci sposób jest oczywiście warunkowany zachowaniem konwencji nazewniczych (ArticleDecorator dla modelu Article)

    • dekorowanie wszystkich elementów kolekcji
      articles, @tag = Article.search_by_tag_name(params[:tag])
      @articles = ArticleDecorator.decorate_collection(articles)
    • dekorowanie kolekcji jako takiej
      Tworzymy dekorator kolekcji, dziedziczący po Draper::CollectionDecorator
      Draper_whole_collection
  • metody w dekoratorze
    • do metod helperowych (link_to, image_tag) odwołujemy się za pomocą h. (patrz tłumaczenie „h” w części o prezenterach)
      draper_h_image
    • możemy nadpisywać, tzn. zamiast deifniować formatted_created_at, możemy zdefiniować created_at i odwoływać się „normalnie”, otrzymując nowy format


  • uwspólnianie metod
    Możemy wyciągnąć metodę, którą chcielibyśmy mieć dostępną dla wszystkich modeli, tworząc ApplicationDecorator
    Pozostałe dekoratory dziedziczyłyby po nim.

    draper_appdecorator

  • b. ważne 2: Draper VS Helper ==> uciekamy od global helper namespace
    Możemy używać modułów
    Wyobraźmy sobie, że chcemy mieć metody, z których chcielibysmy korzystać w odniesieniu do kilku modeli… ale nie wszystkich!
    draper_module
  • ucieczka od używania h. przed każdą metodą helperową:
    include Draper::LazyHelpers
  • Paginacja + Draper

*PS. I teraz, kiedy już przejrzeliście linki z części drugiej, przejrześliście linki dot. Drapera… Prezenter a Dekorator…?🙂


Pomysły na przyszły tydzień:

  1. AWD with Rails 4 cd…
  2. Static model (yml)
  3. Co nowego w Rails 5?
W pogoni za object-oriented