clir-data_manager 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1184 @@
1
+ module Clir
2
+ #
3
+ # Structure qui permet de conserver les items groupés
4
+ # @note
5
+ # Cf. @@group_by et le manuel.
6
+ #
7
+ GroupedItems = Struct.new(:name, :id, :items)
8
+
9
+ module DataManager
10
+ class << self
11
+ def new(classe, data_properties = nil)
12
+ Manager.new(classe, data_properties)
13
+ end
14
+ end #/<< self module
15
+
16
+ class Manager
17
+
18
+ ##
19
+ # Méthode qui retourne un nouvel identifiant pour la classe
20
+ # propriétaire.
21
+ #
22
+ # Suivant le type de données, on trouve le dernier identifiant :
23
+ # - dans un fichier contenant les informations générales sur
24
+ # la classe d'objets
25
+ # - en relevant les ID d'un fichier YAML et en prenant le
26
+ # dernier.
27
+ # - en fouillant dans un dossier de fiches pour trouver la
28
+ # dernière.
29
+ #
30
+ def __new_id
31
+ case save_system
32
+ when :card
33
+ id = ensure_last_id_by_card(__last_id + 1)
34
+ #
35
+ # On peut enregistrer le nouvel identifiant
36
+ #
37
+ File.write(last_id_path, id.to_s)
38
+ return id
39
+ when :file, :csv
40
+ @last_id || begin
41
+ load_data
42
+ @last_id
43
+ end
44
+ return @last_id += 1
45
+ when :conf
46
+ puts "Je ne sais pas encore gérer le système de sauvegarde :conf.".orange
47
+ raise(ExitSilently)
48
+ end
49
+ end
50
+
51
+ # Méthode qui s'assure de retourner un identifiant qui n'existe
52
+ # vraiment pas.
53
+ #
54
+ # Il peut arriver que l'utitilisateur ajoute des données à la
55
+ # main (par fiche), il faut donc s'assurer que cet identifiant est
56
+ # bien inusité
57
+ #
58
+ # @param [Integer] sid L'identifiant à vérifier
59
+ # @return [Integer] L'identifiant libre
60
+ def ensure_last_id_by_card(sid)
61
+ case save_format
62
+ when :yaml
63
+ sid += 1 while File.exist?(File.join(save_location,"#{sid}.yaml"))
64
+ else
65
+ raise "Je ne sais pas traiter le format #{save_format.inspect}…"
66
+ end
67
+ return sid
68
+ end
69
+
70
+ # @return [Integer] Last used ID for objet/instance
71
+ def __last_id
72
+ if File.exist?(last_id_path)
73
+ File.read(last_id_path).strip.to_i
74
+ else
75
+ mkdir(File.dirname(last_id_path)) # to make sure folder exists
76
+ 0
77
+ end
78
+ end
79
+
80
+ attr_reader :classe
81
+ attr_reader :data_properties
82
+ attr_reader :items
83
+
84
+ def initialize(classe, data_properties = nil)
85
+ @data_properties = data_properties || begin
86
+ defined?(classe::DATA_PROPERTIES) || raise(ERRORS[:require_data_properties] % classe.name)
87
+ classe::DATA_PROPERTIES
88
+ end
89
+ @classe = classe
90
+ #
91
+ # Pour que rubocop ne râle pas…
92
+ #
93
+ @table = nil
94
+ @is_full_loaded = false
95
+ #
96
+ # Méthodes d'instance
97
+ #
98
+ prepare_instance_methods_of_class
99
+ #
100
+ # Méthodes de classe
101
+ #
102
+ prepare_class_methods
103
+ #
104
+ # On doit s'assurer que la class propriétaire du manager est
105
+ # valide, c'est-à-dire définit et contient tous les éléments
106
+ # nécessaires.
107
+ #
108
+ owner_class_valid? || raise(ExitSilently)
109
+ #
110
+ # On doit s'assurer que la classe définit bien son système de
111
+ # sauvegarde et son lieu de sauvegarde
112
+ #
113
+ end
114
+
115
+ # @return true si la classe propriétaire est valide
116
+ def owner_class_valid?
117
+ classe.class_variable_defined?("@@save_system") || raise(ERRORS[:require_save_system] % classe.name)
118
+ classe.class_variable_defined?('@@save_location') || raise(ERRORS[:require_save_location] % classe.name)
119
+ classe.class_variable_defined?('@@save_format') || raise(ERRORS[:require_save_format] % classe.name)
120
+ [:card,:file,:conf].include?(save_system) || raise(ERRORS[:bad_save_system] % classe.name)
121
+ [:csv, :yaml].include?(save_format) || raise(ERRORS[:bas_save_format] % classe.name)
122
+
123
+ if save_system == :card && save_format == :csv
124
+ raise(ERRORS[:no_csv_format_with_card])
125
+ end
126
+ if File.exist?(save_location)
127
+ if save_system == :card
128
+ File.directory?(save_location) || raise(ERRORS[:require_save_location_folder] % classe.name)
129
+ else
130
+ File.directory?(save_location) && raise(ERRORS[:require_save_location_file] % classe.name)
131
+ end
132
+ end
133
+ #
134
+ # Si c'est un système d'enregistrement par fiche, on prépare
135
+ # déjà le fichier du dernier identifiant.
136
+ #
137
+ if save_system == :card
138
+ File.write(last_id_path,"0") unless File.exist?(last_id_path)
139
+ end
140
+ rescue Exception => e
141
+ puts "\n#{e.message}\n".rouge
142
+ return false
143
+ else
144
+ return true
145
+ end
146
+
147
+ def add(item)
148
+ item.data.merge!(id: __new_id) if item.data[:id].nil?
149
+ @items ||= []
150
+ @items << item
151
+ @table ||= {}
152
+ @table.merge!(item.id => item)
153
+ end
154
+
155
+ ##
156
+ # Permet de choisir une instance
157
+ #
158
+ # Par défaut, c'est la deuxième propriété qui est utilisée pour
159
+ # l'affichage (sa version "formatée" si elle existe) mais les
160
+ # options fournies peuvent définir une autre propriété avec l'at-
161
+ # tribut :name_property
162
+ #
163
+ # @param [Hash] options
164
+ # @option options [String] :question La question à poser ("Choisir" par défaut)
165
+ # @option options [Boolean] :multi Si true, on peut choisir plusieurs éléments
166
+ # @option options [Boolean] :create Si true, on peut créer un nouvel élément
167
+ # @option options [Boolean] :complete Si true, on ajoute un menu "Finir" qui retourne :complete
168
+ # @option options [Hash] :filter Filtre à appliquer aux valeurs à afficher
169
+ # Avec le filtre, les instances n'apparaitront pas à l'écran, contrairement à :exclude.
170
+ # @option options [Array] :exclude Liste d'identifiants qu'ils faut rendre "inchoisissables".
171
+ # @option options [Array] :default Quand :multi, les valeurs à sélectionner par défaut. C'est une liste d'identifiants.
172
+ # @option options [Symbol] :sort_key Clé (propriété) de classement de la liste
173
+ # @option options [String] :sort_dir Direction du classement, 'asc' ou 'desc' ('asc' par défaut, si :sort_key est défini)
174
+ # @option options [Integer] :per_page Nombre d'éléments affichés dans la fenêtre (par défaut, tous)
175
+ #
176
+ def choose(**options)
177
+ #
178
+ # Définition des options
179
+ #
180
+ options.key?(:multi) || options.merge!(multi: false)
181
+ options[:question] ||= "#{MSG[:choose]} : "
182
+ options[:question] = options[:question].jaune
183
+ load_data unless full_loaded?
184
+ @tty_name_procedure = nil
185
+ #
186
+ # Définition des menus
187
+ #
188
+ # Soit par précédences, soit par classement si options[:sort_key]
189
+ # est défini.
190
+ #
191
+ cs = get_choices_with_precedences(options)
192
+ #
193
+ # Interaction
194
+ #
195
+ if options[:multi]
196
+ #
197
+ # Menus sélectionnés par défaut
198
+ #
199
+ selecteds = nil
200
+ selecteds = get_default_choices(cs, options) if options[:default]
201
+ #
202
+ # L'utilisateur procède aux choix
203
+ #
204
+ choixs = Q.multi_select(options[:question], cs, {per_page: (options[:per_page] || cs.count), filter:true, default: selecteds, echo:false})
205
+ if choixs.include?(:create)
206
+ choixs.delete(:create)
207
+ choixs << classe.new.create
208
+ end
209
+ #
210
+ # Enregistrement de la précédence
211
+ #
212
+ choixs = choixs.map do |choix|
213
+ next if choix.nil?
214
+ if choix.instance_of?(GroupedItems)
215
+ choix = choose_in_list_of_grouped_items(choix, options)
216
+ choose_precedence_set("G#{choix.id}")
217
+ else
218
+ choose_precedence_set(choix.id)
219
+ end
220
+ choix
221
+ end.compact
222
+ #
223
+ # Instances retournées
224
+ #
225
+ choixs
226
+ else
227
+ #
228
+ # L'utilisateur procède au choix
229
+ #
230
+ choix = Q.select(options[:question], cs, {per_page: 20, filter:true, show_help:false})
231
+ choix = classe.new.create if choix == :create
232
+ choix || return # cancel
233
+ return :complete if choix == :complete
234
+ choose_precedence_set(choix.id)
235
+ #
236
+ # Si c'est une liste d'items groupés, il faut encore choisir
237
+ # dans cette liste l'item qui sera renvoyé. Sinon, on retourne
238
+ # l'item choisi.
239
+ #
240
+ if choix.instance_of?(GroupedItems)
241
+ choix = choose_in_list_of_grouped_items(choix, options)
242
+ end
243
+ choix
244
+ end
245
+ end
246
+
247
+ # @return [Any] Any instance chosen in +group+
248
+ # @param [GroupedItemss] group Instance with items grouped
249
+ #
250
+ def choose_in_list_of_grouped_items(group, options)
251
+ choices = group.items.map do |item|
252
+ {name: item.best_name, value: item}
253
+ end + [CHOIX_RENONCER]
254
+ Q.select(options[:question], choices, {per_page:choices.count, show_help:false, echo:false})
255
+ end
256
+
257
+ # Pour afficher des items les uns sur les autres, avec des
258
+ # informations réduites.
259
+ #
260
+ # @param options [Hash|Nil] Options
261
+ # @option options [Hash] :filter Filtre pour n'afficher que les items
262
+ # correspondant à :filter. :filter est une table de clés qui
263
+ # correspondent aux propriétés de l'item et de valeurs qui sont
264
+ # les valeurs attendues.
265
+ # @option options [Periode] :periode Période concernée par l'affichage.
266
+ # @option options [Symbol] :sort_key Clé (propriété) de classement de la liste
267
+ # @option options [String] :sort_dir Direction du classement, 'asc' ou 'desc' ('asc' par défaut, si :sort_key est défini)
268
+ #
269
+ def display_items(options = nil)
270
+ options ||= {}
271
+ full_loaded? || load_data
272
+ #
273
+ # Dans le cas d'absence d'items
274
+ #
275
+ @items.count > 0 || begin
276
+ puts MSG[:no_items_to_display].orange
277
+ return
278
+ end
279
+
280
+ #
281
+ # Filtrage de la liste (s'il le faut)
282
+ #
283
+ if not(options.key?(:filter)) && options[:periode]
284
+ options.merge!(filter: {})
285
+ end
286
+ options[:filter].merge!(periode: options[:periode]) if options[:periode]
287
+ disp_items = filter_items_of_list(@items, options)
288
+
289
+ #
290
+ # Classement des items si nécessaire
291
+ #
292
+ if options.key?(:sort_key) && options[:sort_key]
293
+ sort_key = options[:sort_key]
294
+ if disp_items.first.respond_to?(sort_key)
295
+ begin
296
+ disp_items = disp_items.sort do |a, b|
297
+ a.send(sort_key) <=> b.send(sort_key)
298
+ end
299
+ disp_items = disp_items.reverse if options[:sort_dir].to_s == 'asc'
300
+ rescue Exception => e
301
+ puts "Classement impossible (avec la clé de classement #{sort_key.inspect}) : #{e.message}".rouge
302
+ sleep 4
303
+ end
304
+ else
305
+ #
306
+ # Clé inconnue
307
+ #
308
+ puts "Pour classer par la clé #{sort_key.inspect} il faudrait que les items la reconnaissent.".rouge
309
+ end
310
+ end
311
+
312
+ #
313
+ # Procédure qui permet de récupérer la liste des données pour
314
+ # l'affichage tabulaire des éléments
315
+ #
316
+ header = []
317
+ tableizable_props = []
318
+ properties.each do |property|
319
+ if property.tablizable?
320
+ header << (property.short_name||property.name)
321
+ tableizable_props << property
322
+ end
323
+ end
324
+
325
+ tbl = Clir::Table.new(**{
326
+ title: "AFFICHAGE DES #{class_name}S",
327
+ header: header
328
+ })
329
+ disp_items.each do |item|
330
+ tbl << tableizable_props.map do |property|
331
+ property.formated_value_in(item) || '---'
332
+ end
333
+ end
334
+
335
+ clear unless debug?
336
+ tbl.display
337
+
338
+ end
339
+
340
+ ##
341
+ # @return la liste des indexes des menus sélectionnés dans +cs+
342
+ # Les sélectionnés sont définis par leur identifiant dans
343
+ # options[:default]
344
+ #
345
+ def get_default_choices(cs, options)
346
+ selecteds = options[:default]
347
+ ids_sels = []
348
+ cs.each_with_index do |dmenu, idx|
349
+ ids_sels << (idx + 1) if selecteds.include?(dmenu[:value])
350
+ end
351
+ return ids_sels
352
+ end
353
+
354
+ def choose_precedence_set(id)
355
+ precedence_ids.delete(id)
356
+ precedence_ids.unshift(id)
357
+ mkdir(tmp_folder)
358
+ File.write(precedence_list, precedence_ids.join(' '))
359
+ end
360
+
361
+ def precedence_ids
362
+ @precedence_ids ||= begin
363
+ if File.exist?(precedence_list)
364
+ File.read(precedence_list).split(' ').map(&:to_i)
365
+ else [] end
366
+ end
367
+ end
368
+
369
+ def precedence_list
370
+ @precedence_list ||= File.join(tmp_folder, "#{classe.name.to_s.gsub(/::/,'_').downcase}.precedences")
371
+ end
372
+
373
+ def tmp_folder
374
+ @tmp_folder ||= mkdir(File.join(APP_FOLDER,'tmp','precedences'))
375
+ end
376
+
377
+ ##
378
+ # @return [Array] Liste des "choices" pour le select de Tty-prompt
379
+ # pour choisir une instance de la classe.
380
+ #
381
+ # Cette liste tient compte de la variable @@group_by de la classe,
382
+ # qui détermine les regroupements de données à effectuer.
383
+ #
384
+ # La méthode retourne aussi une liste avec ITEMS CLASSÉS PAR
385
+ # PRÉCÉDENCES aux conditions suivantes :
386
+ # SI la liste de précédence existe.
387
+ # SI les options ne contiennent pas :sort_key, une clé de
388
+ # classement des items.
389
+ #
390
+ # Donc une liste :
391
+ # - items groupés par @@group_by
392
+ # - items classés par liste de précédence.
393
+ # @note
394
+ # La liste de précédence se fiche de savoir s'il s'agit d'un
395
+ # item ou d'un groupement d'items. Pour le moment, ça signifie
396
+ # que l'item enregistré dans la liste de précédences ne sera pas
397
+ # le bon.
398
+ #
399
+ #
400
+ # @param [Hash] options Cf. la méthode #choose qui se sert de
401
+ # cette méthode.
402
+ #
403
+ def get_choices_with_precedences(options)
404
+ #
405
+ # La liste au départ
406
+ #
407
+ list = @items
408
+
409
+ #
410
+ # Filtrer la liste si nécessaire
411
+ #
412
+ list = filter_items_of_list(list, options)
413
+
414
+ #
415
+ # Grouper les éléments si nécessaire
416
+ #
417
+ list = group_items_of_list(list, options)
418
+
419
+ #
420
+ # Quand on a la liste finale, on peut régler la précédence si
421
+ # elle est définie OU classer la liste si une clé de classement
422
+ # est définie (:sort_key et :sort_dir)
423
+ #
424
+ if options[:sort_key]
425
+ list.sort! do |a, b|
426
+ a.send(options[:sort_key]) <=> b.send(options[:sort_key])
427
+ end
428
+ list.reverse! if options[:sort_dir] == 'desc'
429
+ else
430
+ if File.exist?(precedence_list)
431
+ # puts "Classement par rapport à la liste : #{precedence_list.inspect}".jaune
432
+ # sleep 4
433
+ list.sort! do |a, b|
434
+ (precedence_ids.index(a.id)||10000) <=> (precedence_ids.index(b.id)||10000)
435
+ end
436
+ end
437
+ end
438
+
439
+ #
440
+ # On retourne des menus pour TTY-Prompt
441
+ #
442
+ cs = list.map do |item|
443
+ {name: tty_name_for(item, options), value: item}
444
+ end
445
+ cs.unshift(CHOIX_CREATE) if options[:create]
446
+ cs.push({name:MSG[:finir].bleu, value: :complete}) if options[:finir]||options[:complete]
447
+ cs.push(CHOIX_RENONCER)
448
+
449
+ return cs
450
+ end
451
+
452
+ # Filtre la liste +list+ avec le filtre +options[:filter]+ s'il
453
+ # existe.
454
+ # @return [Array] La liste des éléments filtrés (ou pas)
455
+ # @param [Hash] options Options de renvoi des items
456
+ # @option options [Hash] filter Filtre à appliquer à la liste des items à renvoyer
457
+ #
458
+ def filter_items_of_list(list, options = nil)
459
+ return list unless options && options[:filter]
460
+ #
461
+ # Duplication pour pouvoir le modifier
462
+ #
463
+ option_filter = options[:filter].dup
464
+ #
465
+ # Préparer éventuellement certaines valeurs du filtre
466
+ #
467
+ option_filter.each do |k, v|
468
+ case k
469
+ when :periode
470
+ #
471
+ # Si une période est déterminée, il faut ajouter cette condition
472
+ # au filtre.
473
+ #
474
+ # L'idée c'est de déterminer que le temps de l'item doit être
475
+ # supérieur ou égal au temps de départ de la période et doit
476
+ # être inférieur ou égal au temps de fin de la période.
477
+ # Le tout est de savoir quel temps prendre en compte. On
478
+ # cherche dans cet ordre
479
+ # :date, :created_at, :time
480
+ # Pour le savoir on prend le premier élément, qui existe
481
+ # forcément.
482
+ item1 = list.first
483
+ time_prop =
484
+ if item1.respond_to?(:date)
485
+ :date
486
+ elsif item1.respond_to?(:created_at)
487
+ :created_at
488
+ elsif item1.respond_to?(:time)
489
+ :time
490
+ elsif not(time_property)
491
+ time_property
492
+ else
493
+ raise ERRORS[:no_time_property] % ["#{classe.class}"]
494
+ end
495
+ # On prend la période en la retirant du filtre
496
+ periode = options[:filter].delete(:periode)
497
+ # Et on ajoute la condition sur le temps
498
+ options[:filter].merge!(
499
+ time_prop => Proc.new { |inst| periode.time_in?(inst.send(time_prop) ) }
500
+ )
501
+ end #/case k
502
+ end
503
+ #
504
+ # Sélectionner les items valides
505
+ #
506
+ list.select do |item|
507
+ item_match_filter?(item, options[:filter])
508
+ end
509
+ end
510
+
511
+ # Groupe les éléments dans la liste +list+ suivant la variable de
512
+ # classe @@group_by ou +options[:group_by]+
513
+ #
514
+ # @return [Array] La liste Tty-prompt avec les instances groupées
515
+ # @note
516
+ # Cf. le manuel pour le détail de l'utilisation.
517
+ #
518
+ def group_items_of_list(list, options = nil)
519
+ return list if options[:group_by].nil? && items_grouped_by.nil?
520
+ #
521
+ # La clé de groupement
522
+ #
523
+ groupby = options[:group_by] || items_grouped_by
524
+ #
525
+ # La clé de groupe fait-elle référence à une classe relative ?
526
+ #
527
+ is_relative_class = groupby.to_s.match?(/_ids?$/)
528
+ #
529
+ # Table des groupes initiés
530
+ #
531
+ groups = {}
532
+ #
533
+ # La liste finale qui contiendra les nouveaux éléments
534
+ #
535
+ final_list = []
536
+ #
537
+ # On boucle sur la liste en groupant
538
+ #
539
+ list.each do |item|
540
+ if (group_id = item.send(groupby))
541
+ #
542
+ # Si ce groupe n'existe pas, on le crée
543
+ #
544
+ unless groups.key?(group_id)
545
+ #
546
+ # Le nom que prendra le groupe
547
+ #
548
+ property = table_properties[groupby]
549
+ nom =
550
+ if is_relative_class
551
+ property.relative_class.get(group_id).name
552
+ else
553
+ property.name
554
+ end
555
+ group = GroupedItems.new(nom, group_id, [])
556
+ groups.merge!(group_id => group)
557
+ final_list << group
558
+ end
559
+ groups[group_id].items << item
560
+ else
561
+ #
562
+ # Si l'item ne répond pas à la propriété de classement, on
563
+ # le met tel quel
564
+ #
565
+ final_list << item
566
+ end
567
+ end
568
+ #
569
+ # On retourne la liste finale
570
+ #
571
+ return final_list
572
+ end
573
+
574
+ # @return [Boolean] True si l'instance +item+ correspond au filtre
575
+ # +filter+
576
+ # @param [Any] item Instance de classe quelconque (mais qui doit
577
+ # répondre à toutes les clés du filtre)
578
+ # @param [Hash] filter Définition du filtre, avec en clé des
579
+ # méthode de l'item et en valeur les valeurs
580
+ # attendues (comparées avec '!=').
581
+ def item_match_filter?(item, filter)
582
+ filter.each do |key, expected|
583
+ case expected
584
+ when Proc
585
+ return false if not(expected.call(item))
586
+ else
587
+ return false if item.send(key) != expected
588
+ end
589
+ end
590
+ return true
591
+ end
592
+
593
+ # @return le string à utiliser pour l'attribut :name de TTY prompt
594
+ def tty_name_for(item, options)
595
+ @tty_name_procedure ||= begin
596
+ options ||= {}
597
+ if options.key?(:name4tty) && options[:name4tty]
598
+ #
599
+ # Procédure à utiliser définie dans les options
600
+ #
601
+ case v = options[:name4tty]
602
+ when Symbol then Proc.new { |inst| inst.send(options[:name4tty]) }
603
+ when Proc then Proc.new { |inst| options[:name4tty].call(inst) }
604
+ end
605
+ elsif item.respond_to?(:name4tty)
606
+ #
607
+ # :name4tty Définie comme méthode d'intance
608
+ #
609
+ case v = item.send(:name4tty)
610
+ when Symbol then Proc.new { |inst| inst.send(item.send(:name4tty)) }
611
+ when String then Proc.new { |inst| inst.send(:name4tty) }
612
+ end
613
+ elsif item.respond_to?(:best_name)
614
+ Proc.new do |inst|
615
+ begin
616
+ if inst.is_a?(Clir::GroupedItems)
617
+ inst.name
618
+ else
619
+ inst.best_name
620
+ end
621
+ rescue Exception => e
622
+ puts "-> #{e.message}".rouge
623
+ puts "Problème avec l'instance #{inst.inspect} qui ne répond pas à :best_name…".rouge
624
+ puts "POURTANT, #{item.inspect} répondait à :best_name…".rouge
625
+ inst.send(data_properties[1][:prop])
626
+ end
627
+ end
628
+ else
629
+ #
630
+ # Aucune définition => deuxième propriété
631
+ #
632
+ prop = data_properties[1][:prop]
633
+ Proc.new { |inst| inst.send(prop) }
634
+ end
635
+ end
636
+ @tty_name_procedure.call(item)
637
+ end
638
+
639
+ ##
640
+ # Méthode principale du manager, quand le :save_system est :file,
641
+ # qui enregistre toutes les données
642
+ #
643
+ def save_all
644
+ case save_format
645
+ when :yaml
646
+ all_data = @items.map(&:data)
647
+ File.write(save_location, all_data.to_yaml)
648
+ when :csv
649
+ CSV.open(save_location, 'wb') do |csv|
650
+ @items.each do |item|
651
+ csv << item.data
652
+ end
653
+ end
654
+ end
655
+ end
656
+
657
+ def save_system
658
+ classe.class_variable_get("@@save_system")
659
+ end
660
+ def save_location
661
+ classe.class_variable_get("@@save_location")
662
+ end
663
+ def save_format
664
+ classe.class_variable_get("@@save_format")
665
+ end
666
+ def items_grouped_by
667
+ @items_grouped_by ||= begin
668
+ if classe.class_variables.include?(:'@@group_by')
669
+ classe.class_variable_get('@@group_by')
670
+ end
671
+ end
672
+ end
673
+
674
+ #
675
+ # Pour savoir si toutes les données sont chargées
676
+ #
677
+ def full_loaded?
678
+ @is_full_loaded === true
679
+ end
680
+
681
+
682
+ # --- Usefull Method for classes ---
683
+
684
+ # Reçoit quelque chose comme 'edic_test_class' et retourne
685
+ # Edic::TestClass en mémorisant pour accélérer le processus
686
+ #
687
+ def get_classe_from(class_min)
688
+ return self.class.get_class_from_class_mmin(class_min)
689
+ end
690
+ def self.get_class_from_class_mmin(class_min)
691
+ @@class4classMin ||= {}
692
+ @@class4classMin[class_min] ||= begin
693
+ dclass = class_min.split('_').map{|n|n.titleize}
694
+ cc = Object # la classe courante en tant que classe
695
+ ss = nil # le string courant en tan que classe en recherche
696
+ while dclass.count > 0
697
+ x = dclass.shift
698
+ # puts "Étude de dclass.shift = #{x.inspect}"
699
+ if cc.const_defined?(x)
700
+ cc = cc.const_get(x) # => class
701
+ x = nil
702
+ elsif ss.nil?
703
+ ss = x
704
+ elsif ss != nil
705
+ if cc.const_defined?(ss + x)
706
+ cc = cc.const_get(ss + x)
707
+ ss = nil
708
+ else
709
+ ss = ss + x # => "Data" + "Manager" => "DataManager"
710
+ # Et on poursuit
711
+ end
712
+ end
713
+ end
714
+ cc = nil if cc == Object
715
+ if cc.nil?
716
+ raise ERRORS[:unable_to_get_class_from_class_min] % [class_min, "."]
717
+ elsif not(x.nil?) || not(ss.nil?)
718
+ raise ERRORS[:unable_to_get_class_from_class_min] % [class_min, " : #{MSG[:not_treated] % "#{x}#{ss}".inspect}."]
719
+ end
720
+ cc
721
+ end
722
+ end
723
+
724
+ # Le nom simple de la classe propriétaire, sans module
725
+ def class_name
726
+ @class_name ||= classe.name.to_s.split('::').last
727
+ end
728
+
729
+
730
+ ################# MANAGED CLASS METHODS #################
731
+
732
+
733
+ def prepare_class_methods
734
+ my = self
735
+ classe.define_singleton_method 'data_manager' do
736
+ return my
737
+ end
738
+ classe.define_singleton_method 'save_location' do
739
+ return my.save_location
740
+ end
741
+ classe.define_singleton_method 'items' do |options = nil|
742
+ my.load_data if not(my.full_loaded?)
743
+ if options.nil?
744
+ my.items
745
+ else
746
+ get(options)
747
+ end
748
+ end
749
+ classe.define_singleton_method 'table' do
750
+ my.full_loaded? || my.load_data
751
+ my.instance_variable_get("@table")
752
+ end
753
+ classe.define_singleton_method 'get' do |item_id|
754
+ data_manager.get(item_id)
755
+ end
756
+ classe.define_singleton_method 'last_id' do
757
+ return my.__last_id
758
+ end
759
+ classe.define_singleton_method 'class_name' do
760
+ my.class_name
761
+ end
762
+ classe.define_singleton_method 'count' do
763
+ get_all.count
764
+ end
765
+ classe.define_singleton_method 'get_all' do |options = nil|
766
+ my.load_data if not(my.full_loaded?)
767
+ my.filter_items_of_list(my.items, options || {})
768
+ end
769
+ classe.define_singleton_method 'select' do |filter, options = nil|
770
+ options ||= {}
771
+ options.merge!({filter: filter})
772
+ get_all(options)
773
+ end
774
+ classe.class_eval do
775
+ class << self
776
+ alias :filter :select
777
+ end
778
+ end
779
+ # classe.define_singleton_method 'filter' do |options = nil| # alias
780
+ # return get_all(options)
781
+ # end
782
+ classe.define_singleton_method 'display' do |options = nil|
783
+ my.display_items(options)
784
+ end
785
+ classe.define_singleton_method 'remove' do |instances, options = nil|
786
+ my.remove(instances, options)
787
+ end
788
+ if classe.methods.include?(:choose)
789
+ # Rien à faire
790
+ else
791
+ classe.define_singleton_method 'choose' do |options = nil|
792
+ return my.choose(options)
793
+ end
794
+ end
795
+ unless classe.respond_to?(:feminine?)
796
+ classe.define_singleton_method 'feminine?' do
797
+ return false
798
+ end
799
+ end
800
+ end
801
+
802
+ # @return [Any] Any instance with ID +item_id+
803
+ def get(item_id)
804
+ item_id = item_id.to_i
805
+ @table ||= {}
806
+ @table[item_id] || begin
807
+ # La table, même si elle a déjà été chargée, peut peut-être
808
+ # avoir besoin d'être rechargée
809
+ load_data
810
+ end
811
+ @table[item_id]
812
+ end
813
+
814
+ ################# MANAGED ITEM METHODS #################
815
+
816
+ # Add instance methods to managed class (:create, :edit, :display
817
+ # and :remove/:destroy)
818
+ def prepare_instance_methods_of_class
819
+ my = self
820
+ classe.define_method 'initialize' do |data = {}|
821
+ @data = data
822
+ end
823
+ classe.define_method 'create' do |options = {}|
824
+ return my.create(self, options)
825
+ end
826
+ classe.define_method 'edit' do |options = {}|
827
+ return my.edit(self, options)
828
+ end
829
+ classe.define_method 'display' do |options = {}|
830
+ my.display(self, options)
831
+ end
832
+ classe.alias_method(:show, :display)
833
+ classe.define_method 'remove' do |options = {}|
834
+ my.remove(self, options)
835
+ end
836
+ classe.alias_method(:destroy, :remove)
837
+ classe.define_method 'data' do
838
+ return @data
839
+ end
840
+ classe.define_method 'data=' do |value|
841
+ @data = value
842
+ end
843
+
844
+ # @return [String] The best name for the object
845
+ # @note
846
+ # Managed class can define its own best_name method
847
+ unless classe.instance_methods(false).include?(:best_name)
848
+ classe.define_method 'best_name' do
849
+ @best_name ||= begin
850
+ bn = nil
851
+ [:designation, :reference, :ref, :pretty_inspect,
852
+ :titre, :full_name, :fullname
853
+ ].each do |meth|
854
+ bn = self.send(meth) and break if respond_to?(meth)
855
+ end
856
+ if bn.nil?
857
+ if classe.instance_methods(false).include?(:inspect)
858
+ self.inspect
859
+ else
860
+ "#{name} (##{id})"
861
+ end
862
+ else
863
+ bn
864
+ end
865
+ end
866
+ end
867
+ end
868
+
869
+ #
870
+ # Quelques propriétés supplémentaires pour les instances
871
+ #
872
+ classe.define_method "new?" do
873
+ return @data[:is_new] === true
874
+ end
875
+
876
+ prepare_save_methods
877
+
878
+ prepare_properties_methods
879
+
880
+ end
881
+
882
+ def prepare_save_methods
883
+ my = self
884
+ #
885
+ # Méthode de sauvegarde, en fonction du système de sauvegarde
886
+ # choisi.
887
+ #
888
+ case save_system
889
+ when :card
890
+ case save_format
891
+ when :yaml
892
+ classe.define_method "data_file" do
893
+ @data_file ||= begin
894
+ File.join(mkdir(my.save_location),"#{id}.yaml")
895
+ end
896
+ end
897
+ classe.define_method "save" do
898
+ if new?
899
+ my.add(self)
900
+ @data.delete(:is_new)
901
+ end
902
+ File.write(data_file, data.to_yaml)
903
+ end
904
+ end
905
+ when :file
906
+ case save_format
907
+ when :yaml
908
+ classe.define_method "save" do
909
+ load_data unless my.full_loaded?
910
+ if new?
911
+ my.add(self)
912
+ @data.delete(:is_new)
913
+ end
914
+ my.save_all
915
+ end
916
+ when :csv
917
+ classe.define_method "save" do
918
+ load_data unless my.full_loaded?
919
+ if new?
920
+ my.add(self)
921
+ @data.delete(:is_new)
922
+ end
923
+ my.save_all
924
+ end
925
+ end
926
+ when :conf
927
+ raise "Je ne sais pas encore utiliser le système :conf de sauvegarde."
928
+ end
929
+
930
+ end
931
+
932
+ def prepare_properties_methods
933
+ #
934
+ # Chaque propriété de DATA_PROPERTIES doit faire une méthode qui
935
+ # permettra de récupérer et de définir la valeur
936
+ #
937
+ data_properties.each do |dproperty|
938
+ prop = dproperty[:prop]
939
+ classe.define_method "#{prop}" do
940
+ return @data[prop]
941
+ end
942
+ classe.define_method "#{prop}=" do |value|
943
+ @data.merge!( prop => value)
944
+ end
945
+ #
946
+ # Propriétés spéciales qui se terminent par _id et sont des
947
+ # liens avec une autre classe (typiquement : user_id pour faire
948
+ # référence à un user {User})
949
+ #
950
+ if prop.to_s.match?(/_ids?$/)
951
+ traite_property_as_other_class_instance(dproperty)
952
+ end
953
+ end
954
+
955
+ end
956
+
957
+ def traite_property_as_other_class_instance(dproperty)
958
+ prop = dproperty[:prop]
959
+ last = prop.end_with?('_ids') ? -5 : -4
960
+ class_min = prop[0..last]
961
+ other_class = get_classe_from(class_min)
962
+ # other_class.respond_to?(:choose) || begin
963
+ # raise "Impossible d'obtenir la classe relative #{class_min.inspect}. La classe calculée est #{other_class.name} qui ne répond pas à la méthode de classe :choose."
964
+ # end
965
+ # puts "other_classe avec #{class_min.inspect} : #{other_class}"
966
+ # sleep 4
967
+ dproperty.merge!(relative_class: other_class)
968
+ #
969
+ # Les méthodes utiles pour la gestion de l'autre classe.
970
+ # Note : une méthode différente suivant _id ou _ids
971
+ #
972
+ case true
973
+ when prop.end_with?('_ids')
974
+ classe.define_method "#{class_min}" do # p.e. def vente;
975
+ instance_variable_get("@#{class_min}") || begin
976
+ items = self.send(prop).map do |item_id|
977
+ other_class.get(item_id)
978
+ end
979
+ instance_variable_set("@#{class_min}", items)
980
+ end
981
+ end
982
+ when prop.end_with?('_id')
983
+ #
984
+ # @note
985
+ # On ne va mettre la propriété @class_min de l'instance en
986
+ # variable d'instance seulement si elle est définie. Dans le
987
+ # cas contraire, une fois qu'elle est évaluée à nil, elle
988
+ # resterait nil pour toujours…
989
+ #
990
+ classe.define_method "#{class_min}" do # p.e. def user; ... end
991
+ if instance_variables.include?("@#{class_min}".to_sym)
992
+ instance_variable_get("@#{class_min}") # forcément non nil
993
+ else
994
+ inst = other_class.get(self.send(prop))
995
+ instance_variable_set("@#{class_min}", inst) unless inst.nil?
996
+ return inst
997
+ end
998
+ end
999
+ classe.define_method "#{class_min}=" do |owner| # p.e. user=
1000
+ self.send("#{prop}=".to_sym, owner.id)
1001
+ end
1002
+ end
1003
+ end
1004
+
1005
+ # To create a instance
1006
+ def create(instance, options = nil)
1007
+ data = instance.before_create if instance.respond_to?(:before_create)
1008
+ instance.data = data || {id: __new_id}
1009
+ instance.data.merge!(is_new: true)
1010
+ edit(instance, options)
1011
+ if not(instance.new?) # => bien créé
1012
+ key = classe.feminine? ? :item_created_fem : :item_created
1013
+ puts (MSG[key] % {element: class_name}).vert
1014
+ instance.after_create if instance.respond_to?(:after_create)
1015
+ end
1016
+ return instance # chainage
1017
+ end
1018
+
1019
+ def edit(instance, options = nil)
1020
+ @editor ||= Editor.new(self)
1021
+ is_new_item = instance.data[:is_new]
1022
+ instance.before_edit if instance.respond_to?(:before_edit)
1023
+ @editor.edit(instance, options)
1024
+ instance.after_edit if instance.respond_to?(:after_edit)
1025
+ unless is_new_item
1026
+ key = classe.feminine? ? :item_updated_fem : :item_updated
1027
+ puts (MSG[key] % [class_name, instance.id]).vert
1028
+ end
1029
+ return instance # chainage
1030
+ end
1031
+
1032
+ def display(instance, options = nil)
1033
+ @displayer ||= Displayer.new(self)
1034
+ options = instance.before_display(options) if instance.respond_to?(:before_display)
1035
+ @displayer.show(instance, options)
1036
+ instance.after_display if instance.respond_to?(:after_display)
1037
+ return instance # chainage
1038
+ end
1039
+
1040
+ # @note
1041
+ # Cette méthode est testée dans remove_test.rb
1042
+ #
1043
+ def remove(instances, options = nil)
1044
+ has_method_before = instances.first.respond_to?(:before_remove)
1045
+ has_method_after = instances.first.respond_to?(:after_remove)
1046
+ #
1047
+ # Tout charger si ça n'est pas encore fait
1048
+ full_loaded? || load_data
1049
+ #
1050
+ # Pour conserver les IDs supprimés et les supprimer plus
1051
+ # rapidement de @items
1052
+ #
1053
+ table_removed_ids = {}
1054
+ #
1055
+ # Boucle sur toutes les instances à détruire
1056
+ #
1057
+ instances.each do |instance|
1058
+ # Méthode avant ?
1059
+ instance.send(:before_remove) if has_method_before
1060
+ #
1061
+ # Si les instancences sont sauvées dans des cartes, il faut les
1062
+ # détruire
1063
+ #
1064
+ if save_system == :card
1065
+ File.delete(instance.data_file) if File.exist?(instance.data_file)
1066
+ end
1067
+ #
1068
+ # Pour pouvoir retirer l'instance de @items
1069
+ #
1070
+ table_removed_ids.merge!(instance.id => true)
1071
+ #
1072
+ # Retirer l'instance de @table
1073
+ #
1074
+ @table.delete(instance.id)
1075
+ #
1076
+ # Faut-il appeler une méthode après la destruction ?
1077
+ #
1078
+ instance.send(:after_remove) if has_method_after
1079
+ end #/fin boucle sur les instances à détruire
1080
+ #
1081
+ # On retire ces items de @items
1082
+ #
1083
+ @items.reject! { |item| table_removed_ids[item.id] }
1084
+ #
1085
+ # Si les données sont enregistrées dans un fichier, on les
1086
+ # sauve maintenant
1087
+ #
1088
+ save_all if save_system == :file
1089
+
1090
+ return true
1091
+ end
1092
+
1093
+ # Loop on every property (as instances)
1094
+ def each_property(&block)
1095
+ if block_given?
1096
+ properties.each do |property|
1097
+ yield property
1098
+ end
1099
+ end
1100
+ end
1101
+
1102
+ # @return [Array<Property>] All data properties as instance
1103
+ # @note
1104
+ # Also product @table_properties, a table with key = :prop and
1105
+ # value is instance DataManager::Property
1106
+ #
1107
+ def properties
1108
+ @properties ||= begin
1109
+ data_properties.map.with_index do |dproperty, idx|
1110
+ dproperty.merge!(index: idx)
1111
+ Property.new(self, dproperty)
1112
+ end
1113
+ end
1114
+ end
1115
+
1116
+ # @return [Hash] Table of properties. Key is property@prop, value
1117
+ # is DataManager::Property instance.
1118
+ def table_properties
1119
+ @table_properties ||= begin
1120
+ tbl = {}; properties.each do |property|
1121
+ tbl.merge!(property.prop => property)
1122
+ end; tbl
1123
+ end
1124
+ end
1125
+
1126
+ # @prop Pour valider les nouvelles données
1127
+ def validator
1128
+ @validator ||= Validator.new(self)
1129
+ end
1130
+
1131
+
1132
+ # --- Data Methods ---
1133
+
1134
+ def load_data
1135
+ @table = {}
1136
+ @items = []
1137
+ @last_id = 0
1138
+ case save_system
1139
+ when :card
1140
+ load_data_from_cards
1141
+ when :file
1142
+ load_data_from_uniq_file
1143
+ end.each do |ditem|
1144
+ inst = classe.new(ditem)
1145
+ @table.merge!(inst.id => inst)
1146
+ @items << inst
1147
+ @last_id = 0 + inst.id if inst.id > @last_id
1148
+ end
1149
+ @is_full_loaded = true
1150
+ end
1151
+
1152
+ def load_data_from_uniq_file
1153
+ if File.exist?(save_location)
1154
+ case save_format
1155
+ when :yaml
1156
+ YAML.load_file(save_location, {aliases:true, symbolize_names: true})
1157
+ when :csv
1158
+ CSV.read(save_location)
1159
+ end
1160
+ else
1161
+ []
1162
+ end
1163
+ end
1164
+ def load_data_from_cards
1165
+ Dir["#{save_location}/*.#{save_format}"].map do |pth|
1166
+ case save_format
1167
+ when :yaml
1168
+ YAML.load_file(pth, **{aliases:true, symbolize_names: true})
1169
+ else
1170
+ raise "Format de fiche inconnue : #{save_format}"
1171
+ end
1172
+ end
1173
+ end
1174
+
1175
+ # --- Path Methods ---
1176
+
1177
+ def last_id_path
1178
+ @last_id_path ||= begin
1179
+ File.join(mkdir(save_location),"LASTID")
1180
+ end
1181
+ end
1182
+ end #/class Manager
1183
+ end #/module DataManager
1184
+ end #/module Clir