Сделать назначение программ обучения: администратор назначает группе пользователей и/или конкретным пользователям курсы и/или тесты, а они их проходят.
Все детали прорабатывать не надо, достаточно за час-два сделать ключевое, на Ваш взгляд, в этой задаче, чтобы остальные члены команды могли доделать мелочи.
Процесс буду описывать последовательно, шаг за шагом. В конце приведён TODO, чтобы "...остальные члены команды могли доделать мелочи".
- user
- role: роль пользователя: в рамках задачи добавил 2 роли:
:user
и:admin
- group: группа пользователей: в рамках задачи добавил 4 группы:
:msk
,:spb
,:sochi
,:no_group
- task: курсы и/или тесты
- assignment: связывает модели User и Task
- status: статус assignment-а: в рамках задачи добавил 4 статуса:
:fresh
- админ только-что назначил задачу пользователю:in_progress
- пользователь приступил к выполнению:done
- пользователь закончил выполнение назначенной задачи:approved
- админ проверил и подтвердил успешное выполнение задачи:declined
- админ проверил и отклонил решение
- Для решения тестового задания предполагаем, что в проекте используется rails 5 и devise.
- В качестве тестирующего фреймворка использую rspec
- это RESTful JSON API Rails app
$ ruby -v # ruby 2.3.3
$ rails -v # Rails 5.1.2
$ rails new eduson-test -T -d mysql --api
$ cd eduson-test
$ echo ruby-2.3.3 > .ruby-version
$ <add devise and service gems to Gemfile>
$ rails generate devise:install
$ bundle
$ rails g model Task title
$ rails g devise User role:integer group:integer
$ rails g model Assignment user:references task:references status
- Связываем с моделью 'User':
# app/models/task.rb
class Task < ActiveRecord::Base
has_many :users, through: :assignments
end
- Перечисляем в ней роли, группы и назначаем дефолтные значения при создании.
- Связываем с моделями
Task
иAssignment
:
# app/models/user.rb
class User < ApplicationRecord
has_many :assignments
has_many :tasks, through: :assignments
enum role: [:user, :admin]
enum group: [:msk, :spb, :sochi, :no_group]
after_initialize :set_default_role, if: :new_record?
after_initialize :set_default_group, if: :new_record?
private
def set_default_role
self.role ||= :user
end
def set_default_group
self.group ||= :no_group
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
- Связываем с моделями
User
иTask
- перечисляем возможные статусы и назначаем дефолтный при создании
# app/models/assignment.rb
class Assignment < ActiveRecord::Base
belongs_to :user
belongs_to :task
enum status: [:fresh, :in_progress, :done, :approved, :declined]
after_initialize :set_default_status, :if => :new_record?
private
def set_default_status
self.status = :fresh
end
end
$ rails g controller assignments update destroy to_users to_groups
$ rails g controller users index show
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :set_user, only: [:show]
# GET users
def index
@users = User.all
json_response(@users)
end
# GET users/1
def show
json_response(@user)
end
private
def set_user
@user = User.find(params[:id])
end
end
# app/controllers/assignments_controller.rb
class AssignmentsController < ApplicationController
before_action :set_user, :only => [:update, :destroy]
before_action :set_user_assignment, :only => [:update, :destroy]
# PUT users/1/assignments/1 -d 'status=approved'
def update
@assignment.status = params[:status]
@assignment.save!
head :no_content
end
# DELETE users/1/assignments/1
def destroy
@assignment.delete
head :no_content
end
# POST tasks/to_users -d 'task_ids=1,2,3&user_ids=1,2,3'
def to_users
message = AssignTasksToUsers.new(params).perform
json_response({message: message}, :created)
end
# POST tasks/to_groups -d 'task_ids=5&groups=sochi,abc'
def to_groups
message = AssignTasksToGroups.new(params).perform
json_response({message: message}, :created)
end
private
def set_user
@user = User.find(params[:user_id])
end
def set_user_assignment
@assignment = @user.assignments.find_by!(id: params[:id]) if @user
end
end
Добавим пару concerns
, помогающих с json-ответами и с обработкой исключений:
# app/controllers/concerns/exception_handler.rb
module ExceptionHandler
extend ActiveSupport::Concern
included do
# define custom handlers
rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two
rescue_from ActiveRecord::RecordNotFound do |e|
json_response({message: e.message}, :not_found)
end
end
private
# JSON ответ со статус-кодом 422 - unprocessable_entity
def four_twenty_two(e)
json_response({message: e.message}, :unprocessable_entity)
end
end
# app/controllers/concerns/response.rb
module Response
def json_response(object, status = :ok)
render json: object, status: status
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
end
Директорая actions
предназначена для service objects,
которые не использут сторонние сервисы:
- админ назначает tasks конкретному пользователю (по user id)
- админ назначает tasks конкретной группе пользователей (по group id)
# app/actions/assign_tasks_to_users.rb
# Назначить *tasks* конкретным *пользователям*.
#
# Принимает params вида 'task_ids=1,2,3&user_ids=1,2,3'.
#
# если среди task_ids/user_ids присутствует id несуществующей записи -
# в базу не будет ничего записано.
class AssignTasksToUsers
def initialize(params)
@params = params
end
def perform
# соберём параметры для создания Assignments
new_assignments = []
# обработаем входящие параметры
@params[:user_ids].split(',').each do |user_id|
user = User.find(user_id.to_i)
@params[:task_ids].split(',').each do |task_id|
task = Task.find(task_id.to_i)
new_assignments << {
user_id: user.id,
task_id: task.id
}
end
end
# если дошли до сюда - значит с данными всё впорядке, помещаем их в базу
Assignment.create!(new_assignments)
'Задачи назначены указаным пользователям'
end
end
# app/actions/assign_tasks_to_groups.rb
# Назначить *tasks* конкретным *группам*.
#
# Принимает params вида 'task_ids=1,2,3&groups=msk,spb'.
#
# Если среди task_ids присутствует id несуществующей записи -
# в базу не будет ничего записано.
#
# Если среди groups присутствуют несуществующие группы -
# perform вернёт warnings сообщения, а задачи назначены будут
# пользователям из существующих групп.
class AssignTasksToGroups
def initialize(params)
@params = params
end
def perform
# соберём параметры для создания Assignments
new_assignments = []
#
warnings = []
# обработаем входящие параметры
@params[:groups].split(',').each do |group|
users = User.try(group)
if users.nil?
warnings << "Группы #{group} не существует"
next
elsif users.size.zero?
warnings << "В группе #{group} нет пользователей"
next
end
@params[:task_ids].split(',').each do |task_id|
task = Task.find(task_id.to_i)
users.each do |user|
new_assignments << {
user_id: user.id,
task_id: task.id
}
end
end
end
if new_assignments.size.zero? && warnings.size > 0
raise ArgumentError.new('В указанных группах нет пользователей.')
else
# если дошли до сюда - значит с данными всё впорядке, помещаем их в базу
Assignment.create!(new_assignments)
warnings << 'Задачи назначены указаным пользователям'
end
warnings.join(',')
end
end
Rails.application.routes.draw do
resources :users do
resources :assignments, only: [:update, :destroy]
end
devise_for :users
match 'assignments/to_users', to: 'assignments#to_users', via: :post
match 'assignments/to_groups', to: 'assignments#to_groups', via: :post
end
$ rails c
Создаём 3-х пользователей:
# по-умолчанию: user без группы
User.create!({email:'user@no_group.ru', :password => '123456', :password_confirmation => '123456'})
#=> #<User id: 1, email: "user@no_group.ru", role: "user", group: "no_group" ...
# user в группе sochi
User.create!({email:'user@sochi.ru', group:'sochi', :password => '123456', :password_confirmation => '123456'})
#=> #<User id: 2, email: "user@sochi.ru", role: "user", group: "sochi" ...
# admin в группе spb
User.create!({email:'admin@spb.ru', group:'spb', role:'admin', :password => '123456', :password_confirmation => '123456'})
#=> #<User id: 3, email: "admin@spb.ru", role: "admin", group: "spb" ...
Создаём задачи:
Task.create!({title:'task1'})
Task.create!({title:'task2'})
Task.create!({title:'task3'})
Task.create!({title:'task4'})
Сложим создание моделей в seed файл db/seeds/fill_users_and_tasks.rb
,
который исполняется командой rake db:seed:fill_users_and_tasks
с помощью lib/tasks/custom_seed.rake
.
$ curl -X GET localhost:3000/users
# --- получаем список всех пользователей:
# [{"id":2,"email":"user@sochi.ru","role":"user","group":"sochi","created_at":"2017-08-06T03:38:24.000Z","updated_at":"2017-08-06T03:38:24.000Z"},
# {"id":3,"email":"user@no_group.ru","role":"user","group":"no_group","created_at":"2017-08-06T03:38:50.000Z","updated_at":"2017-08-06T03:38:50.000Z"},
# {"id":4,"email":"admin@spb.ru","role":"admin","group":"spb","created_at":"2017-08-06T03:39:25.000Z","updated_at":"2017-08-06T03:39:25.000Z"}]
$ curl -X POST localhost:3000/assignments/to_users -d 'task_ids=1&user_ids=1'
# --- назначаем одному пользователю одну задачу
# в ответ приходит пустота - все корректно
$ curl -X POST localhost:3000/assignments/to_users -d 'task_ids=1,2&user_ids=1,2'
# --- назначаем многим пользователям много задач
# в ответ приходит пустота - все корректно
$ curl -X POST localhost:3000/assignments/to_users -d 'task_ids=1,2&user_ids=1,2,11'
# --- назначаем многим пользователям много задач: среди user_ids есть несуществующий пользователь
# {"message":"Couldn't find User with 'id'=11"}
$ curl -X POST localhost:3000/assignments/to_groups -d 'task_ids=1,2&groups=sochi'
# --- назначаем одной группе c одним пользователем 2 задачи
# SQL (0.6ms) INSERT INTO `assignments` (`user_id`, `task_id`, ...) VALUES (2, 1, 0, ...)
# SQL (0.6ms) INSERT INTO `assignments` (`user_id`, `task_id`, ...) VALUES (2, 2, 0, ...)
# в ответ приходит пустота - все корректно
$ curl -X POST localhost:3000/assignments/to_groups -d 'task_ids=1,2&groups=sochi'
# --- назначаем одной группе (без пользователей) 2 задачи
# в базу ничего не кладётся
# в ответ приходит пустота - все корректно
$ curl -X POST localhost:3000/assignments/to_groups -d 'task_ids=5&group=user'
# --- назначаем группе задачи: среди task_ids есть несуществующая задача
# {"message":"Couldn't find Task with 'id'=5"}
$ curl -X GET localhost:3000/users/11
# --- ищем инфо о несуществующем пользователе
# {"message":"Couldn't find User with 'id'=11"}
$ curl -X GET localhost:3000/users/3
# получаем инфо о существующем пользователе
# {"id":3,"email":"user@no_group.ru","role":"user","group":"no_group","created_at":"2017-08-06T03:38:50.000Z","updated_at":"2017-08-06T03:38:50.000Z"}
$ curl -X PUT localhost:3000/users/1/assignments/1 -d 'status=done'
# --- обновляем статус назначенной задачи
# SQL (0.1ms) UPDATE `assignments` SET `status` = 2 ...
# в ответ приходит пустота - все корректно
$ curl -X PUT localhost:3000/users/1/assignments/11 -d 'status=done'
# --- пытаемся обновить статус несуществующего assignment-a
# {"message":"Couldn't find Assignment with [WHERE `assignments`.`user_id` = ? AND `assignments`.`id` = ?]"}
$ curl -X DELETE localhost:3000/users/1/assignments/1
# --- Удаляем у пользователя назначенную задачу
# SQL (112.2ms) DELETE FROM `assignments` WHERE `assignments`.`id` = 1
# в ответ приходит пустота - все корректно
Этот пункт не входит в те 1-2 часа, отведённые под тестовое задание, добавлен для полноты картины.
Добавляю в Gemfile
gem rspec
:
[...]
group :development, :test do
gem 'rspec-rails', '~> 3.5'
end
group :test do
gem 'factory_girl_rails', '~> 4.0'
gem 'shoulda-matchers', '~> 3.1'
gem 'faker'
gem 'database_cleaner'
end
[...]
Настраиваю, создаю файлы под тесты:
$ bundle
$ rails g rspec:install
$ rails g rspec:controller application
$ rails g rspec:controller items
$ rails g rspec:controller assignments
$ rails g rspec:model user
$ rails g rspec:model task
$ rails g rspec:model assignment
$ rails g rspec:request user
$ rails g rspec:request task
$ rails g rspec:request assignment
Добавляю factories/
, support/
и support/helpers/
.
Подключаю и настраиваю zeus
.
Пишу тесты для моделей и запросов к контроллеру AssignmentController
.
-
Авторизовать запросы:
- список всех пользователей может получить только админ
- Назначать
tasks
может только админ - Менять статусы
assignment
-ов наapproved
/declined
может только админ - Пользователь может менять статус только своих
assignment
-ов - Удалять
assignments
может только админ
-
AssignmentsController#to_users
: если средиtask_ids
/user_ids
присутствуетid
несуществующей записи - не пройдёт весь запрос, т.е. в базу не будет ничего записано -
AssignmentsController#to_groups
: если средиtask_ids
присутствуетid
несуществующей записи - не пройдёт весь запрос, т.е. в базу не будет ничего записано -
AssignmentsController#to_groups
: если средиgroups
присутствует несуществующая группа - задачи будут назначены пользователям из существующих групп и вернётся сообщение о том, какие группы не существуют. -
Пользователь может изменить статус только своих
assignment
-ов -
Пользователь может изменить статус своего
fresh assignment
-а только наin_progress
-
Пользователь может изменить статус своего
in progress assignment
-а только наdone
-
Пользователь не может изменить статус своих
assignment
-ов со статусомapproved
/declined
-
Админ может изменить статус
assignment
-ов только наapproved
/declined
-
Причесать json-ответы (убрать ненужные атрибуты, добавить нужные)
-
Покрыть код тестами:
- Контроллеры:
-
spec/controllers/application_controller_spec.rb
-
spec/controllers/assignments_controller_spec.rb
-
spec/controllers/users_controller_spec.rb
-
- Модели:
-
spec/models/assignment_spec.rb
-
spec/models/task_spec.rb
-
spec/models/user_spec.rb
-
- Запросы:
-
spec/requests/assignments_spec.rb
-
spec/requests/users_spec.rb
-
- Сервисы:
- Контроллеры: