Het bouwen van een persistente in-line editing ervaring met Crystal, MongoDB (datanoise / sam0x17)
Aangezien de documentatie nog schaars is, zou ik er graag wat aan toevoegen.
Dit is wat ik momenteel aan het bouwen ben:
De afzonderlijke velden kunnen worden bewerkt met een inline editor, die automatisch wordt opgeslagen in de backend - de hele pagina hoeft dus niet opnieuw te worden geladen.
De bedoeling van het geheel is een backend voor mijn bedrijf, om een aantal processen te kunnen automatiseren, voorraadprijzen te berekenen, dingen op voorraad te houden, etc. Een magazijn applicatie in principe, op maat gemaakt voor mij!
Gebruikte technologieën
- Crystal
- Kemal
- AdminLTE 2
- Bootstrap 3
- dataTables
- x-editable - een echt geweldige bibliotheek voor inline bewerken, helaas al een tijdje niet meer onderhouden
- zie voorbeelden voor Bootstrap 3 hier
- MongoDB
- Mongo ORM door sam0x17
- die op zijn beurt gebaseerd is op datanoise's Mongo stuurprogramma voor Crystal (mongo.cr)
- Sublime Text voor bewerking
- Robo 3T voor een GUI voor MongoDB
Belangrijke bits
MongoORM gebruikt _id als de hoofdindex. Let op de underscore!
De _id is GEEN tekenreeks. Het is van het type ObjectId
Daarom zal het moeten worden geconstrueerd. Als je het wil opbouwen vanuit een vorig id, gebruik dan deze code:
mongo_id = BSON::ObjectId.new mijn_string_id
waarbij mijn_string_id een String is, en mongo_id een ObjectId.
De code
config/database.yml
is verantwoordelijk voor het opzetten van de communicatie met uw Mongo DB instantie. Raadpleeg hiervoor Sam's documentatie.
stock.cr, verantwoordelijk voor het definiëren van Stockitem, Subitem, en de juiste Kemal handlers:
require "mongo_orm"
require "json"
require "./macros.cr"
require "./weee.cr"module Voorraad
Macro's omvattenclass Itemgroup < Mongo::ORM::Document #used voor het groeperen, bijvoorbeeld Pimoroni producten, Verzending, enz.
veld groep : String
has_many :stockitems 1TP3Merk op dat een stockitem slechts tot één ItemGroup tegelijk kan behoren.
einde1TP3Dit is inclusief andere items ook (scheepvaart, digitale downloads, enz.)
klasse Stockitem < Mongo::ORM::Document
veld sku : String
veld beschrijving : String
veld is_alias : Bool
veld alias_van : String 1TP3Als dit veld is ingesteld, gebruik dan geen andere definities van dit item, haal het gealiaste master item
field is_set : Bool # als dit item een set is van andere items
embeds_many :subitems # moet alleen worden ingesteld indien is_set = true
veld is_physical_item : Bool
field weee_applicable : Bool #kan zijn: betaald door iemand anders, of item is geen elektronica, ...
veld gewicht : Int32 # in g
tijdstempels
eindeklasse Subitem < Mongo::ORM::EmbeddedDocument
field sku : String #a verwijzing naar het belangrijkste voorraaditem. NB: dit kan waarschijnlijk op een mooiere manier worden herwerkt met belongs_to ? bv. belongs_to :itemref, class_name: Stock::Stockitem
veld opmerking : String
veld bedrag : Int32
eindeget "/stock/show" do |env|
#https://github.com/datanoise/mongo.cr
sis = Stockitem.all({"is_alias" => {"$eq" => false}})
name = "Voorraad :: Tonen en bewerken"
mrender "stock"
eindepost "/stock/edit/:what" do |env|
pp "Wijziging invoeren, met doel " + env.params.url["wat"]
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)
waarde = env.params.body["waarde"]?.as(String)
indien si = Stockitem.find(BSON::ObjectId.new pk)
case env.params.url["wat"]
wanneer "sku"
si.sku = waarde
wanneer "omschrijving"
si.description = waarde
wanneer "is_alias"
si.is_alias = value == "true" ? true : false
wanneer "alias_van"
si.alias_van = waarde
wanneer "is_set"
si.is_set = waarde == "true" ? true : false
wanneer "is_physical_item"
si.is_physical_item = value == "true" ? true : false
wanneer "weee_applicable"
pp "weee_applicable"
si.weee_applicable = value == "true" ? true : false
einde
als si.save
pp "inside si.save"
env.response.status_code = 200
anders
env.response.status_code = 400
einde
anders
env.response.status_code = 400
einde
anders
env.response.status_code = 400
einde
einde#genereren uit WEEE inzendingen
get "/stock/generate/:jaar" do |env|
1TP3Wandel door de hele database, verzamel de items, en toon een lijst
voorraad_verzameling = Hash(String,String).new
facturen = WEEE::Invoice.all({"jaar"=>env.params.url["jaar"].to_i32})
invoices.each do |invoice|
#p "verwerking factuur #{invoice.number}"
1TP3Merk op dat we misschien niet helemaal alle facturen hier krijgen, omdat easybill een andere kijk heeft op de data (import)
invoice.items.each do |item|
indien !.item.omschrijving.nihil? && !.item.aantal.nihil?
sleutel = item.nummer == "" ? item.omschrijving : item.nummer
indien !.key.nil?
indien !stock_collection.has_key?(sleutel)
description = ""
indien !.item.description.nil?
beschrijving = item.beschrijving
einde
#stock_collection[key] = item.description
indien !.description.nil?
voorraad_verzameling[sleutel] = beschrijving
einde
end # end of !stock_collection.has_key?
end # end of if !.key.nil?
einde
end #end van invoice.items.each do
einde #end van facturen.elk
1TP3Verifieer dat dit niet al bestaat in onze database, anders maak het aan.
stock_collection.each do |sku,description|
indien !Stockitem.find_by(:sku, sku)
si = Stockitem.new
si.sku = sku
si.is_alias = false
si.alias_van = ""
si.description = beschrijving
si.is_set = false
si.is_physical_item = true
si.weee_applicable = true
si.weight = 100
Ja, spaar!
einde
einde
einde #end van get macroeinde #en einde van module
Opmerkingen:
- Sam's documentatie legt delen van deze code uit, ik zal alleen ingaan op de struikelblokken:
- sis = Stockitem.all({"is_alias" => {"$eq" => false}}) - deze code zoekt naar Stockitems (Mongo ORM zal zorgen voor de DB naam voor u, enz.)
- merk op dat ik er niet in geslaagd ben om ({}) in te stellen - zal dit verder moeten onderzoeken
- deze syntaxis is in principe wat MongoDB verwacht. zie https://docs.mongodb.com/manual/reference/operator/query/
- si = Stockitem.find(BSON::ObjectId.new pk)
- Merk op dat hier BSON::ObjectId.new wordt gebruikt om de id te reconstrueren uit de sleutel pk die werd doorgegeven
- de retourcode 200 bij succes en 400 bij falen is voor x-editable om te weten of de bewerking geslaagd is
Gedrag van si.save
- si.save! geeft een foutmelding als opslaan niet mogelijk is
- si.save zal true teruggeven als opslaan mogelijk was, en false als het niet mogelijk was
stock.ecr, het HTML sjabloon
<% 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”">
Opgelet: alias'ed items worden niet uit de databank geladen.
<thead>
<tr role="”row”">
<th class="”sorting_asc”" tabindex="”0″">sku</th>
<th>beschrijving</th>
<th>is_alias?</th>
<th>alias_van</th>
<th>is_set?</th>
<th>subitems</th>
<th>is_physical_item?</th>
<th>wee_toepasselijk?</th>
<th>gewicht (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="[{waarde: 'true', tekst: 'true'}, {waarde: 'false', tekst: '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(functie(){
$next.editable('toon');
}, 300);
}
});
});
}
</script>
<% end %>
Opmerkingen:
- aangezien we onze JS scripts hierna toevoegen (ik zou dit moeten herwerken om een aangepaste script sectie aan het einde toe te voegen), moeten we ze verpakken in een window.onload functie - anders wordt $ niet gedefinieerd (jQuery snelkoppeling)
- Merk op hoe we hier een functie gebruiken om naar het volgende bewerkbare te springen bij succes met opslaan
- si._id.to_s.rchop - de BSON id wordt vertaald naar een string, en dan wordt het laatste vreemde teken (null terminator?) afgehakt. Dit is een belangrijk struikelblok!
lay-out.ecr (uittreksels)
<!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="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 ::
God zijn is leuk.
</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>
Verdere referenties
- 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 -> definitie van save in mongo.cr
- https://github.com/sam0x17/mongo_orm/blob/015ae29a29ca8b80a98a531de891095bbd42d028/src/mongo_orm/persistence.cr -> definitie van save in mongo_orm