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:

imagem

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.

imagem

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
  • Texto Sublime para edição Sorria
  • Robo 3T para uma GUI para MongoDB

Bocados importantes

imagem

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 Macros

    classe 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
     final

    class 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
     final

    obter "/stock/show" do |env|
         #https://github.com/datanoise/mongo.cr
         sis = Stockitem.all({"is_alias" => {"$eq" => false}})
         nome = "Stock :: Mostrar & Editar"
         "estoque" de resgate
     final

    post "/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 macro

fim #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.)
  • 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.
             <TABLE class=”table table-bordered table-striped dataTable” role=”grid” aria-describedby=”Table with stock items” id=”stock_items”>
                 <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