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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +44 -0
- data/Manual/Manuel_fr.md +784 -0
- data/Manual/Manuel_fr.pdf +0 -0
- data/README.md +279 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/clir-data_manager.gemspec +34 -0
- data/lib/clir/data_manager/Displayer.rb +30 -0
- data/lib/clir/data_manager/Editor.rb +208 -0
- data/lib/clir/data_manager/Manager.rb +1184 -0
- data/lib/clir/data_manager/Periode.rb +366 -0
- data/lib/clir/data_manager/PrecedencedList.rb +98 -0
- data/lib/clir/data_manager/Property.rb +438 -0
- data/lib/clir/data_manager/Validator.rb +157 -0
- data/lib/clir/data_manager/constants.rb +14 -0
- data/lib/clir/data_manager/errors_and_messages.rb +123 -0
- data/lib/clir/data_manager/module_constants.rb +13 -0
- data/lib/clir/data_manager/version.rb +5 -0
- data/lib/clir/data_manager.rb +21 -0
- metadata +114 -0
@@ -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
|