2026-02-26 16:31
Diff
# bin/rails g job fetch_books_titles
# create test/jobs/fetch_books_titles_job_test.rb
# create app/jobs/fetch_books_titles_job.rb
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
queue_as :default
def perform(*args)
# Do something later
end
end
# test/jobs/fetch_books_titles_job_test.rb
require "test_helper"
class FetchBooksTitlesJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
queue_as :default
def perform
titles = Book.pluck(:title)
pp titles
end
end
# bin/rails g job fetch_books_titles --queue urgent
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
# --queue задает очередь в которую попадает задача
queue_as :urgent
def perform(*args)
# Do something later
end
end
# Вызов джобы
FetchBooksTitlesJob.perform_later # Джоба выполняется после выполнения основного процесса
FetchBooksTitlesJob.perform_now # Джоба запускается непосредственно
# Выполнить через 5 секунд
FetchBooksTitlesJob.set(wait: 5.seconds).perform_later
# Запустить через 10 секунд, в определенное время
FetchBooksTitlesJob.set(wait_until: Time.now + 10.seconds).perform_later
# config/application.rb
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Books
class Application < Rails::Application
config.load_defaults 6.1
# https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html
config.active_job.queue_adapter = :sidekiq
config.active_job_queue_name_prefix = Rails.new
config.active_job_queue_name_delimiter = '.'
end
end
# Запуск задачи в заданной очереди
FetchBooksTitlesJob.set(queue: :low_priority).perform_later
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
# Очередь задается динамически
# queue_as { cond ? :default : :high_priority }
def perform
titles = Book.pluck(:title)
pp titles
end
end
# Доступные коллбеки
# https://edgeguides.rubyonrails.org/active_job_basics.html#available-callbacks
before_enqueue
around_enqueue
after_enqueue
before_perform
around_perform
after_perform
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
around_perform :improve_output
def perform
titles = Book.pluck(:title)
pp titles
end
def improve_output
pp '*******************************************'
yield
pp '*******************************************'
end
end
# https://guides.rubyonrails.org/action_mailer_basics.html
# bin/rails g mailer new_book
# create app/mailers/new_book_mailer.rb
# create app/views/new_book_mailer
# create test/mailers/new_book_mailer_test.rb
# create test/mailers/previews/new_book_mailer_preview.rb
# app/mailers/new_book_mailer.rb
class NewBookMailer < ApplicationMailer
default from: 'book@example.com'
def new_book
mail(
to: 'user@examplle.com',
subject: 'New book'
)
end
end
# app/views/new_book_mailer/new_book.txt.erb
New book has been created
# Отправка письма
NewBookMailer.new_book.deliver_later
NewBookMailer.new_book.deliver_now
# https://github.com/mperham/sidekiq
# Для работы sidekiq требуется запущенный redis
# Для выполнения задач требуется запущенный sidekiq
# bundle exec sidekiq
# Gemfile
gem 'sidekiq'
# config/application.rb
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Books
class Application < Rails::Application
config.load_defaults 6.1
# https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html
config.active_job.queue_adapter = :sidekiq
end
end
# config/sidekiq.yml
# В этомф файле хранятся настройки sidekiq
# :concurrency: 2
# :logfile: ./log/sidekiq.log
# :queues:
# - default
# - mailers
# Open http://localhost:3000/sidekiq
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob
#
include Sidekiq::Worker
def perform
titles = Book.pluck(:title)
pp titles
end
def improve_output
pp '*******************************************'
yield
pp '*******************************************'
end
end
# Для асинхронных джоб с sidekiq изменяется способ их вызова
FetchBooksTitlesJob.perform_async
FetchBooksTitlesJob.perform_in(5.seconds)
# ActiveJob perform_now / perform_later
# Sidekiq perform_async / perform_in
<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 16:31:04 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="E3xUaHtH7XAPJsfsgjkp4UHeOeoeKK0S6UTwOYUt13T8rZ9fiTlAELll43SONtmWgdcUQBYfU7BUpGpt1yowGg";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Jobs | Ruby: Полный Rails</title>
<meta name="description" content="Jobs / Ruby: Полный Rails: Знакомимся с задачами">
<link rel="canonical" href="https://ru.hexlet.io/courses/rails-full/lessons/jobs/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Jobs">
<meta property="og:title" content="Ruby: Полный Rails">
<meta property="og:description" content="Jobs / Ruby: Полный Rails: Знакомимся с задачами">
<meta property="og:url" content="https://ru.hexlet.io/courses/rails-full/lessons/jobs/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="H5zBTyiCLcdE9hgvRKehdjP7EqjtWAiT8PiiQYHIeHbwTQp42vyAp_K1PLdIqFEB8_I_AuVv9jFNGDgV08-fGA" />
<script src="/vite/assets/inertia-INZxX8jp.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-6pOtQ3OW.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTExODcsInB1ciI6ImJsb2JfaWQifX0=--21b225d5b1f2f885f46e9ef4e79641fbea2f76dc/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Coding%20workshop-bro.webp"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T16:31:04.670Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"JwHM-Qd0n1u44l1iTcMf6uKDv8UUvvG3_oD6Om0u6hzI0AfO9QoyOw6hefpBzO-dIoqSbxyJDxVDYGBuPykNcg","topics":[{"id":90792,"title":"app/views/web/repositories/index.html.slim\n\nв update пропущен turbo_method","plain_title":"app/views/web/repositories/index.html.slim в update пропущен turbo_method ","creator":{"public_name":"","id":9733,"is_tutor":false},"comments":[{"creator":{"public_name":"Nikolai Gagarinov","id":104929,"is_tutor":true},"id":180088,"body":"Добрый день.\n\nСпасибо, поправил. Изменения вступят в течение часа. Чтобы получить новую версию, необходимо будет сделать сброс (инструкция на на странице ДЗ справа) и скачать заново домашнее задание.","topic_id":90792}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Jobs","entity_url":null,"active":true}},{"id":97340,"title":"Еще в этом и в предыдущем задании 'testing-2' я столкнулся с такой проблемой: при использовании flash notice, rails ругается на `- flash.each_value do |_type, msg|` в файле `app/views/layouts/web/shared/_flash.html.slim`.\n\nЧинится заменой `each_value` на `each`, вот так: `- flash.each do |_type, msg|`","plain_title":"Еще в этом и в предыдущем задании 'testing-2' я столкнулся с такой проблемой: при использовании flash notice, rails ругается на - flash.each_value do |_type, msg| в файле app/views/layouts/web/shared/_flash.html.slim. Чинится заменой each_value на each, вот так: - flash.each do |_type, msg| ","creator":{"public_name":"Иван Жуков","id":714922,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":188359,"body":"**Иван Жуков**, здравствйте! Поправил.","topic_id":97340}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Jobs","entity_url":null,"active":true}},{"id":97336,"title":"Здравствуйте.\n\nНаверно в `make setup` нужно добавить `yarn build` и `yarn build:css`, так как после `make setup` и `make start` приложение не может импортировать bootstrap и стартует с ошибкой:\n```\nError: Can't find stylesheet to import.\n1 | @import 'bootstrap/scss/bootstrap';\napp/assets/stylesheets/application.bootstrap.scss 1:9 root stylesheet\n```","plain_title":"Здравствуйте. Наверно в make setup нужно добавить yarn build и yarn build:css, так как после make setup и make start приложение не может импортировать bootstrap и стартует с ошибкой: Error: Can't find stylesheet to import. 1 | @import 'bootstrap/scss/bootstrap'; app/assets/stylesheets/application.bootstrap.scss 1:9 root stylesheet ","creator":{"public_name":"Иван Жуков","id":714922,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":188353,"body":"**Иван Жуков**, здравствуйте! Спасибо! Перенес команды в сетап.","topic_id":97336}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Jobs","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":4338,"slug":"rails_full_jobs_exercise","name":null,"state":"active","kind":"exercise","language":"ruby","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"Некоторые задачи могут быть ресурсоемкими, долгими или выполняться в несколько этапов.\n\nВ этом задании создадим джобу, в которой будем загружать информацию о репозиториях.\n\nДля модели `Repository` описан конечный автомат:\n\n* `created` — начальное состояние добавленного репозитория\n* `fetching` — при переходе в это состояние начинается загрузка данных о репозитории\n* `fetched` — переход в это состояние происходит при успешной загрузке информации о репозитории\n* `failed` — переход в состояние выполняется, если при запросе информации произошла какая-либо ошибка. Если репозиторий был загружен с ошибкой, то его можно заново загрузить.\n\nПри обращении к API *http://localhost:3000* можно получить информацию о репозитории. К этому API обращается Octokit.\n\n```bash\ncurl http://localhost:3000/repos/hexlet/hexlet-sicp\n{\n \"id\": 2,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"hexlet-sicp\",\n \"full_name\": \"hexlet/hexlet-sicp\",\n # ...\n}\n```\n\nAPI отдает информацию только для репозиториев:\n\n* hexlet/hexlet-sicp\n* hexlet/hexlet-cv\n* hexlet/hexlet-friends\n* hexlet/hexlet-test\n\nДля всех остальных ничего не вернется. Примеры ответов можно посмотреть в директории фикстур *test/fixtures/files*.\n\n### app/jobs/repository_loader_job.rb\n\nСоздайте и реализуйте задачу `RepositoryLoaderJob`, которая обновляет информацию о репозитории с GitHub. Из API должны заполняться следующие поля:\n\n* repo_name\n* owner_name\n* description\n* default_branch\n* watchers_count\n* language\n* repo_created_at\n* repo_updated_at\n\nПри успешном получении репозитория, должно заполняться поле *fetched_at* с текущим временем.\n\n### app/controllers/web/repositories_controller.rb\n\nДопишите методы контроллера:\n\n* `create()` — создание репозитория. Новый репозиторий добавляется по ссылке, например `https://github.com/hexlet-basics/hexlet-test`. Загрузка информации о репозитории выполняется с помощью джобы `RepositoryLoaderJob`\n* `update()` — с помощью задачи `RepositoryLoaderJob` выполняется загрузка свежей информации о репозитории\n* `update_repos()` — обновление всех репозиториев. Для каждого репозитория ставится задача в очередь на выполнение. Порядок обновления зависит от даты обновления. Сперва старые, а затем новые.\n","prepared_readme":"Некоторые задачи могут быть ресурсоемкими, долгими или выполняться в несколько этапов.\n\nВ этом задании создадим джобу, в которой будем загружать информацию о репозиториях.\n\nДля модели `Repository` описан конечный автомат:\n\n* `created` — начальное состояние добавленного репозитория\n* `fetching` — при переходе в это состояние начинается загрузка данных о репозитории\n* `fetched` — переход в это состояние происходит при успешной загрузке информации о репозитории\n* `failed` — переход в состояние выполняется, если при запросе информации произошла какая-либо ошибка. Если репозиторий был загружен с ошибкой, то его можно заново загрузить.\n\nПри обращении к API *http://localhost:3000* можно получить информацию о репозитории. К этому API обращается Octokit.\n\n```bash\ncurl http://localhost:3000/repos/hexlet/hexlet-sicp\n{\n \"id\": 2,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n \"name\": \"hexlet-sicp\",\n \"full_name\": \"hexlet/hexlet-sicp\",\n # ...\n}\n```\n\nAPI отдает информацию только для репозиториев:\n\n* hexlet/hexlet-sicp\n* hexlet/hexlet-cv\n* hexlet/hexlet-friends\n* hexlet/hexlet-test\n\nДля всех остальных ничего не вернется. Примеры ответов можно посмотреть в директории фикстур *test/fixtures/files*.\n\n### app/jobs/repository_loader_job.rb\n\nСоздайте и реализуйте задачу `RepositoryLoaderJob`, которая обновляет информацию о репозитории с GitHub. Из API должны заполняться следующие поля:\n\n* repo_name\n* owner_name\n* description\n* default_branch\n* watchers_count\n* language\n* repo_created_at\n* repo_updated_at\n\nПри успешном получении репозитория, должно заполняться поле *fetched_at* с текущим временем.\n\n### app/controllers/web/repositories_controller.rb\n\nДопишите методы контроллера:\n\n* `create()` — создание репозитория. Новый репозиторий добавляется по ссылке, например `https://github.com/hexlet-basics/hexlet-test`. Загрузка информации о репозитории выполняется с помощью джобы `RepositoryLoaderJob`\n* `update()` — с помощью задачи `RepositoryLoaderJob` выполняется загрузка свежей информации о репозитории\n* `update_repos()` — обновление всех репозиториев. Для каждого репозитория ставится задача в очередь на выполнение. Порядок обновления зависит от даты обновления. Сперва старые, а затем новые.\n","has_solution":true,"entity_name":"Jobs"},"units":[{"id":4856,"name":"theory","url":"/courses/rails-full/lessons/jobs/theory_unit"},{"id":14578,"name":"quiz","url":"/courses/rails-full/lessons/jobs/quiz_unit"},{"id":14564,"name":"exercise","url":"/courses/rails-full/lessons/jobs/exercise_unit"}],"links":[{"id":422837,"name":"Rails Guides -Active Job Basics","url":"https://guides.rubyonrails.org/active_job_basics.html\n"}],"ordered_units":[{"id":4856,"name":"theory","url":"/courses/rails-full/lessons/jobs/theory_unit"},{"id":14578,"name":"quiz","url":"/courses/rails-full/lessons/jobs/quiz_unit"},{"id":14564,"name":"exercise","url":"/courses/rails-full/lessons/jobs/exercise_unit"}],"id":2180,"slug":"jobs","state":"approved","name":"Jobs","course_order":600,"goal":"Знакомимся с задачами","self_study":"Создайте Rails-проект, если его еще нет.\n\nСоздайте задачу Active Job, который отправляет приветственное письмо пользователю после регистрации.\n","theory_video_provider":"vimeo","theory_video_uid":"637815176","theory":"## Программа урока\n\n* Создание Active Job\n* Бэкенд и коллбеки Active Job\n* Отправка писем, обработка ошибок с Active Job\n\n```ruby\n# bin/rails g job fetch_books_titles\n# create test/jobs/fetch_books_titles_job_test.rb\n# create app/jobs/fetch_books_titles_job.rb\n\n# app/jobs/fetch_books_titles_job.rb\nclass FetchBooksTitlesJob < ApplicationJob\n queue_as :default\n\n def perform(*args)\n # Do something later\n end\nend\n\n# test/jobs/fetch_books_titles_job_test.rb\nrequire \"test_helper\"\n\nclass FetchBooksTitlesJobTest < ActiveJob::TestCase\n # test \"the truth\" do\n # assert true\n # end\nend\n\n\n# app/jobs/fetch_books_titles_job.rb\nclass FetchBooksTitlesJob < ApplicationJob\n queue_as :default\n\n def perform\n titles = Book.pluck(:title)\n pp titles\n end\nend\n\n# bin/rails g job fetch_books_titles --queue urgent\n# app/jobs/fetch_books_titles_job.rb\nclass FetchBooksTitlesJob < ApplicationJob\n # --queue задает очередь в которую попадает задача\n queue_as :urgent\n\n def perform(*args)\n # Do something later\n end\nend\n\n# Вызов джобы\nFetchBooksTitlesJob.perform_later # Джоба выполняется после выполнения основного процесса\nFetchBooksTitlesJob.perform_now # Джоба запускается непосредственно\n\n# Выполнить через 5 секунд\nFetchBooksTitlesJob.set(wait: 5.seconds).perform_later\n# Запустить через 10 секунд, в определенное время\nFetchBooksTitlesJob.set(wait_until: Time.now + 10.seconds).perform_later\n\n# config/application.rb\nrequire_relative \"boot\"\n\nrequire \"rails/all\"\n\n# Require the gems listed in Gemfile, including any gems\n# you've limited to :test, :development, or :production.\nBundler.require(*Rails.groups)\n\nmodule Books\n class Application < Rails::Application\n config.load_defaults 6.1\n\n # https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html\n config.active_job.queue_adapter = :sidekiq\n config.active_job_queue_name_prefix = Rails.new\n config.active_job_queue_name_delimiter = '.'\n end\nend\n\n# Запуск задачи в заданной очереди\nFetchBooksTitlesJob.set(queue: :low_priority).perform_later\n\n# app/jobs/fetch_books_titles_job.rb\nclass FetchBooksTitlesJob < ApplicationJob\n # Очередь задается динамически\n # queue_as { cond ? :default : :high_priority }\n\n def perform\n titles = Book.pluck(:title)\n pp titles\n end\nend\n\n# Доступные коллбеки\n# https://edgeguides.rubyonrails.org/active_job_basics.html#available-callbacks\nbefore_enqueue\naround_enqueue\nafter_enqueue\nbefore_perform\naround_perform\nafter_perform\n\n# app/jobs/fetch_books_titles_job.rb\nclass FetchBooksTitlesJob < ApplicationJob\n around_perform :improve_output\n\n def perform\n titles = Book.pluck(:title)\n pp titles\n end\n\n def improve_output\n pp '*******************************************'\n yield\n pp '*******************************************'\n end\nend\n\n# https://guides.rubyonrails.org/action_mailer_basics.html\n# bin/rails g mailer new_book\n# create app/mailers/new_book_mailer.rb\n# create app/views/new_book_mailer\n# create test/mailers/new_book_mailer_test.rb\n# create test/mailers/previews/new_book_mailer_preview.rb\n\n# app/mailers/new_book_mailer.rb\nclass NewBookMailer < ApplicationMailer\n default from: 'book@example.com'\n\n def new_book\n mail(\n to: 'user@examplle.com',\n subject: 'New book'\n )\n end\nend\n\n# app/views/new_book_mailer/new_book.txt.erb\nNew book has been created\n\n# Отправка письма\nNewBookMailer.new_book.deliver_later\nNewBookMailer.new_book.deliver_now\n\n\n\n# https://github.com/mperham/sidekiq\n# Для работы sidekiq требуется запущенный redis\n# Для выполнения задач требуется запущенный sidekiq\n# bundle exec sidekiq\n\n# Gemfile\ngem 'sidekiq'\n\n# config/application.rb\nrequire_relative \"boot\"\n\nrequire \"rails/all\"\n\n# Require the gems listed in Gemfile, including any gems\n# you've limited to :test, :development, or :production.\nBundler.require(*Rails.groups)\n\nmodule Books\n class Application < Rails::Application\n config.load_defaults 6.1\n\n # https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html\n config.active_job.queue_adapter = :sidekiq\n end\nend\n\n# config/sidekiq.yml\n# В этомф файле хранятся настройки sidekiq\n# :concurrency: 2\n# :logfile: ./log/sidekiq.log\n# :queues:\n# - default\n# - mailers\n\n# Open http://localhost:3000/sidekiq\n\n# app/jobs/fetch_books_titles_job.rb\nclass FetchBooksTitlesJob\n #\n include Sidekiq::Worker\n\n def perform\n titles = Book.pluck(:title)\n pp titles\n end\n\n def improve_output\n pp '*******************************************'\n yield\n pp '*******************************************'\n end\nend\n\n# Для асинхронных джоб с sidekiq изменяется способ их вызова\nFetchBooksTitlesJob.perform_async\nFetchBooksTitlesJob.perform_in(5.seconds)\n\n# ActiveJob perform_now / perform_later\n# Sidekiq perform_async / perform_in\n```\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":{"id":4334,"slug":"rails_full_api_exercise","name":null,"state":"active","kind":"exercise","language":"ruby","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"В предыдущих домашних заданиях мы реализовывали так называемые \"классические\" Web-приложения состоящие из страниц гипертекста, отображаемых в браузере. Такие приложения ориентированы на взаимодействие с человеком - пользователем сайта. Но часто возникает задача когда одно приложение должно взаимодействовать с другим, и использование HTML разметки с формами не является удобным решением. Для таких случаев в приложении добавляют API или Application Programming Interface (Программный интерфейс приложения) — протокол взаимодействия между вашим приложением и другими программами, который позволяет обмениваться информацией с использованием, например, JSON формата.\n\n### app/controllers/api/application_controller.rb\n\nУстановите формат ответа по умолчанию JSON, используя метод `respond_to()`.\n\n### app/controllers/api/users_controller.rb\n\nРеализуйте обработчики `show()` для получения данных об одном пользователе и `index()` — для получения данных о всех пользователей. Используйте метод `respond_with()` для представления сущности в формате JSON. Такой подход, когда отдаются все поля не совсем является корректным, потому что сущность может содержать поля, которые нужно скрыть, например, в целях безопасности. Как правильно решается эта задача, мы рассмотрим в следующем домашнем задании.\n\n### config/routes.rb\n\nДля роутинга будем использовать знания, полученные из прошлых уроков, поэтому в неймспейс *api* добавьте ресурc *users* с методами `index()` и `show()`.\n\nЭндпоинты:\n\n* **GET** *api/users.json* - получение списка пользователей отсортированных по *id* по возрастанию\n* **GET** *api/users/:id.json* - получение информации о пользователе\n\nВ API не должно быть полей *password_digest*, *created_at*, *updated_at*.\n\nПроверьте в веб-доступе или терминале, данные отдаются в JSON формате.\n\n\n```bash\ncurl http://localhost:8080/api/users/1.json\n{\n \"id\": 1,\n \"first_name\": \"Sophie\",\n \"last_name\": null,\n \"email\": \"sam_fadel@frami.test\",\n \"address\": \"Phoebe's Apartment\",\n \"balance\": \"8163\",\n \"state\": \"archive\"\n}\n```\n\n```bash\ncurl http://localhost:8080/api/users.json\n[\n {\n \"id\": 1,\n \"first_name\": \"Sophie\",\n \"last_name\": null,\n \"email\": \"sam_fadel@frami.test\",\n \"address\": \"Phoebe's Apartment\",\n \"balance\": \"8163\",\n \"state\": \"archive\"\n },\n ...\n]\n```\n\n## Подсказки\n\n* [as_json()](https://apidock.com/rails/ActiveModel/Serializers/JSON/as_json)\n","prepared_readme":"В предыдущих домашних заданиях мы реализовывали так называемые \"классические\" Web-приложения состоящие из страниц гипертекста, отображаемых в браузере. Такие приложения ориентированы на взаимодействие с человеком - пользователем сайта. Но часто возникает задача когда одно приложение должно взаимодействовать с другим, и использование HTML разметки с формами не является удобным решением. Для таких случаев в приложении добавляют API или Application Programming Interface (Программный интерфейс приложения) — протокол взаимодействия между вашим приложением и другими программами, который позволяет обмениваться информацией с использованием, например, JSON формата.\n\n### app/controllers/api/application_controller.rb\n\nУстановите формат ответа по умолчанию JSON, используя метод `respond_to()`.\n\n### app/controllers/api/users_controller.rb\n\nРеализуйте обработчики `show()` для получения данных об одном пользователе и `index()` — для получения данных о всех пользователей. Используйте метод `respond_with()` для представления сущности в формате JSON. Такой подход, когда отдаются все поля не совсем является корректным, потому что сущность может содержать поля, которые нужно скрыть, например, в целях безопасности. Как правильно решается эта задача, мы рассмотрим в следующем домашнем задании.\n\n### config/routes.rb\n\nДля роутинга будем использовать знания, полученные из прошлых уроков, поэтому в неймспейс *api* добавьте ресурc *users* с методами `index()` и `show()`.\n\nЭндпоинты:\n\n* **GET** *api/users.json* - получение списка пользователей отсортированных по *id* по возрастанию\n* **GET** *api/users/:id.json* - получение информации о пользователе\n\nВ API не должно быть полей *password_digest*, *created_at*, *updated_at*.\n\nПроверьте в веб-доступе или терминале, данные отдаются в JSON формате.\n\n\n```bash\ncurl http://localhost:8080/api/users/1.json\n{\n \"id\": 1,\n \"first_name\": \"Sophie\",\n \"last_name\": null,\n \"email\": \"sam_fadel@frami.test\",\n \"address\": \"Phoebe's Apartment\",\n \"balance\": \"8163\",\n \"state\": \"archive\"\n}\n```\n\n```bash\ncurl http://localhost:8080/api/users.json\n[\n {\n \"id\": 1,\n \"first_name\": \"Sophie\",\n \"last_name\": null,\n \"email\": \"sam_fadel@frami.test\",\n \"address\": \"Phoebe's Apartment\",\n \"balance\": \"8163\",\n \"state\": \"archive\"\n },\n ...\n]\n```\n\n## Подсказки\n\n* [as_json()](https://apidock.com/rails/ActiveModel/Serializers/JSON/as_json)\n","has_solution":true,"entity_name":"API"},"units":[{"id":4839,"name":"theory","url":"/courses/rails-full/lessons/api/theory_unit"},{"id":14574,"name":"quiz","url":"/courses/rails-full/lessons/api/quiz_unit"},{"id":14561,"name":"exercise","url":"/courses/rails-full/lessons/api/exercise_unit"}],"links":[{"id":422793,"name":"Гем responders","url":"https://github.com/heartcombo/responders"}],"ordered_units":[{"id":4839,"name":"theory","url":"/courses/rails-full/lessons/api/theory_unit"},{"id":14574,"name":"quiz","url":"/courses/rails-full/lessons/api/quiz_unit"},{"id":14561,"name":"exercise","url":"/courses/rails-full/lessons/api/exercise_unit"}],"id":2167,"slug":"api","state":"approved","name":"API","course_order":200,"goal":"Знакомимся с программным интерфейсом приложения","self_study":"Если у вас нет Rails-проекта для экспериментов, то создайте его.\n\n\nНапишем небольшой сервис для создания заказов. Для этого вам понадобятся модели товара, корзины и заказа.\n\n* Подключите в Rails-проект гем [responders](https://github.com/heartcombo/responders).\n* Создайте API-метод для получения списка товаров.\n* Создайте методы для работы с корзиной.\n * Метод создания корзины. Метод принимает массив с позициями ID, количество и возвращает ID корзины\n * Метод обновления корзины для добавления новых товаров или удаления\n * Метод получения товаров.\n* Создайте метод создания заказа, он должен получать ID корзины и возвращать код ответа 201 с номером заказа. При создании заказа, корзина удаляется.\n\nВ итоге должен получиться небольшой API сервис с несколькими эндпоинтами.\n","theory_video_provider":"vimeo","theory_video_uid":"628992748","theory":"## Программа урока\n\n* Роутинг. Форматы ответа сервера. Тесты\n* Responders\n* Версионирование API\n\n```ruby\n# Scaffold генерирует в контроллерах методы, которые работают с несколькими форматами\nclass BooksController < ApplicationController\n before_action :set_book, only: %i[ show edit update destroy ]\n\n # Также как с шаблонами, ищется во вью файл для показа в нужном формате (html, json)\n # GET /books or /books.json\n def index\n @books = Book.all\n end\n\n # GET /books/1 or /books/1.json\n def show\n end\n\n # GET /books/new\n def new\n @book = Book.new\n end\n\n # GET /books/1/edit\n def edit\n end\n\n # POST /books or /books.json\n def create\n @book = Book.new(book_params)\n\n respond_to do |format|\n if @book.save\n format.html { redirect_to @book, notice: \"Book was successfully created.\" }\n format.json { render :show, status: :created, location: @book }\n else\n format.html { render :new, status: :unprocessable_entity }\n format.json { render json: @book.errors, status: :unprocessable_entity }\n end\n end\n end\n\n # PATCH/PUT /books/1 or /books/1.json\n def update\n respond_to do |format|\n if @book.update(book_params)\n format.html { redirect_to @book, notice: \"Book was successfully updated.\" }\n format.json { render :show, status: :ok, location: @book }\n else\n format.html { render :edit, status: :unprocessable_entity }\n format.json { render json: @book.errors, status: :unprocessable_entity }\n end\n end\n end\n\n # DELETE /books/1 or /books/1.json\n def destroy\n @book.destroy\n respond_to do |format|\n format.html { redirect_to books_url, notice: \"Book was successfully destroyed.\" }\n format.json { head :no_content }\n end\n end\n\n private\n # Use callbacks to share common setup or constraints between actions.\n def set_book\n @book = Book.find(params[:id])\n end\n\n # Only allow a list of trusted parameters through.\n def book_params\n params.require(:book).permit(:title)\n end\nend\n\nclass BooksController < ApplicationController\n before_action :set_book, only: %i[ show edit update destroy ]\n\n # GET /books or /books.json\n def index\n @books = Book.all\n\n # respond_to переопределяет стандартное поведение\n # запрос к /books.json выводит {\"hi\": \"books\"}\n # https://apidock.com/rails/ActionController/MimeResponds/InstanceMethods/respond_to\n respond_to do |format|\n format.html do\n redirect_to root_path\n end\n format.json do\n render json: { hi: 'Books' }\n end\n end\n end\nend\n\n# format приходит в переменную params\n# {\"controller\":\"books\",\"action\":\"index\",\"format\":\"json\"}\n# bin/rails routes -g books\n# Prefix Verb URI Pattern Controller#Action\n# books GET /books(.:format) books#index\n# POST /books(.:format) books#create\n# new_book GET /books/new(.:format) books#new\n# edit_book GET /books/:id/edit(.:format) books#edit\n# book GET /books/:id(.:format) books#show\n# PATCH /books/:id(.:format) books#update\n# PUT /books/:id(.:format) books#update\n# DELETE /books/:id(.:format) books#destroy\n\n\nclass BooksController < ApplicationController\n before_action :set_book, only: %i[ show edit update destroy ]\n\n # GET /books or /books.json\n def index\n # метод принимает только запросы вида /books.json и отвечает только в json\n respond_to :json\n\n @books = Book.all\n end\nend\n\nclass BooksController < ApplicationController\n before_action :set_book, only: %i[ show edit update destroy ]\n\n def show\n respond_to do |format|\n format.html { redirect_to root_path }\n # При преобразовании вызывается метод to_json()\n # https://apidock.com/rails/ActiveRecord/Serialization/to_json\n format.json { render json: @book.to_json, root: true }\n # root: true кладет сущность в под ключем\n # => {\"book\":{\"id\":1,\"title\":\"example\",\"created_at\":\"2021-11-10T11:57:36.381Z\",\"updated_at\":\"2021-11-10T11:57:36.381Z\"}}\n end\n end\nend\n\nclass BooksController < ApplicationController\n # GET /books/1 or /books/1.json\n def show\n respond_to :json\n # as_json позволяет выводить только определенные поля\n render json: @book.as_json(\n root: true,\n only: :title\n )\n # => {\"book\":{\"title\":\"example\"}}\n end\nend\n\nRails.application.routes.draw do\n resources :books\n\n # api логика лежит отдельно\n namespace :api do\n # изолируем изменения на каждой версии api\n namespace :v1 do\n resources :books\n end\n end\nend\n\n# api_v1_books GET /api/v1/books(.:format) api/v1/books#index\n# POST /api/v1/books(.:format) api/v1/books#create\n# new_api_v1_book GET /api/v1/books/new(.:format) api/v1/books#new\n# edit_api_v1_book GET /api/v1/books/:id/edit(.:format) api/v1/books#edit\n# api_v1_book GET /api/v1/books/:id(.:format) api/v1/books#show\n# PATCH /api/v1/books/:id(.:format) api/v1/books#update\n# PUT /api/v1/books/:id(.:format) api/v1/books#update\n# DELETE /api/v1/books/:id(.:format) api/v1/books#destroy\n\n# app/controllers/api/application_controller.rb\n# Работает для всех версий\n# Перед этим необходимо подключить гем gem \"responders\"\nclass Api::ApplicationController < ApplicationController\n respond_to :json\nend\n\n# app/controllers/api/v1/application_controller.rb\nclass Api::V1::ApplicationController < Api::ApplicationController\nend\n\n# app/controllers/api/v1/books_controller.rb\nclass Api::V1::BooksController < Api::V1::ApplicationController\n # /api/v1/books.json\n def index\n @books = Books.all\n\n # Без гема responders выглядело бы так\n # render json: @books.as_json(only: :title)\n respond_with @books.as_json(only: :title)\n end\nend\n\nclass Api::V1::BooksControllerTest < ActionDispatch::IntegrationTest\n setup do\n @book = books(:one)\n end\n\n test 'should get api index' do\n get api_books_url(format: :json)\n assert_response :success\n end\n\n test 'should get api show' do\n get api_book_url(@book, format: :json)\n assert_response :success\n end\nend\n```\n"},"id":243,"slug":"rails-full","challenges_count":0,"name":"Ruby: Полный Rails","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы погрузитесь в особенности Ruby on Rails. Вы узнаете о стриминге, Webmock и Rails Engines. В итоге научитесь использовать очереди для обработки тяжелых запросов и кеширование. Это поможет разработать свое API в соответствии с REST архитектурой.","kind":"additional","updated_at":"2026-01-20T11:39:01.186Z","language":"ruby","duration_cache":33900,"skills":["Использовать асинхронные задачи","Строить REST-api","Тестировать код с побочными эффектами","Представлять данные в разных форматах"],"keywords":[],"lessons_count":7,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTk5NywicHVyIjoiYmxvYl9pZCJ9fQ==--887f298cbbb1aea83535ad9973f4e692534eb468/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[{"stack":{"id":475,"slug":"rails","title":"Ruby on Rails","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"not_finished","order":null,"duration_in_months":4},"id":632,"slug":"rails","title":"Разработка на Ruby on Rails","subtitle":"Изучите Ruby, Rails и проектирование REST API","subtitle_for_lists":"Изучите Ruby, Rails и проектирование REST API","locale":"ru","current":true,"duration_in_months_text":"4 месяца","stack_slug":"rails","price_text":"от 3 900 ₽","duration_text":"4 месяца","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTExODcsInB1ciI6ImJsb2JfaWQifX0=--21b225d5b1f2f885f46e9ef4e79641fbea2f76dc/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Coding%20workshop-bro.webp"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/rails-full/lessons/jobs/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Ruby: Полный Rails</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Jobs</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Jobs","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Ruby: Полный Rails"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><h2 id="heading-2-1">Программа урока</h2>
<ul>
<li>Создание Active Job</li>
<li>Бэкенд и коллбеки Active Job</li>
<li>Отправка писем, обработка ошибок с Active Job</li>
</ul>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code"># bin/rails g job fetch_books_titles
# create test/jobs/fetch_books_titles_job_test.rb
# create app/jobs/fetch_books_titles_job.rb
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
queue_as :default
def perform(*args)
# Do something later
end
end
# test/jobs/fetch_books_titles_job_test.rb
require "test_helper"
class FetchBooksTitlesJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
queue_as :default
def perform
titles = Book.pluck(:title)
pp titles
end
end
# bin/rails g job fetch_books_titles --queue urgent
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
# --queue задает очередь в которую попадает задача
queue_as :urgent
def perform(*args)
# Do something later
end
end
# Вызов джобы
FetchBooksTitlesJob.perform_later # Джоба выполняется после выполнения основного процесса
FetchBooksTitlesJob.perform_now # Джоба запускается непосредственно
# Выполнить через 5 секунд
FetchBooksTitlesJob.set(wait: 5.seconds).perform_later
# Запустить через 10 секунд, в определенное время
FetchBooksTitlesJob.set(wait_until: Time.now + 10.seconds).perform_later
# config/application.rb
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Books
class Application < Rails::Application
config.load_defaults 6.1
# https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html
config.active_job.queue_adapter = :sidekiq
config.active_job_queue_name_prefix = Rails.new
config.active_job_queue_name_delimiter = '.'
end
end
# Запуск задачи в заданной очереди
FetchBooksTitlesJob.set(queue: :low_priority).perform_later
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
# Очередь задается динамически
# queue_as { cond ? :default : :high_priority }
def perform
titles = Book.pluck(:title)
pp titles
end
end
# Доступные коллбеки
# https://edgeguides.rubyonrails.org/active_job_basics.html#available-callbacks
before_enqueue
around_enqueue
after_enqueue
before_perform
around_perform
after_perform
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob < ApplicationJob
around_perform :improve_output
def perform
titles = Book.pluck(:title)
pp titles
end
def improve_output
pp '*******************************************'
yield
pp '*******************************************'
end
end
# https://guides.rubyonrails.org/action_mailer_basics.html
# bin/rails g mailer new_book
# create app/mailers/new_book_mailer.rb
# create app/views/new_book_mailer
# create test/mailers/new_book_mailer_test.rb
# create test/mailers/previews/new_book_mailer_preview.rb
# app/mailers/new_book_mailer.rb
class NewBookMailer < ApplicationMailer
default from: 'book@example.com'
def new_book
mail(
to: 'user@examplle.com',
subject: 'New book'
)
end
end
# app/views/new_book_mailer/new_book.txt.erb
New book has been created
# Отправка письма
NewBookMailer.new_book.deliver_later
NewBookMailer.new_book.deliver_now
# https://github.com/mperham/sidekiq
# Для работы sidekiq требуется запущенный redis
# Для выполнения задач требуется запущенный sidekiq
# bundle exec sidekiq
# Gemfile
gem 'sidekiq'
# config/application.rb
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Books
class Application < Rails::Application
config.load_defaults 6.1
# https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html
config.active_job.queue_adapter = :sidekiq
end
end
# config/sidekiq.yml
# В этомф файле хранятся настройки sidekiq
# :concurrency: 2
# :logfile: ./log/sidekiq.log
# :queues:
# - default
# - mailers
# Open http://localhost:3000/sidekiq
# app/jobs/fetch_books_titles_job.rb
class FetchBooksTitlesJob
#
include Sidekiq::Worker
def perform
titles = Book.pluck(:title)
pp titles
end
def improve_output
pp '*******************************************'
yield
pp '*******************************************'
end
end
# Для асинхронных джоб с sidekiq изменяется способ их вызова
FetchBooksTitlesJob.perform_async
FetchBooksTitlesJob.perform_in(5.seconds)
# ActiveJob perform_now / perform_later
# Sidekiq perform_async / perform_in</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/rails?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">4 месяца</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Разработка на Ruby on Rails</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите Ruby, Rails и проектирование REST API</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTExODcsInB1ciI6ImJsb2JfaWQifX0=--21b225d5b1f2f885f46e9ef4e79641fbea2f76dc/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Coding%20workshop-bro.webp" alt="Разработка на Ruby on Rails" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/rails-full/lessons/jobs/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 7</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/rails-full/lessons/jobs/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-CdBlNCiQ.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>