Construire une expérience d'édition en ligne persistante avec Crystal, MongoDB (datanoise / sam0x17)
Comme la documentation est encore peu abondante, j'aimerais en ajouter.
C'est ce que je suis en train de construire actuellement :
Les différents champs seront modifiables à l'aide d'un éditeur en ligne, qui sera automatiquement sauvegardé dans le backend - il ne sera pas nécessaire de recharger la page entière.
Le but de l'ensemble est de créer un backend pour mon entreprise, afin de pouvoir automatiser certains processus, calculer les prix du stock, garder les choses en stock, etc. Une application d'entrepôt, en fait, faite sur mesure pour moi !
Technologies utilisées
- Cristal
- Kemal
- AdminLTE 2
- Bootstrap 3
- dataTables
- x-editable - une bibliothèque vraiment impressionnante pour l'édition en ligne, malheureusement non maintenue depuis un certain temps
- voir les exemples pour Bootstrap 3 ici
- MongoDB
- Mongo ORM par sam0x17
- qui à son tour est basé sur Pilote Mongo pour Crystal de datanoise (mongo.cr)
- Sublime Text pour l'édition
- Robo 3T pour une interface graphique pour MongoDB
Bouts importants
MongoORM utilise _id comme index principal. Veuillez noter le trait de soulignement!
L'_id n'est PAS une chaîne de caractères. Il est du type ObjectId
Il faudra donc le construire. Si vous voulez le construire à partir d'un id précédent, utilisez ce code :
mongo_id = BSON::ObjectId.new my_string_id
où ma_chaîne_id est une Chaîne, et mongo_id est un ObjectId.
Le code
config/database.yml
est responsable de la mise en place de la communication avec votre instance de Mongo DB. Veuillez vous référer à Documentation de Sam.
stock.cr, responsable de la définition de Stockitem, Subitem, et des gestionnaires Kemal appropriés :
requiert "mongo_orm"
exigez "json"
exiger "./macros.cr".
exiger "./weee.cr"module Stock
inclure les macrosclass Itemgroup < Mongo::ORM::Document 1TP3Utilisé pour le regroupement, par exemple les produits Pimoroni, l'expédition, etc.
champ groupe : Chaîne
has_many :stockitems 1TP3Notez que le stockitem ne peut appartenir qu'à un ItemGroup à la fois.
fin#cela inclut également d'autres éléments (frais d'expédition, téléchargements numériques, etc.).
class Stockitem < Mongo::ORM::Document
field sku : String
champ description : String
champ is_alias : Bool
champ alias_of : String 1TP3Si ce champ est défini, n'utilisez pas d'autres définitions de cet élément, obtenez l'élément principal aliasé.
field is_set : Bool # si cet élément est un ensemble d'autres éléments
embeds_many : les sous-items # ne doivent être définis que si is_set = true
field is_physical_item : Bool
field weee_applicable : Bool #can be either : paid for someone else, or item is not electronics, ...
poids du champ : Int32 # en g
horodatage
finclasse Subitem < Mongo::ORM::EmbeddedDocument
field sku : String #a référence à l'article principal du stock. NB : ceci pourrait probablement être retravaillé de façon plus agréable avec belongs_to ? par exemple belongs_to :itemref, class_name : Stock::Stockitem
note de champ : Chaîne
champ montant : Int32
finget "/stock/show" do |env|
#https://github.com/datanoise/mongo.cr
sis = Stockitem.all({"is_alias" => {"$eq" => false}})
name = "Stock : : Montrer et modifier".
mrender "stock"
finpost "/stock/edit/:what" do |env|
pp "Entrée en modification, avec la cible " + 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)
valeur = env.params.body["valeur"] ?.as(String)
if si = Stockitem.find(BSON::ObjectId.new pk)
cas env.params.url["quoi"]
quand "sku"
si.sku = valeur
quand "description"
si.description = valeur
when "is_alias"
si.is_alias = valeur == "true" ? true : false
quand "alias_of"
si.alias_of = valeur
quand "is_set"
si.is_set = valeur == "true" ? true : false
when "is_physical_item"
si.is_physical_item = value == "true" ? true : false
quand "weee_applicable"
pp "weee_applicable"
si.weee_applicable = valeur == "true" ? true : false
fin
si si.save
pp "inside si.save"
env.response.status_code = 200
sinon
env.response.status_code = 400
fin
sinon
env.response.status_code = 400
fin
sinon
env.response.status_code = 400
fin
fin#générer à partir des entrées DEEE
get "/stock/generate/:year" do |env|
#raverser toute la base de données, collecter les éléments, et afficher une liste
stock_collection = Hash(String,String).new
factures = WEEE::Invoice.all({"year"=>env.params.url["year"].to_i32})
factures.each do |invoice|
#p "traitement de la facture #{numéro.facture}"
#enez compte du fait qu'il se peut que nous n'obtenions pas absolument toutes les factures ici, car easybill a une vision différente des dates (importation).
facture.items.each do |item|
if !item.description.nil ? && !item.number.nil ?
clé = numéro de l'article == "" ? description de l'article : numéro de l'article
if !key.nil ?
if !stock_collection.has_key ?(key)
description = ""
if !item.description.nil ?
description = item.description
fin
#stock_collection[key] = item.description
if !description.nil ?
stock_collection [clé] = description
fin
end # end of !stock_collection.has_key ?
end # end of if !key.nil ?
fin
end #end of invoice.items.each do
fin #end des factures.each
1TP3Vérifiez que cette information n'existe pas déjà dans notre base de données, sinon créez-la.
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 = description
si.is_set = false
si.is_physical_item = true
si.weee_applicable = true
si.weight = 100
si.save !
fin
fin
fin de la macro #end of getfin 1TP3Fin du module
Notes :
- La documentation de Sam explique certaines parties de ce code, je n'aborderai que les points d'achoppement :
- sis = Stockitem.all({"is_alias" => {"$eq" => false}} - ce code recherche les Stockitems (Mongo ORM se chargera du nom de la base de données pour vous, etc.)
- notez que je n'ai pas réussi à mettre ({}) - il faudra approfondir la question
- cette syntaxe est fondamentalement ce que MongoDB attend. voir https://docs.mongodb.com/manual/reference/operator/query/
- si = Stockitem.find(BSON::ObjectId.new pk)
- Notez qu'ici BSON::ObjectId.new est utilisé pour reconstruire l'identifiant à partir de la clé pk qui a été passée.
- le code de retour 200 en cas de succès et 400 en cas d'échec permet à x-editable de savoir si la modification a réussi.
Comportement de si.save
- si.save ! lèvera une erreur si la sauvegarde n'est pas possible.
- si.save retournera vrai si la sauvegarde a été possible, et faux si elle n'a pas été possible.
stock.ecr, le modèle 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”">
Veuillez noter que les éléments aliasés ne sont pas chargés à partir de la base de données.
<thead>
<tr role="”row”">
<th class="”sorting_asc”" tabindex="”0″">sku</th>
<th>description</th>
<th>is_alias ?</th>
<th>alias_de</th>
<th>is_set ?</th>
<th>sous-rubriques</th>
<th>is_physical_item ?</th>
<th>weee_applicable ?</th>
<th>poids (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="[{value : 'true', text : 'true'}, {value : 'false', text : 'false'}]"
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.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 %>
Notes :
- puisque nous ajoutons nos scripts JS après ceci (je devrais retravailler ceci pour ajouter une section de script personnalisé à la fin), nous devons les envelopper dans une fonction window.onload - sinon $ n'est pas défini (raccourci jQuery)
- notez comment nous utilisons une fonction ici pour passer à l'élément éditable suivant lorsque la sauvegarde est réussie.
- si._id.to_s.rchop - l'id BSON est traduit en une chaîne de caractères, puis le dernier caractère bizarre (terminateur nul ?) est coupé. C'est une importante pierre d'achoppement !
layout.ecr (extraits)
<!DOCTYPE html>
<html>
<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”>
.
<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 : :
Etre Dieu est amusant.
</h1>
<!– Main content –>
<%= yield_content “main” %>
<!– /.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>
Autres références
- 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 -> définition de save dans mongo.cr
- https://github.com/sam0x17/mongo_orm/blob/015ae29a29ca8b80a98a531de891095bbd42d028/src/mongo_orm/persistence.cr -> définition de save dans mongo_orm