Проблема
В приложении есть модель со списком, который нужно отсортировать. Желательно управлять порядком сортировки списка, находящегося в базе данных, и предоставить пользователям привлекательный, современный интерфейс с перетаскиванием, позволяющий установить порядок следования элементов списка.
Решение
Предположим, что создается приложение для управления списком закупаемых продуктов. Учитывая размеры современных продуктовых супермаркетов, прежде чем добраться до торговых рядов, важно выработать стратегию закупок. В противном случае можно потратить впустую драгоценные часы жизни, следуя по неоптимальным закупочным маршрутам. Файл миграции Active Record для приложения по оптимизации закупок имеет следующий вид:
Из кода видно, что у нас есть таблицы со списком людей, списками закупок и записями продуктов, попадающих в эти списки (наряду с нужным количеством каждого продукта). Все они связаны стандартным в Active Record отношением has_many( ), за исключением столбца position в таблице food_items. Через пару минут мы поймем, что этот столбец играет особую роль.
Все связанные с таблицами файлы моделей одинаково коротки и незатейливы. Модели Person принадлежит множество объектов GroceryList:
А у каждой модели GroceryList имеется список объектов Foodltem, который будет извлекаться в соответствии со значением столбца position таблицы food_items:
И наконец, мы дошли до самого вкусного. Класс Foodltem содержит объявление Active Record acts_as_list( ), позволяющее содержащемуся в нем объекту (Grocery -List) «автоматически» управлять порядком следования его элементов:
Параметр :scope сообщает acts_as_list(), что порядок сортировки распространяется на содержимое одного списка, элементы которого имеют одинаковый grocery_ list_id. Таким образом, сортировка одного закупочного списка не повлияет на порядок, установленный в других списках.
Имя столбца position играет для acts_as_list( ) особую роль. По соглашению, когда в модели присутствует объявление acts_as_list( ), Rails будет автоматически использовать это имя столбца для управления порядком сортировки. Если здесь нужно будет использовать нестандартное имя столбца, то можно передать объявлению параметр :column, но для нашего скромного списка продуктов имеет смысл имя position, поэтому мы его оставим в покое.
После запуска миграции и создания файлов моделей, давайте включим консоль Rails и испытаем эту новую структуру:
chad> ruby script/console
>> kelly = Person.create(:name => «Kelly»)
=> #<Person:0x26ec854 ...>>
>> list = kelly.grocery_lists.create(:name => «Dinner for Tibetan New Year Party»)
=> #<GroceryList:0x26b9788 ...>>
>> list.food_items.create(:name => «Bag of flour», :quantity => 1)
=> #<FoodItem:0x26a8898 ...>>
>> list.food_items.create(:name => «Pound of Ground Beef», :quantity => 2)
=> #<FoodItem:0x269b60c ...>>
>> list.food_items.create(:name => «Clove of Garlic», :quantity => 5)
=> #<FoodItem:0x26937e0 ...>>
Итак, теперь в нашей базе данных есть человек по имени Kelly, который, похоже, планирует вечеринку по случаю празднования Нового года по тибетскому календарю. Пока в ее списке лишь три наименования. Разумеется, под списком еще не подведена черта, но вы сможете приготовить тибетское блюдо и из этих трех ингредиентов. Давайте взглянем на то, что произошло со столбцом position при создании этих объектов:
>> list.food_items.find_by_name(«Pound of Ground Beef»).position
=> 2
>> list.food_items.find_by_name(«Bag of flour»).position
=> 1
Вот это да! Active Record обновил для нас столбец position! К тому же объявление acts_as_list( ) привело к установке целого пакета превосходных и удобных методов для выполнения таких задач, как выбор следующей (по порядку) записи в списке или перемещение позиции записи вверх или вниз. Но давайте все же не будем именно сейчас разбираться во всем, что есть в модели. У нас уже все готово для того, чтобы добраться до интересующей нас вещи — перетаскивания.
Как и всегда, собираясь применить какую-нибудь модную вещь, связанную с Ajax, нужно где-нибудь в HTML включить необходимые JavaScript-библиотеки. Обычно я создаю в файле app/views/layouts/standard.rhtml стандартный макет, а затем наполняю его следующим кодом:
Далее, вообразив, что у нас уже есть некий интерфейс для создания списка и связывания его с конкретной персоной, давайте создадим контроллер и действие, откуда будет проводиться изменение порядка следования элементов списка. Мы создадим контроллер app/views/controllers/grocery_list_controller.rb, содержащий действие под названием show().
Начало кода контроллера должно выглядеть следующим образом:
Заметьте, что мы включили макет standard.rhtml и определили главное действие, которое будет просто искать список продуктов на основе предоставленных параметров.
Затем в файле app/views/grocery_list/show.rhtml мы создадим соответствующее представление:
Пока что не видно ничего необычного. Это стандартный материал Action View, предназначенный только для чтения. Хотя стоит отметить, что для тегов <li> автоматически сгенерированы уникальные идентификаторы элементов. Это понадобится для перехода к коду сортировки, поэтому на данном этапе нужно не упустить это обстоятельство. Мы можем взглянуть, на что похожа эта страница, запустив сервер разработки приложения и указав браузеру (предположив использование порта по умолчанию) на localhost:3000/grocery_list/show/listid, где listid — это id объекта модели Grocerytist, созданного при работе в режиме консоли.
Теперь давайте сделаем список доступным для сортировки. Для этого в конце содержимого файла show.rhtml добавим следующий код:
Этот помощник сгенерирует JavaScript, необходимый для превращения нашего неупорядоченного списка в динамическую, сортируемую путем перетаскивания форму. Первый параметр, grocery-list, ссылается на идентификатор элемента на текущей HTML-странице, который должен быть преобразован в сортируемый список. Параметр :url определяет такие элементы, как контроллер и действие, из которых будет составлен URL, вызываемый после внесения изменений в сортировку. В нем мы определили действие sort( ) текущего контроллера, к которому добавлен идентификатор текущего списка продуктов. И наконец, параметр xomplete устанавливает визуальный эффект, который будет применен, как только действие sort( ) будет завершено.
Давайте воплотим код действия sort( ) в реальность и посмотрим, как все это будет работать. Добавим в файл grocery_list_controller.rb действие sort( ), которое выглядит следующим образом:
Сначала по предоставленному идентификатору выбирается список продуктов. Затем осуществляется последовательный перебор записей списка, и позиция каждой записи изменяется в соответствии с ее индексом в параметре grocery-list. Этот параметр автоматически генерируется помощником sortable_element( ), который создает упорядоченный массив идентификаторов записей списка. Поскольку значение столбцов position начинается с единицы, а индексация массива начинается с нуля, перед тем, как сохранить позицию, мы увеличиваем значение индекса на единицу.
В завершение мы абсолютно ясно указываем Rails, что действие не должно ничего отправлять. Поскольку визуальное отображение сортируемого списка и есть сам этот список (который уже отображается), мы позволим действию завершить его работу без внешних проявлений.
Если бы мы хотели обновить HTML-страницу результатами выполнения действия, нам нужно было бы добавить к вызову sortable_element( ) параметр update, передав ему идентификатор HTML-элемента, заполняемого этими результатами.
Если мы после добавления помощника sortable_element( ) обновим список продуктов на странице show( ), то получим возможность перетаскивать записи вверх и вниз по списку, изменяя порядок их размещения как на странице, так и в базе данных.
Кросспост с моего блога
В приложении есть модель со списком, который нужно отсортировать. Желательно управлять порядком сортировки списка, находящегося в базе данных, и предоставить пользователям привлекательный, современный интерфейс с перетаскиванием, позволяющий установить порядок следования элементов списка.
Решение
Предположим, что создается приложение для управления списком закупаемых продуктов. Учитывая размеры современных продуктовых супермаркетов, прежде чем добраться до торговых рядов, важно выработать стратегию закупок. В противном случае можно потратить впустую драгоценные часы жизни, следуя по неоптимальным закупочным маршрутам. Файл миграции Active Record для приложения по оптимизации закупок имеет следующий вид:
class CreateAddPersonAndGroceryListsAndFoodItemsTables < ActiveRecord:: Migration
def self.up
create_table: people do |t|
t.column: name,: string
end
create_table: grocery_lists do |t|
t.column: name,: string
t.column: person_id,: integer
end
create_table: food_items do |t|
t.column: grocery_list_id,: integer
t.column: position,: integer
t.column: name,: string
t.column: quantity,: integer
end
end
def self.down
drop_table: people
drop_table: grocery_lists
drop_table: food_items
end
end* This source code was highlighted with Source Code Highlighter.
Из кода видно, что у нас есть таблицы со списком людей, списками закупок и записями продуктов, попадающих в эти списки (наряду с нужным количеством каждого продукта). Все они связаны стандартным в Active Record отношением has_many( ), за исключением столбца position в таблице food_items. Через пару минут мы поймем, что этот столбец играет особую роль.
Все связанные с таблицами файлы моделей одинаково коротки и незатейливы. Модели Person принадлежит множество объектов GroceryList:
class Person < ActiveRecord:: Base
has_many: grocery_lists
end* This source code was highlighted with Source Code Highlighter.
А у каждой модели GroceryList имеется список объектов Foodltem, который будет извлекаться в соответствии со значением столбца position таблицы food_items:
class GroceryList < ActiveRecord:: Base
has_many: food_items,: order =>: position
belongs_to: person
end* This source code was highlighted with Source Code Highlighter.
И наконец, мы дошли до самого вкусного. Класс Foodltem содержит объявление Active Record acts_as_list( ), позволяющее содержащемуся в нем объекту (Grocery -List) «автоматически» управлять порядком следования его элементов:
class FoodItem < ActiveRecord:: Base
belongs_to: grocery_list
acts_as_list: scope =>: grocery_list
end
* This source code was highlighted with Source Code Highlighter.
Параметр :scope сообщает acts_as_list(), что порядок сортировки распространяется на содержимое одного списка, элементы которого имеют одинаковый grocery_ list_id. Таким образом, сортировка одного закупочного списка не повлияет на порядок, установленный в других списках.
Имя столбца position играет для acts_as_list( ) особую роль. По соглашению, когда в модели присутствует объявление acts_as_list( ), Rails будет автоматически использовать это имя столбца для управления порядком сортировки. Если здесь нужно будет использовать нестандартное имя столбца, то можно передать объявлению параметр :column, но для нашего скромного списка продуктов имеет смысл имя position, поэтому мы его оставим в покое.
После запуска миграции и создания файлов моделей, давайте включим консоль Rails и испытаем эту новую структуру:
chad> ruby script/console
>> kelly = Person.create(:name => «Kelly»)
=> #<Person:0x26ec854 ...>>
>> list = kelly.grocery_lists.create(:name => «Dinner for Tibetan New Year Party»)
=> #<GroceryList:0x26b9788 ...>>
>> list.food_items.create(:name => «Bag of flour», :quantity => 1)
=> #<FoodItem:0x26a8898 ...>>
>> list.food_items.create(:name => «Pound of Ground Beef», :quantity => 2)
=> #<FoodItem:0x269b60c ...>>
>> list.food_items.create(:name => «Clove of Garlic», :quantity => 5)
=> #<FoodItem:0x26937e0 ...>>
Итак, теперь в нашей базе данных есть человек по имени Kelly, который, похоже, планирует вечеринку по случаю празднования Нового года по тибетскому календарю. Пока в ее списке лишь три наименования. Разумеется, под списком еще не подведена черта, но вы сможете приготовить тибетское блюдо и из этих трех ингредиентов. Давайте взглянем на то, что произошло со столбцом position при создании этих объектов:
>> list.food_items.find_by_name(«Pound of Ground Beef»).position
=> 2
>> list.food_items.find_by_name(«Bag of flour»).position
=> 1
Вот это да! Active Record обновил для нас столбец position! К тому же объявление acts_as_list( ) привело к установке целого пакета превосходных и удобных методов для выполнения таких задач, как выбор следующей (по порядку) записи в списке или перемещение позиции записи вверх или вниз. Но давайте все же не будем именно сейчас разбираться во всем, что есть в модели. У нас уже все готово для того, чтобы добраться до интересующей нас вещи — перетаскивания.
Как и всегда, собираясь применить какую-нибудь модную вещь, связанную с Ajax, нужно где-нибудь в HTML включить необходимые JavaScript-библиотеки. Обычно я создаю в файле app/views/layouts/standard.rhtml стандартный макет, а затем наполняю его следующим кодом:
<! DOCTYPE HTML PUBLIC «-//W3C//DTD HTML 4.01//EN» «http://www.w3.org/TR/html4/strict.dtd»>
<html>
<head>
<%= javascript_include_tag: defaults %>
</head>
<body>
<%= yield %>
</body>
</html>
* This source code was highlighted with Source Code Highlighter.
Далее, вообразив, что у нас уже есть некий интерфейс для создания списка и связывания его с конкретной персоной, давайте создадим контроллер и действие, откуда будет проводиться изменение порядка следования элементов списка. Мы создадим контроллер app/views/controllers/grocery_list_controller.rb, содержащий действие под названием show().
Начало кода контроллера должно выглядеть следующим образом:
class GroceryListController < ApplicationController
layout «standard»
def show
@grocery_list = GroceryList.find(params[: id])
end
#…* This source code was highlighted with Source Code Highlighter.
Заметьте, что мы включили макет standard.rhtml и определили главное действие, которое будет просто искать список продуктов на основе предоставленных параметров.
Затем в файле app/views/grocery_list/show.rhtml мы создадим соответствующее представление:
Пока что не видно ничего необычного. Это стандартный материал Action View, предназначенный только для чтения. Хотя стоит отметить, что для тегов <li> автоматически сгенерированы уникальные идентификаторы элементов. Это понадобится для перехода к коду сортировки, поэтому на данном этапе нужно не упустить это обстоятельство. Мы можем взглянуть, на что похожа эта страница, запустив сервер разработки приложения и указав браузеру (предположив использование порта по умолчанию) на localhost:3000/grocery_list/show/listid, где listid — это id объекта модели Grocerytist, созданного при работе в режиме консоли.
Теперь давайте сделаем список доступным для сортировки. Для этого в конце содержимого файла show.rhtml добавим следующий код:
<h2><%= @grocery_list.person.name %>'s Grocery List</h2>
<h3><%= @grocery_list.name %></h3>
<ul id=«grocery-list»>
<% @grocery_list.food_items.each do |food_item| %>
<li id=«item_<%= food_item.id %>»>
<%= food_item.quantity %> units of <%= food_item.name %>
</li>
<% end %>
</ul>
<%= sortable_element 'grocery-list',
: url => {: action => «sort»,: id => @grocery_list },
: complete => visual_effect(: highlight, 'grocery-list')
%>
* This source code was highlighted with Source Code Highlighter.
Этот помощник сгенерирует JavaScript, необходимый для превращения нашего неупорядоченного списка в динамическую, сортируемую путем перетаскивания форму. Первый параметр, grocery-list, ссылается на идентификатор элемента на текущей HTML-странице, который должен быть преобразован в сортируемый список. Параметр :url определяет такие элементы, как контроллер и действие, из которых будет составлен URL, вызываемый после внесения изменений в сортировку. В нем мы определили действие sort( ) текущего контроллера, к которому добавлен идентификатор текущего списка продуктов. И наконец, параметр xomplete устанавливает визуальный эффект, который будет применен, как только действие sort( ) будет завершено.
Давайте воплотим код действия sort( ) в реальность и посмотрим, как все это будет работать. Добавим в файл grocery_list_controller.rb действие sort( ), которое выглядит следующим образом:
def sort
@grocery_list = GroceryList.find(params[: id])
@grocery_list.food_items.each do |food_item|
food_item.position = params['grocery-list'].index(food_item.id.to_s) + 1
food_item.save
end
render: nothing => true
end
* This source code was highlighted with Source Code Highlighter.
Сначала по предоставленному идентификатору выбирается список продуктов. Затем осуществляется последовательный перебор записей списка, и позиция каждой записи изменяется в соответствии с ее индексом в параметре grocery-list. Этот параметр автоматически генерируется помощником sortable_element( ), который создает упорядоченный массив идентификаторов записей списка. Поскольку значение столбцов position начинается с единицы, а индексация массива начинается с нуля, перед тем, как сохранить позицию, мы увеличиваем значение индекса на единицу.
В завершение мы абсолютно ясно указываем Rails, что действие не должно ничего отправлять. Поскольку визуальное отображение сортируемого списка и есть сам этот список (который уже отображается), мы позволим действию завершить его работу без внешних проявлений.
Если бы мы хотели обновить HTML-страницу результатами выполнения действия, нам нужно было бы добавить к вызову sortable_element( ) параметр update, передав ему идентификатор HTML-элемента, заполняемого этими результатами.
Если мы после добавления помощника sortable_element( ) обновим список продуктов на странице show( ), то получим возможность перетаскивать записи вверх и вниз по списку, изменяя порядок их размещения как на странице, так и в базе данных.
Кросспост с моего блога