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 :

image

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.

image

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
  • Sublime Text pour l'édition Sourire
  • Robo 3T pour une interface graphique pour MongoDB

Bouts importants

image

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 macros

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

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

    get "/stock/show" do |env|
         #https://github.com/datanoise/mongo.cr
         sis = Stockitem.all({"is_alias" => {"$eq" => false}})
         name = "Stock : : Montrer et modifier".
         mrender "stock"
     fin

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

fin 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.)
  • 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| %>
                
                    
                    
                    
                    
                    
                    
                    
                    
                    
                
                 <% end %>

           

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

         </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