Construyendo una experiencia de edición en línea persistente con Crystal, MongoDB (datanoise / sam0x17)
Como la documentación sigue siendo escasa, me gustaría añadir algo.
Esto es lo que estoy construyendo actualmente:
Los campos individuales se podrán editar con un editor en línea, que se guardará automáticamente en el backend, sin necesidad de recargar toda la página.
El objetivo del conjunto es un backend para mi empresa, para poder automatizar algunos procesos, calcular precios de stock, mantener cosas en stock, etc. Una aplicación de almacén básicamente, ¡a mi medida!
Tecnologías utilizadas
- Cristal
- Kemal
- AdminLTE 2
- Bootstrap 3
- dataTables
- x-editable - una biblioteca realmente impresionante para la edición en línea, lamentablemente no se ha mantenido durante algún tiempo
- ver ejemplos de Bootstrap 3 aquí
- MongoDB
- Mongo ORM por sam0x17
- que a su vez se basa en Controlador Mongo de datanoise para Crystal (mongo.cr)
- Sublime Text para la edición
- Robo 3T para una GUI para MongoDB
Partes importantes
MongoORM utiliza _id como índice principal. Tenga en cuenta el guión bajo!
El _id NO es una cadena. Es del tipo ObjectId
Por lo tanto, tendrá que ser construido. Si quieres construirlo a partir de un id anterior, utiliza este código:
mongo_id = BSON::ObjectId.new mi_cadena_id
donde my_string_id es una Cadena, y mongo_id es un ObjectId.
El código
config/base de datos.yml
es responsable de establecer la comunicación con su instancia de Mongo DB. Por favor, consulte Documentación de Sam.
stock.cr, responsable de definir Stockitem, Subitem, y los manejadores Kemal apropiados:
requiere "mongo_orm"
requieren "json"
require "./macros.cr"
requerir "./weee.cr"módulo Stock
incluir Macrosclass Itemgroup < Mongo::ORM::Document #utilizado para agrupar, por ejemplo, productos Pimoroni, envíos, etc.
grupo de campos : Cadena
has_many :stockitems #enga en cuenta que el stockitem sólo puede pertenecer a un ItemGroup a la vez.
fin1TP3Esto incluye también otros artículos (envío, descargas digitales, etc.)
class Stockitem < Mongo::ORM::Document
campo sku : Cadena
descripción del campo : Cadena
campo is_alias : Bool
campo alias_of : String 1TP3Si este campo está configurado, no utilice ninguna otra definición de este elemento, obtenga el elemento maestro aliased
field is_set : Bool # si este elemento es un conjunto de otros elementos
embeds_many :subitems # sólo debe establecerse si is_set = true
campo is_physical_item : Bool
campo weee_applicable : Bool 1TP3Puede ser: pagado por otra persona, o el artículo no es electrónico, ...
peso del campo : Int32 # en g
marcas de tiempo
finclass Subitem < Mongo::ORM::EmbeddedDocument
field sku : String #a referencia al artículo de stock principal. NB: esto probablemente podría ser reelaborado de una manera más agradable con belongs_to ? por ejemplo belongs_to :itemref, class_name: Stock::Stockitem
campo nota : Cadena
campo cantidad : Int32
finget "/stock/show" do |env|
#https://github.com/datanoise/mongo.cr
sis = Stockitem.all({"is_alias" => {"$eq" => false}})
name = "Stock :: Mostrar y editar"
mrender "stock"
finpost "/stock/edit/:what" do |env|
pp "Entrando en edición, con objetivo " + env.params.url["what"]
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)
si si = Stockitem.find(BSON::ObjectId.new pk)
case env.params.url["what"]
cuando "sku"
si.sku = valor
cuando "descripción"
si.descripción = valor
cuando "is_alias"
si.is_alias = valor == "true" ? true : false
cuando "alias_de"
si.alias_of = valor
cuando "is_set"
si.is_set = valor == "true" ? true : false
cuando "is_physical_item"
si.is_physical_item = valor == "true" ? true : false
cuando "weee_applicable"
pp "weee_applicable"
si.weee_applicable = valor == "true" ? true : false
fin
si.save
pp "dentro de si.save"
env.response.status_code = 200
si no
env.response.status_code = 400
fin
si no
env.response.status_code = 400
fin
si no
env.response.status_code = 400
fin
fin#generar a partir de entradas de RAEE
get "/stock/generate/:year" do |env|
1TP3Recorrer toda la base de datos, recoger los elementos y mostrar una lista
stock_collection = Hash(String,String).new
facturas = WEEE::Invoice.all({"año"=>env.params.url["año"].to_i32})
facturas.each do |factura|
#p "procesando la factura #{número de factura}"
#enga en cuenta que es posible que no obtengamos absolutamente todas las facturas aquí, ya que easybill tiene una visión diferente de las fechas (importación)
invoice.items.each do |item|
if !item.description.nil? && !item.number.nil?
key = item.number == "" ? item.description : item.number
if !key.nil?
if !stock_collection.has_key?(key)
descripción = ""
if !item.description.nil?
descripción = item.description
fin
#stock_collection[key] = item.description
if !description.nil?
stock_collection[key] = descripción
fin
end # end of ¿acervo_colección.tiene_clave?
end # end of if !key.nil?
fin
end #end de invoice.items.each do
end #end de invoices.each
1TP3Comprobar que no existe ya en nuestra base de datos, si no crearla.
stock_collection.each do |sku,description|
if !Stockitem.find_by(:sku, sku)
si = Stockitem.new
si.sku = sku
si.is_alias = false
si.alias_of = ""
si.description = descripción
si.is_set = false
si.is_physical_item = true
si.weee_applicable = true
si.peso = 100
¡si.save!
fin
fin
end #end de la macro getfin #end del módulo
Notas:
- La documentación de Sam explica partes de este código, yo sólo me referiré a los escollos:
- sis = Stockitem.all({"is_alias" => {"$eq" => false}}) - este código busca Stockitems (Mongo ORM se encargará del nombre de la BD por ti, etc.)
- nota que no conseguí poner ({}) - tendré que investigarlo más a fondo
- esta sintaxis es básicamente lo que espera MongoDB. ver https://docs.mongodb.com/manual/reference/operator/query/
- si = Stockitem.find(BSON::ObjectId.new pk)
- nota que aquí se utiliza BSON::ObjectId.new para reconstruir el id a partir de la clave pk que se pasó
- el código de retorno 200 en caso de éxito y 400 en caso de fallo es para que x-editable sepa si la edición ha tenido éxito
Comportamiento de si.save
- si.save! dará un error si no es posible guardar
- si.save devolverá true si se ha podido guardar, y false si no se ha podido
stock.ecr, la plantilla 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”">
Tenga en cuenta que los elementos con alias no se cargan desde la base de datos.
<thead>
<tr role="”row”">
<th class="”sorting_asc”" tabindex="”0″">sku</th>
<th>descripción</th>
<th>¿es_alias?</th>
<th>alias_de</th>
<th>¿es_conjunto?</th>
<th>subartículos</th>
<th>¿es_físico_elemento?</th>
<th>¿wee_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"
data-pk=""
data-value="true"
data-source="[{valor: 'true', texto: 'true'}, {valor: 'false', texto: 'false'}]"
data-url="/stock/edit/weee_applicable"
data-title="RAEE: "
>
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.editable.defaults.mode = 'popup';
$(".editable").editable();
$(".editable").on('hidden', function(e, reason){
if(reason === 'save' | reason === 'nochange'){
var $next = $(this).closest('tr').next().find('.editable');
setTimeout(function(){
$next.editable('show');
}, 300);
}
});
});
}
</script>
<% end %>
Notas:
- ya que añadimos nuestros scripts JS después de esto (debería rehacer esto para añadir una sección de scripts personalizados al final), tenemos que envolverlos en una función window.onload - de lo contrario $ no está definido (atajo de jQuery)
- observe cómo usamos una función aquí para saltar al siguiente editable al guardar con éxito
- si._id.to_s.rchop - el id de BSON se traduce a una cadena, y luego se corta el último carácter extraño (¿terminador nulo?). Este es un obstáculo importante.
layout.ecr (extractos)
<!DOCTYPE html>
<html>
<head>
<meta charset=”utf-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
Dios de los Impuestos |
<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="stylesheet"
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 Dios es 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>
Otras referencias
- 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 -> definición de guardar en mongo.cr
- https://github.com/sam0x17/mongo_orm/blob/015ae29a29ca8b80a98a531de891095bbd42d028/src/mongo_orm/persistence.cr -> definición de guardar en mongo_orm