Construindo uma experiência persistente de edição em linha com Crystal, MongoDB (datanoise / sam0x17)
Como a documentação ainda é escassa, eu gostaria de acrescentar alguma.
Isto é o que eu estou a construir actualmente:
Os campos individuais serão editáveis com um editor inline, que salvará automaticamente para o backend - não há necessidade de recarregar a página inteira.
O objectivo do todo é um backend para a minha empresa, para poder automatizar alguns processos, calcular os preços das acções, manter as coisas em stock, etc. Uma aplicação de armazém basicamente, feita sob medida para mim!
Tecnologias utilizadas
- Crystal
- Kemal
- AdminLTE 2
- Pega de Botas 3
- dataTables
- x-editable - uma biblioteca realmente fantástica para edição em linha, infelizmente não mantida por algum tempo.
- ver exemplos para o Bootstrap 3 aqui
- MongoDB
- Mongo ORM por sam0x17
- que, por sua vez, é baseada Datanoise's Mongo Driver for Crystal (mongo.cr)
- Texto Sublime para edição
- Robo 3T para uma GUI para MongoDB
Bocados importantes
MongoORM usa _id como o índice principal. Por favor, note o sublinhado!
O _id NÃO é um fio. É do tipo ObjectId
Portanto, terá de ser construída. Se você quiser construí-lo a partir de uma identificação anterior, use este código:
mongo_id = BSON::ObjectId.new my_string_id
onde my_string_id é um String, e mongo_id é um ObjectId.
O código
config/database.yml
é responsável por estabelecer a comunicação com a sua instância Mongo DB. Por favor, consulte Documentação da Sam.
stock.cr, responsável pela definição de Stockitem, Subitem, e os manipuladores Kemal apropriados:
requerem "mongo_orm".
exigir "json".
exigir "./macros.cr".
exigir "./weee.cr".módulo Estoque
incluir Macrosclasse Grupo de itens < Mongo::ORM::Documento # utilizado para agrupamento, por exemplo, produtos Pimoroni, Expedição, etc.
grupo de campo : Corda
has_many :stockitems #otar que o item de estoque só pode pertencer a um ItemGroup de cada vez.
final#isso inclui também outros itens (envio, downloads digitais, etc.)
classe Stockitem < Mongo::ORM::Document
field sku : Corda
descrição do campo : String
campo is_alias : Bool
alias_de_campo : String #if este campo está definido, não use nenhuma outra definição deste item, obtenha o item principal alias_of
campo is_set : Bool # se este item for um conjunto de outros itens
embeds_many :subitens # só deve ser definido se is_set = verdadeiro
campo is_physical_item : Bool
field weee_applicable : Bool #can ser: pago por outra pessoa, ou item não é eletrônico, ...
peso do campo : Int32 # em g
carimbos temporais
finalclass Subitem < Mongo::ORM::EmbeddedDocument
campo sku : String #a referência ao item de estoque principal. NB: isto provavelmente poderia ser retrabalhado de uma forma mais agradável com belongs_to ? por exemplo, belongs_to :itemref, class_name: Stock::Stockitem
nota de campo : Cordão
valor do campo : Int32
finalobter "/stock/show" do |env|
#https://github.com/datanoise/mongo.cr
sis = Stockitem.all({"is_alias" => {"$eq" => false}})
nome = "Stock :: Mostrar & Editar"
"estoque" de resgate
finalpost "/stock/edit/:what" do |env|
pp "Entering edit, with target " + env.params.url["o quê"]
pp env.params.body
if env.params.body.has_key?("pk") && env.params.body.has_key?("value")
pp "Inside pk & value"
pk = env.params.body["pk"]?.as(String)
value = env.params.body["value"]?.as(String)
if si = Stockitem.find(BSON::ObjectId.new pk)
case env.params.url["o quê"]
quando "sku"
si.sku = valor
quando "descrição"
si.description = valor
quando "is_alias
si.is_alias = valor == "verdadeiro" ? verdadeiro : falso
quando "alias_of"
si.alias_of = valor
quando "is_set"
si.is_set = valor == "verdadeiro" ? verdadeiro : falso
quando "is_physical_item"
si.is_physical_item = valor == "true" ? true : false
quando "weee_applicable"
pp "weee_applicable"
si.weee_applicable = valor == "true" ? true : false
final
se si.save
pp "inside si.save"
env.response.status_code = 200
além disso
env.response.status_code = 400
final
além disso
env.response.status_code = 400
final
além disso
env.response.status_code = 400
final
final#generar a partir de entradas REEE
obter "/stock/generate/:ano" do |env|
#walk por toda a base de dados, recolher os itens e exibir uma lista
stock_collection = Hash(String,String).new
facturas = WEEE::Invoice.all({"ano"=>env.params.url["ano"].to_i32})
facturas.cada uma fazem |facturação|
#p "processar factura #{invoice.number}"
#note que podemos não receber absolutamente todas as faturas aqui, pois a easybill tem uma visão diferente das datas (importação)
invoice.items.each do |item|
se !item.description.nil? && !item.number.nil?
chave = item.número == "" ? item.descrição : item.número
se !key.nil?
se !stock_collection.has_key?(key)
descrição = ""
se !item.description.nil?
descrição = item.descrição
final
#stock_collection[chave] = item.descrição
se !description.nil?
stock_collection[chave] = descrição
final
end # end de !stock_collection.has_key?
end # end of if !key.nil?
final
fim #end of invoice.items.each do
fim #endência de facturas.cada
#check que isto não existe já na nossa base de dados, caso contrário crie-o.
stock_collection.each do |sku,description|
if !Stockitem.find_by(:sku, sku)
si = Stockitem.new
si.sku = sku
si.is_alias = falso
si.alias_of = ""
si.description = descrição
si.is_set = falso
si.is_physical_item = verdadeiro
si.weee_applicable = true
si.peso = 100
si.save!
final
final
fim #end of get macrofim #end do módulo
Notas:
- A documentação do Sam explica partes deste código, só vou abordar os tropeços:
- sis = Stockitem.all({"is_alias" => {"$eq" => false}}) - este código procura Stockitems (Mongo ORM tratará do nome do BD para si, etc.)
- note que eu não consegui definir ({}) - terei que investigar isto mais a fundo
- esta sintaxe é basicamente o que o MongoDB espera. veja https://docs.mongodb.com/manual/reference/operator/query/
- si = Stockitem.find(BSON::ObjectId.new pk)
- note que aqui BSON::ObjectId.new é usado para reconstruir o id a partir da chave pk que foi passada
- código de retorno 200 no sucesso e 400 no fracasso é para x-editable saber se a edição foi bem sucedida
Comportamento do si.save
- si.save! irá levantar um erro se não for possível salvar
- si.save retornará verdadeiro se for possível salvar, e falso se não for possível
stock.ecr, o modelo HTML
<% content_for “name” do %>
<%= name %>
<% end %><% content_for “main” do%>
<div class="”col-md-12″">
<div class="”box" box-primary”>
<div class="box-header" with-border”>
<h3 class="”box-title”"><%= name %></h3>
</div>
<div class="”box-body”">
Atenção: os itens alias'ed não são carregados a partir da base de dados.
<thead>
<tr role="”row”">
<th class="”sorting_asc”" tabindex="”0″">sku</th>
<th>descrição</th>
<th>is_alias?</th>
<th>alias_of</th>
<th>is_set?</th>
<th>subitens</th>
<th>is_physical_item?</th>
<th>weee_applicable?</th>
<th>peso (g)</th>
</tr>
</thead>
<% sis.each do |si| %>
tbd.
<<A HREF="#"
id=".weee_applicable"
class="weee_applicable editable"
data-type="select" (selecionar)
data-pk=""
data-value="true" (verdadeiro)
data-source="[{valor: 'verdadeiro', texto: 'verdadeiro'}, {valor: 'falso', texto: 'falso'}]".
data-url="/stock/edit/weee_applicable"
data-title="WEEE: "
>
g
<% end %>
</div>
</div>
</div>
<script>
//https://datatables.net/examples/styling/bootstrap
//demo.js x-editable
window.onload = function() {
$(document).ready(function) (){
$("#stock_items").DataTable();
$.fn.editável.defaults.mode = 'popup';
$(".editável").editável();
$(".editável").on('oculto', function(e, razão){
if(reason ==== 'save' || reason ==== 'nochange'){
var $next = $(this).closest('tr').next().find('.editável');
setTimeout(function(){
$next.editable('show');
}, 300);
}
});
});
}
</script>
<% end %>
Notas:
- como adicionamos nossos scripts JS depois disso (eu deveria retrabalhar isso para adicionar uma seção de scripts personalizados no final), temos que embrulhá-los em uma função window.onload - caso contrário o $ não está definido (atalho jQuery)
- note como usamos uma função aqui para saltar para a próxima editável no save success
- si._id.to_s.rchop - o id BSON é traduzido para uma string, e depois o último carácter estranho (terminador nulo?) é cortado. Este é um importante tropeço!
layout.ecr (trechos)
<!DOCTYPE html>
<html>
<head>
<meta charset=”utf-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
TaxGod |
<meta content=”width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no” name=”viewport”>
<link rel=”stylesheet” href=”/bower_components/bootstrap/dist/css/bootstrap.min.css”>
<link rel=”stylesheet” href=”/bower_components/font-awesome/css/font-awesome.min.css”>
<link rel=”stylesheet” href=”/bower_components/Ionicons/css/ionicons.min.css”>
<link rel=”stylesheet” href=”/dist/css/AdminLTE.min.css”>
<link rel=”stylesheet” href=”/dist/css/skins/skin-blue.min.css”>
<link rel=”stylesheet” href=”/danielm_uploader/css/jquery.dm-uploader.min.css”>
<link rel=”stylesheet” href=”/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css”>
<link rel=”stylesheet” href=”/tg_css/bootstrap-editable.css”>
<link rel=”stylesheet” href=”/tg_css/picockpit.css”>
<link rel=”stylesheet” href=”/tg_css/dropzone.css”>
<!–[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js”>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js”>
<![endif]–><link rel="folha de estilo"
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic”>
</head>
<body class="”hold-transition" skin-blue sidebar-mini”>
<div class="”wrapper”">(…)
<!– Content Wrapper. Contains page content –>
<div class="”content-wrapper”">
<!– Content Header (Page header) –>
<h1>
TaxGod ::
Ser Deus é divertido.
</h1>
</section><!– Main content –>
<%= yield_content “main” %>
</section>
<!– /.content –>
</div>
<!– /.content-wrapper –>(…)
</div>
<!– ./wrapper –><!– REQUIRED JS SCRIPTS –>
<!– jQuery 3 –>
<script src=”/bower_components/jquery/dist/jquery.min.js”></script>
<!– Bootstrap 3.3.7 –>
<script src=”/bower_components/bootstrap/dist/js/bootstrap.min.js”></script>
<!– AdminLTE App –>
<script src=”/danielm_uploader/js/jquery.dm-uploader.min.js”></script>
<script src=”/bower_components/datatables.net/js/jquery.dataTables.min.js”></script>
<script src=”/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js”></script>
<script src=”/dist/js/adminlte.min.js”></script>
<script src=”/tg_js/bootstrap-editable.min.js”></script>
<script src=”/tg_js/dropzone.min.js”></script>
</body>
</html>
Outras referências
- https://github.com/sam0x17/mongo_orm/blob/893cd520cb90f7049d53b809ada30198deeed0f7/spec/fields_spec.cr
- https://github.com/datanoise/mongo.cr/commit/86c9e530c0ac980ed7b15e653746bc5b6f2527fc
- https://github.com/amberframework/granite/blob/master/docs/getting_started.md
- https://docs.mongodb.com/manual/reference/operator/query/
- https://github.com/datanoise/mongo.cr/issues/19
- https://github.com/datanoise/mongo.cr/blob/86c9e530c0ac980ed7b15e653746bc5b6f2527fc/src/mongo/collection.cr -> definição de salvar em mongo.cr
- https://github.com/sam0x17/mongo_orm/blob/015ae29a29ca8b80a98a531de891095bbd42d028/src/mongo_orm/persistence.cr -> definição de salvar em mongo_orm