Пример написания приложения на Rails с использованием MongoDB и MongoMapper вместо MySQL и ActiveRecord.
Наше приложение должно уметь сохранять траты за день и выводить статистику по ним за разное время. Траты хранятся в древообразных категориях и им присваюваются метки. Будет облако меток и вывод списка трат для определенной метки. Приложение будет доступно только для зарегистрированных пользователей и данные пользователей будут скрыты друг от друга.
Статистика будет выводиться за определенные периоды в виде графиков и таблиц. Должно быть два варианта статистики: общая и личная. Личная статистика будет учитывать траты конкретного пользователя и будет доступна только ему. Общая статистика будет основана на данных всех пользователей, она будет анонимна и доступна всем. Общая статистика будет ограниченной детализации. В статистике необходимо учесть, какое эмоциональное воздействие оказала трата: положительное (трата принесла радость, удовлетворение), нейтральное (никаких эмоций) или отрицательное (неприятные траты).
Это приложение не ведет баланс, тут не будет прихода. Только расход. Это сервис статистики по тратам.
Сначала создадим проект, и назовем его, к примеру, spending:
Rails 2.2.2
undr:ruby undr$ rails spending
create
create app/controllers
create app/helpers
...
undr:ruby undr$ cd spending
undr:spending undr$ mate .
В файл environment.rb добавим необходимые gems и отключим ActiveRecord
config.gem 'mongo', :source => "http://gemcutter.org", :version => '>0.18'
config.gem 'mongo_ext', :source => "http://gemcutter.org", :lib => false
config.gem 'mongo_mapper', :source => "http://gemcutter.org"
config.gem 'faker'
config.gem 'haml'
config.gem 'yaroslav-russian', :lib => 'russian', :source => 'http://gems.github.com'
В консоли:
undr:spending undr$ rake gems:unpack
создадим файл initializers/database.rb и добавим туда:
if db_config[Rails.env] && db_config[Rails.env]['adapter'] == 'mongodb'
mongo = db_config[Rails.env]
MongoMapper.connection = Mongo::Connection.new(mongo['hostname'], 27017, :logger => RAILS_DEFAULT_LOGGER)
MongoMapper.database = mongo['database']
end
Это для того чтобы мы могли хранить настройки базы данных, так как мы привыкли, в config/database.yml
development:
adapter: mongodb
database: spending_development
hostname: localhost
test:
adapter: mongodb
database: shop_test
hostname: localhost
production:
adapter: mongodb
database: shop_production
hostname: localhost
Все, приложение настроено на использование с MongoMapper.
Задача приложения – хранить данные о совершенных тратах и показывать статистику по ним. Поэтому, на текущий момент, логично предположить что они должно содержать модель отвечающую за хранение данных о тратах (Spending). Траты должны храниться в каталоге, поэтому я создам модель Category.
Модельки с использованием MongoMapper – это обычные классы к которым подключен модуль MongoMapper::Document и добавленны вызовы метода key, который создает сохраняемое в базе данных поле. В данном случае для Spending это:
- amount – Потраченная сумма
- description – Описание траты, на что конкретно потрачена сумма
- emotion – Указывает на эмоциональное воздействие от траты (-1, 0, 1)
- category_id – Указатель на категорию траты
- spend_at – Когда была совершена трата
А для Category это:
- name – Имя категории
- slug – Имя для URL
include MongoMapper::Document
key :amount, Float, :required => true
key :description, String, :required => true
key :emotion, Integer, :required => true
key :spend_at, Time, :required => true
end
class Category
include MongoMapper::Document
key :name, String, :required => true
key :slug, String, :required => true
end
Для поля spend_at (потрачено в) мы используем класс Time, а не класс Date (что логичнее), потому что драйвер mongo не работает с классом Date. Но в шаблоне редактирования и создания новой траты лучше использовать помощника date_select.
:required => true означает обязательное поле, то которое должно иметь хоть какое-нибудь значение. У нас они все такие. Добавляем связи. В Spending
key :category_id, ObjectId, :required => true
В Category
Важно! key :category_id, ObjectId, :required => true. Поле category_id указывает на категорию траты и содержит _id категории. Это свойство должно иметь тип ObjectId, так как первичные ключи в MongoDB имеют этот тип.
Для Category добавляем код который генерирует уникальный slug на основе name. Это метод sluggize, сделаем его приватным, а метод to_param потребуется для генерации URL.
def to_param
self.slug
end
private
def sluggize
if self.slug.blank?
initial = Russian::translit(self.name).gsub(/[^A-Za-z0-9\s\-]/, "")[0,40].strip.gsub(/\s+/, "-").downcase
tail, int = "", 1
while Category.find_by_slug(initial + tail) do
int += 1
tail = "-#{int}"
end
self.slug = initial + tail
end
end
Добавляем проверку данных. Поле amount должно быть больше нуля, точнее, больше 0.01. Поле emotion должно быть -1, 0 или 1. Поле spend_at должно содержать дату в прошлом. В Spending добавим
и в приватную секцию
errors.add(:amount, "должна быть больше или равена 0.01") if amount.nil? || amount < 0.01
errors.add(:emotion, "должна быть -1, 0 или 1") unless [-1, 0, 1].include?(emotion)
errors.add(:spend_at, "должно быть в прошлом") if spend_at.nil? || spend_at > Time.now
end
В модель Category добавляем проверку поля slug на уникальность
Пока потребуются только два контроллера SpendingsController и CategoriesController.
...
undr:spending undr$ script/generate controller Categories index show new create edit update delete
...
Определим маршруты. Ну пока не будем изобретать велосипед, создадим обычные REST ресурсы. Действия и их шаблоны в контроллерах такие же как и для ресурсов использующих ActiveRecord. Стандартные.
map.resources :spendings
map.resources :categories
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
end
Добавляем через консоль несколько категорий:
Loading development environment (Rails 2.2.2)
>> Category.all
=> []
>> Category.create(:name => 'Transport')
=> #<Category name: "Transport", slug: "transport", _id: 4b3f0f32b34fb497ec000001>
>> Category.create(:name => 'Communications')
=> #<Category name: "Communications", slug: "communications", _id: 4b3f0f46b34fb497ec000002>
>>
Категории нормально добавились, значит эта часть работает. Проверяем дальше. Запускаем сервер и заходим по адресу http://127.0.0.1:3000/spendings/new. В появившуюся форму вводим значения, нажимаем "Сохранить" и... Ошибка!
(eval):1: syntax error, unexpected tINTEGER, expecting ')'
def spend_at(1i)
^
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:110:in `create_accessors_for'
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:53:in `key'
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/document.rb:31:in `key'
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:345:in `ensure_key_exists'
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:277:in `[]='
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:224:in `attributes='
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:218:in `each_pair'
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:218:in `attributes='
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/embedded_document.rb:191:in `initialize'
/Users/undr/.gem/ruby/1.8/gems/mongo_mapper-0.6.0/lib/mongo_mapper/dirty.rb:50:in `initialize'
app/controllers/spendings_controller.rb:27:in `new'
app/controllers/spendings_controller.rb:27:in `create'
...
В params:
"authenticity_token"=>"378d0fc3973092616e55c739bcab90a4c6ed5fbd",
"spending"=>{"amount"=>"500.00",
"description"=>"Переплата за интернет трафик",
"category_id"=>"4b3f0f46b34fb497ec000002",
"spend_at(1i)"=>"2009",
"spend_at(2i)"=>"11",
"emotion"=>"-1",
"spend_at(3i)"=>"16"}}
Ошибка возникает при попытке вызова @spending = Spending.new(params[:spending]). Модель не понимает что такое spend_at(1i), spend_at(2i) и spend_at(3i). MongoMapper если не находит у модели метода устанавливающего свойство, в нашем случае метод spend_at(1i)=, вызывает метод класса key для несуществующего свойства. Это вызывает ошибку, так как это недопустимое имя для метода. Чтобы этого избежать мы можем в контроллере изменить код создания объекта:
#@spending = Spending.new(params[:spending])
spending_params = params[:spending]
spend_at = Time.gm(spending_params["spend_at(1i)"].to_i, spending_params["spend_at(2i)"].to_i, spending_params["spend_at(3i)"].to_i)
spending_params.delete("spend_at(1i)")
spending_params.delete("spend_at(2i)")
spending_params.delete("spend_at(3i)")
spending_params[:spend_at] = spend_at
@spending = Spending.new(spending_params)
respond_to do |format|
if @spending.save
flash[:notice] = 'Трата добавлена.'
format.html { redirect_to(@spending) }
format.xml { render :xml => @spending, :status => :created, :location => @spending }
else
format.html { render :action => "new" }
format.xml { render :xml => @spending.errors, :status => :unprocessable_entity }
end
end
end
Но у этого способа есть недостатки:
- Во-первых, он не защищает приложение от неверных данных, а точнее лишних ключей переданных в базу данных. То есть если мы передадим в
params[:spending]какие-нибудь поля не относящиеся к моделиSpending, например:
"authenticity_token"=>"378d0fc3973092616e55c739bcab90a4c6ed5fbd",
"spending"=>{"amount"=>"500.00",
"description"=>"Переплата за интернет трафик",
"undefined_field"=>"Какие-то данные",
"another_undefined_field"=>"Какие-то данные",
"category_id"=>"4b3f0f46b34fb497ec000002",
"spend_at(1i)"=>"2009",
"spend_at(2i)"=>"11",
"emotion"=>"-1",
"spend_at(3i)"=>"16"}}
они сохранятся в базе.
- Во-вторых, этот способ сам по себе не очень красив и многословен.
Мы пойдем другим путем, создадим следующий модуль:
def self.included(model)
model.class_eval do
include InstanceMethods
extend ClassMethods
end
end
module ClassMethods
def safe_build(values)
safe_values = {}
keys.keys.each do |k|
prepare_value(safe_values, k, values)
end
self.new(safe_values)
end
def prepare_value(safe_values, k, values)
if date_or_time_or_datetime?(k) && values_has_datetime_key?(k, values)
safe_values[k] = get_time(k, values)
else
safe_values[k] = values[k] if values.has_key?(k)
end
end
def get_time(k, values)
time = []
1.upto(6) { | i | time[i] = values["#{k}(#{i}i)"] && values["#{k}(#{i}i)"].to_i || nil }
Time.gm(time[1], time[2], time[3], time[4], time[5], time[6])
end
def date_or_time_or_datetime?(k)
keys[k].type == Date || keys[k].type == Time || keys[k].type == DateTime
end
def values_has_datetime_key?(k, values)
!values.has_key?(k) && (values.has_key?("#{k}(1i)") || values.has_key?("#{k}(2i)") || values.has_key?("#{k}(3i)"))
end
end
module InstanceMethods
def safe_update_attributes(values)
safe_values = {}
self.class.keys.keys.each() do | k |
self.class.prepare_value(safe_values, k, values)
end
update_attributes(safe_values)
end
end
end
поместив этот код, ну например, в config/initializers/mm_ext.rb. Теперь перепишем код создания объекта Spending и в класс Spending подключим наш модуль include SafeAttributes
@spending = Spending.safe_build(params[:spending])
...
end
def update
@spending = Spending.find(params[:id])
respond_to do |format|
if @spending.safe_update_attributes(params[:spending])
flash[:notice] = 'Трата изменена.'
format.html { redirect_to(@spending) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @spending.errors, :status => :unprocessable_entity }
end
end
end
Пробуем. Все работает. Так же поступим с категориями.
Приложение должно разделять траты по категориям, по меткам и по месяцам. Добавим в config/routes.rb новые маршруты
map.spendings_by_tag 'spendings/by_tag/:id', :controller => 'spendings', :action => 'tag'
map.spendings_by_month 'spendings/by_month/:year/:month', :controller => 'spendings',
:action => 'month',
:requirements => {:year => /(19|20)\d\d/,
:month => /[01]?\d/}
Добавим в контроллер новые методы
@category = Category.find_by_slug(params[:id])
@spendings = @category.spendings.find(:all)
respond_to do |format|
format.html # category.html.haml
format.xml { render :xml => @spendings }
end
end
def tag
# С метками потом разберемся
end
def month
@date_start = Time.gm(params[:year].to_i, params[:month].to_i)
@date_end = @date_start.end_of_month
@spendings = Spending.all(:spend_at => { '$gte' => @date_start, '$lt' => @date_end})
respond_to do |format|
format.html # month.html.haml
format.xml { render :xml => @spendings }
end
end
Все три шаблона примерно такие (потом украсим):
=link_to "Добавить", new_spending_path
%br/
-@spendings.each do |spending|
%h3
=link_to h(spending.description), spending_path(spending)
Заплачено
=spending.amount
руб. в
=spending.spend_at
%br/
=link_to spending.category.name, category_path(spending.category)
%br/
%br/
Теперь Мы умеем добавлять траты, редактировать и удалять их, а так же просматривать списки группированные по категориям и датам. Ну теперь можно разукрасить шаблоны. Создадим шаблон app/views/layouts/default.html.haml:
%html{:xmlns => "http://www.w3.org/1999/xhtml"}
%head
%title Title
%meta(http-equiv="Content-Type" content="text/html; charset=utf-8")
%meta(name="description" content=" add page description... ")
%meta(name="keywords" content=" add keywords... ")
%link(href="/stylesheets/style.css" rel="stylesheet" type="text/css")
%body
#header.fixed
.logo
=link_to image_tag('logo.png', :style => "border:0;", :size => '220x75', :alt => 'logo'), root_path
.nav
%ul
%li
=link_to 'Главная', root_path
%li
=link_to 'Траты', spendings_path
%li
%a{:href => "#"}
Статистика
#content.fixed
#maincontent
=yield
#sidebar
%h3
=link_to "Добавить трату", new_spending_path
%h3 по датам
%ul.sidebar_nav
-Spending.sum_by_month.each do |date|
%li
=link_to Russian::strftime(date['spend_at'], '%B %Y'), spendings_by_month_path(:year => date['spend_at'].year, :month => date['spend_at'].month)
(
=number_to_currency(date['sum'], :unit => "руб.", :separator => ",", :delimiter => " ", :format => "%n %u")
)
%p
%h3 по категориям
%ul.sidebar_nav
-Category.all.each do |category|
%li
=link_to category.name, spendings_by_category_path(category)
%p
#footer.fixed
%p.copyright © 2009 - 2010 spendings.undr.su
%p.credits
Иллюстрация к статье
%a{:href => "http://memo.undr.su"}
Пример написания приложения на Rails с использованием MongoDB и MongoMapper
\.
CSS файл:
------------------ */
body { margin:0; padding:0;
background:#666 url(/images/bg.gif) repeat-x top left;
color:#666;
font:normal 13px Arial, Helvetica, sans-serif; position:relative; }
a { color:#005EB3; }
a:hover { }
img{ border:none; }
table{ width:100%; margin-bottom:15px; line-height:24px; }
th{ border-top:3px solid #970205; padding:7px 10px; color:#fff; background-color:#CA0308; text-align:left; }
td{ border-bottom:1px solid #f4f4f4; padding:10px; }
code{ display:block; margin-bottom:15px; padding:10px; border-left:5px solid #ddd; }
blockquote{ display:block; margin:15px; padding-left:50px; background:#fff url(/images/blockquote-quotemark.gif) no-repeat top left; }
blockquote p{ font-style:italic; font-family:Georgia,"Times New Roman",Times,serif; margin:0; height:1%; }
/* align images + text */
.img-left{ float:left; margin:10px 15px 15px 5px; } /* Add this to any image you want to left align */
.img-right{ float:right; margin:10px 5px 15px 15px; } /* Add this to any image you want to right align */
.text-right{ text-align:right; }
.text-center{ text-align:center; }
/* Clear Fix Hack - add class="fixed" to div's that have floated elements in them */
.fixed:after{content:"."; display:block; height:0; clear:both; visibility:hidden;}
.fixed{display:block;}
/* \*/
.fixed{min-height:1%;}
* html .fixed{height:1%;}
/* =HEADER
------------------ */
#header { margin:60px auto 0px auto; width:900px; background-color:#FFFFFF; }
/* =LOGO
------------------*/
.logo { padding:31px 0 20px 30px; margin:0; float:left; color:#FFFFFF; }
.logo a{ outline:none; }
/* =NAVIGATION
------------------*/
.nav { padding: 0 20px; margin:0px 30px 0 0; float:right; border-bottom:1px solid #efefef; }
.nav ul { padding:0; margin:0; list-style:none; border:0;}
.nav ul li { float:left; margin:0; padding:0 2px 0 0; border:0;}
.nav ul li a { float:left; margin:0; padding:75px 11px 11px 11px; color:#111; font-size: 14px; font-weight:bold; text-decoration:none; outline:none; }
.nav ul li a:hover{ color:#CC0000; }
.nav ul li a.active { background-color:#CC0000; color:#fff; }
.nav ul li a.active:hover{ color:#fff; }
h2.pagetitle{ color:#fff; font-size:30px; width:865px; margin:0 auto; padding:35px 0 35px 35px; border-left:1px solid #ccc; border-right:1px solid #ccc; }
/* =CONTENT
-------------------*/
#content { width:840px; margin:0 auto; background-color:#FFF; padding:25px 30px; }
#content h2 { margin:0; padding:10px 5px; font-size: 28px; color:#000; }
#content h3 { margin:0; padding:15px 5px; font-size:20px; color:#000; }
#content h4 { margin:0; padding:15px 5px; font-size:16px; font-weight:bold; color:#000; }
#content ul { margin:0 40px 0 0; padding:0 10px 15px 20px; list-style:inside; }
#content li { margin:0; padding:0;}
#content ul li ul{ padding-bottom: 0px; }
#content p, #content li { line-height:24px; }
#content p { padding:5px; margin:0;}
/* =MAIN CONTENT
------------------*/
#maincontent { float:left; width:550px; padding:0px 30px 30px 0; margin:0; }
#maincontent h2 { margin-bottom:15px; color:#000; }
/* =SIDEBAR
------------------*/
#sidebar { float:right; width:240px; padding:12px 0 0 0px; }
#sidebar h3{ padding:7px 10px; margin-bottom:10px; font-size:17px; border-bottom:3px solid #e3e3e3; }
#sidebar .title { background:url(/images/news_title.gif) no-repeat left center; padding:5px 0 5px 20px; font-weight:bold;}
/* =SIDEBAR NAVIGATION
---------------------*/
#sidebar ul.sidebar_nav { padding:0; margin:0; list-style:none;}
#sidebar ul.sidebar_nav li { padding:5px 10px; border-bottom:1px solid #e5e5e5;}
#sidebar ul.sidebar_nav li a { background:none; color:#6e6e6e; font-weight:normal; padding:0 0 0 15px; text-decoration:none; }
#sidebar ul.sidebar_nav li a.active { color:#0000FF;}
#sidebar ul.sidebar_nav li a:hover { text-decoration:underline;}
/* =CONTACT INFO
-------------------*/
#sidebar ul.contact_info { padding:0; margin:0; list-style:none;}
#sidebar ul.contact_info li { width:210px; float:left; background:url(/images/cont_bg.gif) no-repeat left center; padding:0 0 0 15px; margin:0 0 0 5px;}
#sidebar ul.contact_info li a { padding:0; margin:0; background:none;}
/* =FOOTER
-------------------*/
#footer { padding:0; margin:0 auto; padding:5px 40px; width:820px; }
#footer p { font-size: 12px; color:#fff;}
#footer a { color:#fff; text-decoration:underline;}
.copyright{ float:left;}
.credits{ float:right;}
/* =MISC
-------------------*/
/* =CONTACT FORM
-------------------*/
form { margin:0; padding:5px 10px;}
form ol { margin:0; padding:0; list-style:none;}
form li { margin:0; padding:0; background:none; border:none; display:block;}
form li.buttons { margin:5px 0 5px 110px;}
form label { margin:2px 10px 2px 0; width:170px; display:block; padding:3px 0;
text-transform:capitalize; float:left; text-align:right;}
form label span { color:#2c2c2c;}
form input.text { width:320px; border:1px solid #dcdcdc; margin:5px 0; padding:5px; height:16px; background:#FFF; float:left;}
form textarea { width:320px; border:1px solid #dcdcdc; margin:5px 0; padding:5px; background:#FFF; float:left;}
Шаблон вызывает метод Spending.sum_by_month. Его пока нет. Он должен выдавать список месяцев с общей потраченной суммой каждого. Это для меню. Далее, было бы неплохо, сгруппировать список трат по дням, чтобы удобнее показывать список, типа так:
706,00 руб. - rerum ex laborum
Категория: Жилье
8 253,00 руб. - quis voluptate at fuga
Категория: Средства связи
1 380,00 руб. - deleniti ullam unde sit labore
Категория: Развлечения
30 октября 2009
8 245,00 руб. - ea at ut
Категория: Разные мелочи
6 031,00 руб. - perferendis nihil voluptatem ducimus
Категория: Питание
6 582,00 руб. - temporibus harum aspernatur quod
Так же, нужно не забыть про постраничный вывод списков.
Начнем с метода Spending.sum_by_month. Здесь обычная групировка. SQL запрос выглядел бы так: SELECT spend_at, SUM(amount) FROM spendings GROUP BY spend_at, но мы используем MongoDB и MongoMapper, поэтому тут нужна особая групировка. Я не нашел в MongoMapper метода group, поэтому будем использовать ссылку на коллекцию драйвера. Ее возвращает метод self.collection. Описание метода коллекции драйвера group(key, condition, initial, reduce, command=false, finalize=nil) такое:
key- это 1) массив полей для группировки, 2) javascript функция которая возвращает объект для группировки, или 3)nil.condition- условие для выборки группируемых полей.initial- начальное значениеreduce- функция занимающаяся подсчетом, как стока JavaScriptfinalize- (выборочно). JavaScript функция модифицирующая результат, вызывается последнейcommand- устаревший аргумент, всегда должен бытьtrue
В итоге, у меня получилось вот такое:
self.collection.group(['spend_at'],
nil,
{:sum => 0},
"function(doc, prev) { prev.sum += doc.amount;}",
true)
end
Но у меня получилось группировка по дням, а нужна по месяцам.
Loading development environment (Rails 2.2.2)
>> Spending.sum_by_month
=> [{"spend_at"=>Fri Oct 02 00:00:00 UTC 2009, "sum"=>10340.0}, {"spend_at"=>Wed Oct 07 00:00:00 UTC 2009, "sum"=>870.0}, {"spend_at"=>Thu Oct 08 00:00:00 UTC 2009, "sum"=>1950.5}, {"spend_at"=>Fri Oct 09 00:00:00 UTC 2009, "sum"=>500.0}, {"spend_at"=>Sat Oct 10 00:00:00 UTC 2009, "sum"=>4005.0}, {"spend_at"=>Sun Oct 11 00:00:00 UTC 2009, "sum"=>9000.0}, ...]
>>
Исправим. Первый аргумент может быть функцией, которая возвратит объект для групировки. Напишем функцию, которая возвращает объект с единственным свойством
spend_at, содержащим округленную дату до месяца:
self.collection.group('function(doc) {d = new Date(doc.spend_at.getUTCFullYear(), doc.spend_at.getUTCMonth()+1); return {spend_at : d};}',
nil,
{:sum => 0},
"function(doc, prev) { prev.sum += doc.amount;}",
true)
end
Теперь то что надо:
=> [{"spend_at"=>Sat Oct 31 21:00:00 UTC 2009, "sum"=>865156.5}, {"spend_at"=>Mon Nov 30 21:00:00 UTC 2009, "sum"=>949832.0}]
>>
Важно! Если Вы получаете что-то типа такого: => [{"f"=>nil, "u"=>nil, "n"=>nil, "c"=>nil, "t"=>nil, "i"=>nil, "o"=>nil, "("=>nil, "d"=>nil, ")"=>nil, ...}], значит Вам надо обновить гем mongo, ну хотя бы до версии 0.18
Теперь группировка списков по датам. Все просто, в контроллере:
def index
@grouped_spendings = Spending.all(:order => 'spend_at desc').group_by {|s| s.spend_at.beginning_of_day}
respond_to do |format|
format.html # index.html.haml
format.xml { render :xml => @spendings }
end
end
...
def category
@category = Category.find_by_slug(params[:id])
@grouped_spendings = @category.spendings.all(:order => 'spend_at desc').group_by {|s| s.spend_at.beginning_of_day}
respond_to do |format|
format.html # category.html.haml
format.xml { render :xml => @spendings }
end
end
def month
@date_start = Time.gm(params[:year].to_i, params[:month].to_i)
@date_end = @date_start.end_of_month
@grouped_spendings = Spending.all(:conditions => {:spend_at => { '$gte' => @date_start, '$lt' => @date_end}},
:order => 'spend_at desc').group_by {|s| s.spend_at.beginning_of_day}
respond_to do |format|
format.html # month.html.haml
format.xml { render :xml => @spendings }
end
end
В шаблоне app/views/spendings/index.html.haml:
-for spending_day in @grouped_spendings
%h4
=Russian::strftime(spending_day[0], "%d %B %Y")
-for spending in spending_day[1]
=render :partial => "spending", :locals => {:spending => spending}
В шаблоне app/views/spendings/category.html.haml:
=@category.name
-for spending_day in @grouped_spendings
%h4
=Russian::strftime(spending_day[0], "%d %B %Y")
-for spending in spending_day[1]
=render :partial => "spending", :locals => {:spending => spending}
В шаблоне app/views/spendings/month.html.haml:
Список трат с
=Russian::strftime(@date_start, "%d %B %Y")
по
=Russian::strftime(@date_end, "%d %B %Y")
-for spending_day in @grouped_spendings
%h4
=Russian::strftime(spending_day[0], "%d %B %Y")
-for spending in spending_day[1]
=render :partial => "spending", :locals => {:spending => spending}
В шаблоне app/views/spendings/_spending.html.haml:
%strong
=number_to_currency(spending.amount, :unit => "руб.", :separator => ",", :delimiter => " ", :format => "%n %u")
\ -
=spending.description
%br/
Категория:
=link_to spending.category.name, spendings_by_category_path(spending.category)
Остался постраничный вывод списков. Я использую модуль WillPaginate из проекта Shapado (http://github.com/patcito/shapado/blob/master/lib/mm-paginate.rb). Изменяем контроллер.
# В методе index
@spendings = Spending.paginate( :per_page => 30, :page => params[:page] || 1)
@grouped_spendings = @spendings.group_by {|s| s.spend_at.beginning_of_day}
...
# в методе category
@spendings = @category.spendings.paginate( :per_page => 30, :page => params[:page] || 1, :order => 'spend_at desc')
@grouped_spendings = @spendings.group_by {|s| s.spend_at.beginning_of_day}
...
# В методе month
@spendings = Spending.paginate( :per_page => 30,
:page => params[:page] || 1,
:conditions => {:spend_at => { '$gte' => @date_start, '$lt' => @date_end}},
:order => 'spend_at desc')
@grouped_spendings = @spendings.group_by {|s| s.spend_at.beginning_of_day}
...
@spendings нужен для помощника will_paginate, а @grouped_spendings для вывода списка. Добавляем в шаблоны ссылки на страницы:
=will_paginate @spendings, :previous_label => '« Ранее', :next_label => 'Позднее »'
...
Осталось добавить метки и категории сделать древообразными, а так же самое интересное, статистику по тратам и авторизацию. Чуть не забыл про полнотекстовый поиск и комментарии к трате, как же без них
Статья получилась длинноватой, поэтому я решил разбить ее на части. Первую часть я на этом закончу. В следующей части я напишу про остальное, если не успею в одной, то перенесу в следующие части.
В ближайшее время постараюсь запустить это приложение для просмотра. Код его выложу на http://github.com
Несколько ссылок:

(5 голосов, средний: 4.60 из 5)
Спасибо за обстоятельную статью на эту тему!
Рассматривали ли вы в качестве маппера к Mongodb проект Mongoid (http://mongoid.org/). На мой взгляд, продукт более качественный, неплохо описанный, и не уступающий функционально MongoMapper. Автор же последнего не спешит документировать, мол сам по себе код говорящий и объясняющий.
В настоящий момент MongoMapper более часто упоминается, и, потому, и чаще всего используется.
Musthave
5 Фев 10 at 7:10
Нет, MongoId я не пробовал. Обязательно гляну. Стал использовать MongoMapper вероятно из-за частого упоминания в сети и из-за доходчивых презентаций.
Кстати, ORMов достаточно много на текущий момент. MongoMapper, MongoId, MongoDoc, ActiveMongo, MongoRecord…
Из всех просмотренных мне понравился MongoMapper. Про MongoId я тогда не знал, сейчас думаю с ним познакомиться.
undr
5 Фев 10 at 11:20
Кстати под MongoMapper уже выходит множество плагинов, что является несомненным плюсом.
undr
5 Фев 10 at 11:21
Он мне начинает уже нравиться
http://mongoid.org/docs/querying
Прям как Rails 3
undr
5 Фев 10 at 11:48
Вот и я о том же…) MongoMapper по-большей
Musthave
5 Фев 10 at 12:03
Вот и я о том же…) MongoMapper по-большей части пишется для использования в Harmony, исходя из потребностей приложения, а фидбэк от коммунити в результате опенсурности им только на руку… Mongoid же, ощущается, делается основательно и аккуратно, без раздувания API, а те, что реализованы – грамотны и сдержаны. В стиле google-api любого рода)))
Так что время покажет, какой маппер станет де-факто для MongoDB.
Musthave
5 Фев 10 at 12:05
Ну я уже решил переписать свое приложение на MongoId. И плагины под него уже пишутся, http://github.com/azisaka/mongoid_state_machine например.
А Вы, случаем, не подскажите плагин для работы с деревьями для MongoId?
undr
5 Фев 10 at 13:06
Нет, не подскажу. Я пока на стадии исследования-выбора технологий для тяжелого проекта.
Кстати, хотел сам переписать ваше приложение под Mongoid и сравнить.
Насчет похожести на Rails 3 – автор уже прорабатывает вопрос совместимости с ними (http://www.pivotaltracker.com/projects/27482).
Вообще, интересная тенденция: Ruby стал популярен благодаря Ruby On Rails, теперь MongoDB популяризируется на локомотиве RoR, где уже сложились методы-привычки работы с ActiveRecord, которые не хочется менять – отсюда и AR-like api у мапперов (что логично), но так хочется привычных плагинов
)) Быстро появятся соответствующие порты, но… как бы не погребли под этим обертками ключевые фишки MongoDB, обернув ее ActiveRecord-привычки.
Как, вам, например, возможность сохранения и повторного использования процедур, написанных на JavaScript, на стороне сервера? Реально затачивают под web-разработчиков, но с хорошим прицелом на масштабируемость. Время перемен: MongoDB vs CouchDB, MongoMapper vs Mongoid. Это как Blu-Ray vs HD DVD на этапе своего появления на рынке)
Musthave
5 Фев 10 at 20:22
Да, это интересная тенденция. Ну я думаю не стоить бояться пагубного влияния ActiveRecord
Народ потому активно и интересуется MongoDB, чтоб использовать эти фишки.
Я думаю что сообщество возмет из него только лучшее.
Возможность сохранять javascript на сервере мне тоже очень нравится, я уже представляю библиотеки javascript под MongoDB. Или серверный javascript работающий нарямую с MongoDB.
ITшникам будущее представляется всегда в красочных позитивных тонах, в отличии от социологов.
undr
6 Фев 10 at 3:04
Обновился Mongoid до версии 1.2.0. Сделал вольный перевод update’s history (http://mongodb.rightcloud.ru/-mongoid-120-object-mappers-ruby-mongodb)
Musthave
8 Фев 10 at 11:50
Нет сил молчать о приятной новости на тему хостинга проектов с MongoDB! Не сочти за спам и рекламу)))
http://mongodb.rightcloud.ru/-c-mongodb-locumru-offers-a-hosting-for-mongo
Musthave
9 Фев 10 at 9:44
Любопытно. Сейчас нет надобности, но в будущем вероятно потребуется.
undr
9 Фев 10 at 11:29
спасибо за статью. Увидел, как правильно использовать gem russian для денежных единиц)
ferio
9 Фев 10 at 20:16
Это сарказм?
В статье гем russian для отображения денежных единиц не используется. Используется стандартный помощник number_to_currency.
undr
11 Фев 10 at 15:14
не пинайте сильно, но у меня такой вопрос
после того как вы создали контроллеры с действиями. у Вас шаблоны создались с уже готовыми полями ввода? (пытался делать все как описано, но шаблоны без полей ввода, то есть минимальны) что я сделал не так?
да. в строке: MongoMapper.connection = Mongo::Connection.new(mongo['hostname']), 27017, :logger => RAILS_DEFAULT_LOGGER)
ошибка (после mongo['hostname'] скобка не нужна)
DiZ
26 мая 10 at 20:30
Спасибо, что указали на опечатку. Исправил.
Шаблоны я создал сам. Я заменил сгенеренные шаблоны своими.
Имеются ввиду формы new, edit?
undr
26 мая 10 at 22:40
да. я имел в виду формы для создания и редактирования. из контекста статьи я подумал что генерация была автоматической этих формочек.
DiZ
26 мая 10 at 23:16
Ну, по идее, автоматически сгенерированные шаблоны тоже должны работать. Хотя я уже и не помню. В итоге код шаблонов сильно изменился с тех пор. Да и в качестве ORM уже MongoId.
По любому придется добавлять поля.
А стоп!! Присмотрелся к статье:
script/generate controller создает пустые шаблоны. Это не script/generate scaffold
undr
26 мая 10 at 23:43
а чем не устроил MongoMapper?
DiZ
27 мая 10 at 1:18
Как-то кравиве, логичнее и богаче показался MongoId. Там вверху есть небольшая дискуссия на эту тему
undr
27 мая 10 at 9:22