How does UX transform businesses? The effects of usability testing
It has already become clear that User Experience (UX) is a key factor in shaping how companies build a business. The customer-centric approach has almost completely replaced […]
Kurs Ruby on Rails – Lekcja 9 – Funkcjonalności
Myślę, że nie ma drugiej tak często pojawiającej się rzeczy na stronach jak rejestracja i logowanie się. Na szczęście istnieje gem, który zrobi wszystko za nas z możliwością modyfikacji praktycznie każdej części. Nazywa się on Devise i można go znaleźć tu: https://github.com/heartcombo/devise
Najpierw przechodzimy przez sekcję Getting started
gdzie znajduje się cała instrukcja w jaki sposób dodać go do naszej aplikacji.
1. Dodajemy do naszego Gemfile gem 'devise'
i następnie w terminalu wołamy komendę bundle install
.
2. Gem potrzebuje kilka różnych plików konfiguracyjnych, aby go zainstalować wołamy w terminalu rails generate devise:install
3. Następnie, ponieważ devise używa mailera, musimy dodać dla niego URL naszej aplikacji. Ponieważ aktualnie używamy aplikacji tylko w trybie developerskim, wystarczy, że dodamy do config/environments/development.rb
linijkę:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
4. Teraz potrzebujemy wygenerować model oraz tabelę w bazie dla naszego użytkownika, która przechowywałaby takie informacje jak email i hasło. Osiągniemy to poprzez wywołanie komendy rails generate devise User
. User
jest nazwą naszego modelu.
Ponieważ posiadamy już w aplikacji model User
, devise to wykryje i zamiast tworzyć nowe pliki, doda potrzebne rzeczy do istniejących. Tyczy się to również migracji, zamiast tworzyć nową tabelę, zmodyfikuje tę, która istnieje.
Musimy teraz wywołać rake db:migrate
aby dodać wygenerowane zmiany do naszej bazy danych. Polecenie to najprawdopodobniej zwróci nam błąd UNIQUE constraint failed: users.email
.
Stało się tak, ponieważ migracja stworzona przez devise dodaje pole email
, które musi być unikalne, a każdy z naszych użytkowników dostaje domyślnie tam nil
, a więc jeśli mamy więcej niż jeden rekord użytkownika w bazie, wartości pól nie będą unikalne.
Jeśli mamy tylko jednego użytkownika, to również dostaniemy błąd, ponieważ pole to musi posiadać wartość.
Najłatwiejszym sposobem na rozwiązanie tego problemu, będzie stworzenie bazy danych od nowa poprzez polecenia (stracimy przy tym wszystkie dane):
rake db:drop
rake db:create
rake db:migrate
Istnieją sposoby na poradzenie sobie z tym w taki sposób, aby nie tracić danych, jednak zakładam, że nie ma w tej bazie nic ważnego, więc możemy pójść na skróty.
W tym momencie posiadamy już na stronie opcję rejestracji oraz logowania. Devise dodał również ścieżki do pliku routes
. Możemy wejść na /users/sign_up
gdzie zobaczymy formularz rejestracji, lub na users/sign_in
gdzie znajduje się logowanie.
Podczas próby rejestracji wyświetlą się błędy walidacyjne, mówiące o tym, że brakuje nam poprawnej wartości w polu name
. Można również zauważyć, że cały formularz wygląda dość ubogo. Możemy wygenerować widoki, z których devise korzysta, aby je dowolnie zmodyfikować:
rails generate devise:views
Możemy teraz dodać pole name
do widoku rejestracji oraz użyć klas CSS bootstrapa, aby cały formularz wyglądał bardziej przyjaźnie. Następnie, musimy zmodyfikować parametry jakie controller przepuszcza (permituje) przed zapisem użytkownika. Na githubie devisa możemy znaleźć sekcję Strong Parameters
, która dokładnie opisuje jak to zrobić.
Dodajemy do naszego ApplicationControllera (z którego dziedziczą wszystkie inne controllery, w tym te należące do devise):
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
end
end
Teraz możemy się zarejestrować uzyskując użytkownika z nazwą, adresem email oraz hasłem.
Mając tak skonfigurowany projekt, mamy teraz dużo dodatkowych możliwości, np:
before_action :authenticate_user!
w kontrolerze sprawi, że tylko zalogowany użytkownik będzie mógł “korzystać” z jego akcjiuser_signed_in?
, która zwróci true/false w zależności od tego czy użytkownik jest zalogowanycurrent_user
, która zwraca nam obiekt użytkownika aktualnie zalogowanego. Dzięki temu, możemy wyświetlać dane należące tylko do zalogowanego użytkownika np. current_user.posts
.User
, możemy dodawać/odejmować moduły devise, które np. pozwalają na wygasanie sesji. Wszystkie moduły są opisane na stronie devise.Bardzo często będziemy mieć do wyświetlenia jakąś tabelę z bardzo dużą ilością wierszy. Jednym ze sposobów poradzenia sobie z tym jest dodanie paginacji. Czasami wystarczy użyć paczki JS, która zrobi to po stronie przeglądarki, ale w tym przypadku serwer railsowy wciąż będzie procesować tą samą ilość danych, co może powodować długie czasy odpowiedzi.
Dodanie paginacji po stronie railsów jest bardzo proste, pomoże nam w tym gem kaminari
https://github.com/kaminari/kaminari.
Dodajemy gem do Gemfile gem 'kaminari'
i instalujemy za pomocą bundle install
Jako że przed chwilą usunęliśmy całą bazę danych, musimy sobie wygenerować kilka rekordów. Wpisujemy w konsoli railsowej:
user = User.create(email: "kursror2021@sample.com", password: "strongpwd", name: "Kurs")
100.times { |i| Post.create(user: user, title: "Post #{i}", body: "Posts body #{i}")}
Mając tyle rekordów, możemy poeksperymentować z możliwościami kaminari:
Post.count
=> 100
Zwróci ilość wszystkich postów jakie są w bazie danych.
Post.page(1).count
=> 25
Zwróci ilość postów na pierwszej stronie. Domyślnie strony będą wyświetlać 25 elementów.
Post.page(1).first.id
=> 1
Pierwszy element na pierwszej stronie ma ID 1, ponieważ paginacja nie zmienia kolejności.
Post.page().first.id
=> 1
Jeśli nie przekażemy żadnego parametru do metody page
, zostanie wyświetlona pierwsza strona.
Post.page(2).first.id
=> 26
Pierwszy element na drugiej stronie ma ID 26, co potwierdza fakt, że paginacja nie wpływa na kolejność.
Post.page(1).total_count
=> 100
Metoda total_count
, zwróci nam ilość wszystkich rekordów, niezależnie od paginacji.
Post.page(10).per(11).count
=> 1
Przy podziale 11 elementów na stronę, ostatnia będzie posiadać tylko 1 element.
Post.page(11).per(11).count
=> 0
Jeśli podamy numer strony większy niż jest możliwy, dostaniemy po prostu pustą kolekcję.
current_user = User.last
current_user.posts.page(2).per(15).count
=> 15
Możemy również dodać paginację do wywołania relacji.
Skoro już wiemy jak to działa, możemy teraz dodać paginację do widoku postów w naszej aplikacji. W tym celu w controllerze dodajemy wywołanie metody page:
def index
@posts = Posts::Sort.new(Posts::Recent.call).call.page(params[:page])
end
Następnie musimy dodać użytkownikowi możliwość przełączania stron. W widoku app/views/posts/index.html.erb
dodajemy linijkę:
<%= paginate @posts %>
Dodatkowo, możemy informować użytkownika o ilości stron i rekordów:
<%= page_entries_info @posts, entry_name: 'Posts' %>
Wygląda to niezbyt ciekawie, ale podobnie jak w przypadku devise, mamy możliwość wygenerowania plików widoków do paginacji. Dodatkowo, możemy je wygenerować już ostylowane dowolnym frameworkiem. Ponieważ używamy bootstrapa, wygenerujmy dla niego:
rails g kaminari:views bootstrap4
I teraz nasza paginacja wygląda bardziej zachęcająco.
Jeśli mamy potrzebę załączenia jakiegokolwiek pliku, czy to obrazka, czy dokumentu do modelu, możemy tego dokonać za pomocą wbudowanego w railsy ActiveStorage
https://github.com/rails/rails/tree/v6.1.4.6/activestorage.
Załóżmy, że chcemy dodać obrazek do naszego posta.
Najpierw, generujemy potrzebne pliki dla ActiveStorage:
rails active_storage:install
Wygenerowana zostanie migracja tworząca tabelę, która będzie przechowywać nasze pliki. Wołamy:
rake db:migrate
Teraz możemy w klasie Post
dodać linijkę:
has_one_attached :image
Teraz dodajemy w formularzu pole dla pliku:
<div class='mb-3 col-md-2'>
<%= form.label :image %>
<%= form.file_field :image, class: 'form-control' %>
</div>
A w PostsController
dodajemy image do permitted params
:
params.require(:post).permit(:title, :body, :user_id, :image)
Możemy wyświetlić obrazek w dowolnym miejscu, jednak najlepiej będzie to zrobić w show
, dodatkowo upewniając się, że dany post posiada obrazek do wyświetlenia:
<% if presenter.image.persisted? %>
Image: <%= image_tag presenter.image, width: '100px' %>
<% end %>
Dodatkowo, możemy wymusić po stronie przeglądarki akceptowanie plików w odpowiednim formacie:
<%= form.file_field :image, accept: 'image/png, image/jpg, image/jpeg', class: 'form-control' %>
Ponieważ wszystko, co dzieje się w przeglądarce może być w łatwy sposób oszukane, należy dodać walidację po stronie backendu. W pliku app/models/post.rb
dodajemy metodę, którą będziemy sprawdzać nasz obiekt:
private
def correct_image_file_type
if image.attached? && ['image/png', 'image/jpg', 'image/jpeg'].exclude?(image.content_type)
errors.add(:image, 'Must be jpg or png file')
end
end
A następnie używamy jej jako walidatora naszego modelu:
validate :correct_image_file_type
Możemy też użyć gema, który dodaje nam takie walidatory: https://github.com/aki77/activestorage-validator.
Gdy użytkownik wykonuje jakąś akcję, która polega na wysłaniu zapytania do serwera, musi on poczekać, aż odpowiednia akcja kontrolera wykona się w całości i zwróci odpowiedź. Czasem mamy potrzebę wykonać jakąś część logiki nie wstrzymując użytkownika lub przeprocesować coś niezależnie od tego, co się dzieje na stronie. Rozwiązaniem w tym przypadku jest ActiveJob, który domyślnie instaluje się razem z railsami https://github.com/rails/rails/tree/main/activejob.
Załóżmy, że chcemy zmodyfikować aktualny kod, by komentarz, który jest tworzony wraz z postem był tworzony asynchronicznie. Użyjemy takiego przypadku czysto na potrzeby kursu, ponieważ w rzeczywistości nie wymaga on używania procesowania w tle, ponieważ stworzenie jednego obiektu nie jest w żadnym stopniu czasochłonne. Prawdziwym przypadkiem użycia takiego mechanizmu, byłoby np. wysyłanie notyfikacji na telefon, podczas gdy ktoś komentuje nasz post. Wszystkie wiadomości email również są wysyłane asynchronicznie. Wynika to z łączenia się do innych serwisów, co może spowodować opóźnienie, a użytkownik, który wykonuje akcję wywołującą je w żadnym stopniu nie musi znać ich rezultatu.
Stwórzmy nową klasę: app/jobs/wellcome_comment_creation_job.rb
class WellcomeCommentCreationJob < ApplicationJob
queue_as :default
def perform
end
end
Nasza klasa składa się z nazwy oraz końcówki Job
, nie jest to wymagane, jednak taka jest konwencja. Dzięki temu, podczas używania tej klasy w aplikacji odrazu wiemy, że będzie ona wykonywać się asynchronicznie. Dodatkowo, musimy określić do jakiej kolejki będą trafiać zadania. Joby nie wykonują się wszystkie na raz, tylko po kolei. Tworząc różne kolejki możemy zdecydować, które są ważniejsze i nadawać priorytety, by odblokować te ważniejsze. Metoda perform
zawierać będzie logikę naszej klasy.
Potrzebujemy stworzyć komentarz do konkretnego postu, a więc nasz job, musi wiedzieć o jaki post chodzi. Modyfikujemy metodę perform w taki sposób, by przyjmowała post_id
. Nie zaleca się przekazywać pełnych obiektów, lepiej używać ID i w jobie pobierać obiekt z bazy danych.
def perform(post_id)
end
A następnie tworzymy logikę, która wykona to co chcemy:
post = Post.find(post_id)
Comment.create(post_id: post.id, user_id: post.user_id, content: "Hello post")
Dodatkowo, jeśli chcemy, możemy udać, że ta akcja wymaga trochę czasu aby się przeprocesować dodając w pierwszej linijce metody:
sleep(60)
Zatrzyma to wykonywanie kodu w tym miejscu na 60s.
Teraz możemy użyć naszego joba w miejscu, w którym chcemy. #app/commands/posts/create.rb
modyfikujemy kod by zamiast tworzyć komentarz synchronicznie, stworzył się za pomocą naszego joba:
def create_post_with_comment
post = Post.create!(@params)
WellcomeCommentCreationJob.perform_later(post.id)
end
Mamy kilka możliwości powołania do życia WellcomeCommentCreationJob. WellcomeCommentCreationJob.perform_later(post.id)
– doda go do kolejki i w swoim czasie wykona WellcomeCommentCreationJob.set(wait_until: Date.tomorrow.noon).perform_later(post.id)
– wykona go jutro w południe WellcomeCommentCreationJob.set(wait: 1.week).perform_later(post.id)
– wykona go za tydzień
Należy pamiętać, że nie jesteśmy w stanie przypisać wyniku naszego joba do zmiennej. Metoda perform_later
zwróci nam instancję WellcomeCommentCreationJob z takimi danymi jak nazwa kolejki i ID.
letter_opener
https://github.com/ryanb/letter_opener – dzięki niemu nie musimy konfigurować klienta SMTP do wysyłki maili. Dodatkowo nie będą się one nigdzie wysyłać, a otwierać w oknie naszej przeglądarki.pundit
https://github.com/varvet/pundit lub cancancan
https://github.com/CanCanCommunity/cancancan – służą do stworzenia systemu autoryzacji użytkownika. Możemy za jego pomocą sprawdzać, czy dany użytkownik może wykonać daną akcję lub użyć dany zasób (np. edytować czyjś post).sidekiq
https://github.com/mperham/sidekiq – alternatywa dla ActiveJob. Wymaga więcej konfiguracji, ale jest szybszy i ma więcej opcji.awesome_print
https://github.com/awesome-print/awesome_print – wyświetla w ładniejszy sposób dane w konsolipry
https://github.com/pry/pry lub byebug https://github.com/deivid-rodriguez/byebug – dzięki niemu mamy możliwość przerwania wykonywanie kodu i sprawdzenie stanu w danym momencie. Wystarczy wpisać w wybranym miejscu binding.pry
lub byebug
bullet
https://github.com/flyerhzm/bullet – pomaga w optymalizacji aplikacji wyszukując N+1 query.dotenv-rails
https://github.com/bkeepers/dotenv – konfiguruje aplikacje by zmienne systemowe (ENV) były wczytywane z pliku .env znajdującego się w projekcie.faraday
https://github.com/lostisland/faraday – HTTP client dzięki któremu łatwiej stworzymy zapytania do API innych aplikacjicurrent_user
.Przeczytaj również o...
It has already become clear that User Experience (UX) is a key factor in shaping how companies build a business. The customer-centric approach has almost completely replaced […]
What if there was one simple method to find opportunities for improved design, uncover UX problems, and learn about your target users’ behavior and preferences? Would you […]