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,438 @@
1
+ module Clir
2
+ module DataManager
3
+ class Property
4
+ include ClirDataManagerConstants
5
+
6
+ attr_reader :manager
7
+ attr_reader :data
8
+
9
+ # @param manager {Clir::DataManager::Manager} The manager
10
+ # @param data {Hash} Property data table
11
+ def initialize(manager, data = nil)
12
+ @manager = manager
13
+ @data = data || {}
14
+ end
15
+
16
+ # --- Edition Methods ---
17
+
18
+ ##
19
+ # Méthode principale de l'édition de la propriété pour l'instance
20
+ # +instance+ avec les options éventuelles +options+
21
+ #
22
+ # @return TRUE si la donnée a été modifiée, FALSE dans le cas
23
+ # contraire.
24
+ #
25
+ def edit(instance, options = nil)
26
+ #
27
+ # La valeur par défaut
28
+ # Soit la valeur actuelle de l'instance, soit la valeur définie
29
+ # par :default dans les propriétés, qui peut être soit une procé-
30
+ # dure soit une méthode de classe ou d'instance.
31
+ #
32
+ defvalue = instance.send(prop) || default(instance)
33
+ #
34
+ # On utilise une édition différente en fonction du type de la
35
+ # donnée
36
+ #
37
+ error = nil
38
+ question = question(instance).jaune
39
+ while true
40
+ puts error.rouge if error
41
+ new_value =
42
+ case type
43
+ when :id
44
+ # [N0001]
45
+ # Cas spécial d'une propriété <>_id. Si tout a bien été
46
+ # défini, DataManager a mis dans l'attribut relative_class
47
+ # la classe de l'élément.
48
+ if relative_class
49
+ item = relative_class.choose({create: true, filter: values_filter})
50
+ item&.id
51
+ else
52
+ raise ERRORS[:require_relative_class] % [prop.to_s, relative_class.to_s]
53
+ end
54
+ when :ids
55
+ # Cf. [N0001] ci-dessus
56
+ if relative_class
57
+ multi_choose(instance, options)
58
+ else
59
+ raise ERRORS[:require_relative_class] % [prop.to_s, relative_class.to_s]
60
+ end
61
+ when :date
62
+ defvalue ||= Time.now.strftime(MSG[:date_format])
63
+ Q.ask(question, {default: defvalue})&.strip
64
+ when :string, :email, :prix, :url, :people, :number, :float
65
+ nval = Q.ask(question, {help:"'---' = nul", default: defvalue})
66
+ nval = nil if nval == '---'
67
+ unless nval.nil?
68
+ nval = nval.to_s.force_encoding('UTF-8').strip
69
+ case type
70
+ when :number, :float
71
+ if nval.sub(/,/,'.').match?(/\./)
72
+ nval = nval.to_f
73
+ else
74
+ nval = nval.to_i
75
+ end
76
+ when :prix
77
+ nval = nval.to_f
78
+ when :url
79
+ nval = "https://#{nval}" unless nval.start_with?('http')
80
+ end
81
+ end
82
+ nval
83
+ when :select
84
+ #
85
+ # Type :select
86
+ #
87
+ choices = select_values_with_precedences(instance)
88
+ if multi?
89
+ vals = Q.multi_select(question, choices, {default:default_select_value(instance, choices), per_page:choices.count})
90
+ vals.each { |val| values(instance).set_last(val) }
91
+ vals
92
+ else
93
+ value = Q.select(question, choices, {default:default_select_value(instance, choices), per_page:choices.count, show_help:false})
94
+ values(instance).set_last(value)
95
+ value
96
+ end
97
+ when :bool
98
+ Q.select(question, BOOLEAN_VALUES, {default: boolean_default_value(instance), per_page:BOOLEAN_VALUES.count, show_help:false})
99
+ else
100
+ puts "Je ne sais pas encore éditer une donnée de type #{type.inspect}.".orange
101
+ sleep 3
102
+ break
103
+ end
104
+ #
105
+ # Si la propriété définit une méthode de transformation de
106
+ # l'entrée, on l'utilise
107
+ if new_value && itransform
108
+ new_value = transform_new_value(instance, new_value)
109
+ end
110
+ #
111
+ # On vérifie la validité de la donnée, si une méthode de
112
+ # validation a été définie. Si la donnée est valide, on la
113
+ # consigne, sinon non demande à la modifier.
114
+ #
115
+ error = valid?(new_value, instance)
116
+ break if error.nil?
117
+
118
+ end #/while invalid
119
+ #
120
+ # La donnée a-t-elle changée ?
121
+ #
122
+ modified = new_value != current_value(instance)
123
+ #
124
+ # S'il y a eu modification, on affecte la nouvelle valeur
125
+ #
126
+ instance.send("#{prop}=".to_sym, new_value) if modified
127
+ #
128
+ # On indique si la donnée a été modifiée
129
+ #
130
+ return modified
131
+ end
132
+
133
+ # --- Méthodes de check ---
134
+
135
+ # @return Nil si OK ou le message d'erreur à afficher
136
+ def valid?(new_value, instance)
137
+ return if new_value && (new_value == current_value(instance))
138
+ return manager.validator.valid?(self, new_value, instance)
139
+ end
140
+
141
+ # --- Helpers Methods ---
142
+
143
+ def formated_value_in(instance)
144
+ curval = instance.send(prop)
145
+ if curval.nil?
146
+ #
147
+ # Value non définie
148
+ #
149
+ return '---'
150
+ elsif format_method # :mformat dans la définition de la propriété
151
+ #
152
+ # Si la propriété définit une valeur de formatage explicitement
153
+ #
154
+ if format_method.is_a?(Proc)
155
+ format_method.call(current_value(instance), instance)
156
+ else
157
+ instance.send(format_method)
158
+ end
159
+ elsif instance.respond_to?("f_#{prop}".to_sym)
160
+ #
161
+ # Si l'instance définit la méthode de formatage
162
+ #
163
+ instance.send("f_#{prop}".to_sym)
164
+ elsif prop == :name && instance.respond_to?(:best_name)
165
+ instance.best_name
166
+ elsif prop.match?(/_ids?$/) && [:id, :ids].include?(type)
167
+ #
168
+ # Propriété avec classe relative
169
+ #
170
+ if relative_class
171
+ dmanager = relative_class.data_manager
172
+ if type == :id
173
+ # inst = relative_class.get(current_value(instance))
174
+ inst = relative_class.get(curval)
175
+ return dmanager.tty_name_for(inst, nil)
176
+ elsif type == :ids
177
+ return curval.map do |id|
178
+ inst = relative_class.get(id)
179
+ dmanager.tty_name_for(inst, nil)
180
+ end.join(', ')
181
+ end
182
+ else
183
+ raise ERRORS[:require_relative_class]
184
+ end
185
+ elsif type == :select && data[:values]
186
+ #
187
+ # Propriété avec des values (on renvoie :full_name ou :name
188
+ # du choix correspondant)
189
+ #
190
+ values_for_instance = values(instance)
191
+ values_for_instance.each do |dchoix|
192
+ if dchoix[:value] == curval
193
+ return dchoix[:full_name]||dchoix[:name]
194
+ end
195
+ end
196
+ raise ERRORS[:choice_unfound_in_choices_list] % [curval.inspect, self.name(instance), values_for_instance.inspect]
197
+ else
198
+ #
199
+ # En dernier recours, la valeur telle quelle
200
+ #
201
+ curval
202
+ end
203
+ end
204
+
205
+ # --- Functional Methods ---
206
+
207
+ ##
208
+ # Permet de choisir des valeurs multiples, pour le moment plusieurs
209
+ # identifiants d'une classe relative.
210
+ #
211
+ # La méthode doit être appelée après avoir vérifié que la
212
+ # relative_class existait bien.
213
+ #
214
+ def multi_choose(instance, options)
215
+ curvalue = current_value(instance) || []
216
+ #
217
+ # La valeur propre du filtre
218
+ filter_for_instance = nil
219
+ if values_filter
220
+ filter_for_instance = {}
221
+ values_filter.each do |key, value|
222
+ if value.is_a?(Symbol) # => propriété de l'instance
223
+ value = instance.send(value)
224
+ end
225
+ filter_for_instance.merge!(key => value)
226
+ end
227
+ end
228
+ #
229
+ # On demande toutes les instances choisies
230
+ #
231
+ insts = relative_class.choose({multi:true, create: false, filter: filter_for_instance, default: curvalue})
232
+ curvalue = insts.map(&:id)
233
+ # puts "curvalue : #{curvalue.inspect}"
234
+ #
235
+ # Valeur finale à retourner
236
+ #
237
+ curvalue = nil if curvalue.empty?
238
+ return curvalue
239
+ end
240
+
241
+ ##
242
+ # Si la propriété définit :itransform (méthode de transformation
243
+ # de la donnée entrée), cette méthode est appelée pour transformer
244
+ # la donnée.
245
+ def transform_new_value(instance, new_value)
246
+ case itransform
247
+ when Symbol
248
+ if instance.respond_to?(itransform)
249
+ instance.send(itransform, new_value)
250
+ elsif new_value.respond_to?(itransform)
251
+ new_value.send(itransform)
252
+ else
253
+ raise ERRORS[:value_doesnt_respond_to] % [new_value.inspect, "#{new_value.class}", itransform.inspect]
254
+ end
255
+ when Proc
256
+ itransform.call(instance, new_value)
257
+ end
258
+ end
259
+
260
+ def select_values_with_precedences(instance)
261
+ return values(instance).to_prec
262
+ end
263
+
264
+ # @return l'index de la valeur actuelle de l'instance pour la
265
+ # propriété courante, lorsque c'est un select (tty-prompt, en
266
+ # valeur par défaut, ne supporte que l'index, ou le :name du menu)
267
+ # Si la valeur n'est pas définie ou si elle est introuvable, on
268
+ # retourne nil
269
+ def default_select_value(instance, vals)
270
+ cvalue = current_value(instance) || default(instance) || return
271
+ vals.each_with_index do |dchoix, idx|
272
+ return idx + 1 if dchoix[:value] == cvalue
273
+ end
274
+ return nil
275
+ end
276
+
277
+ # @prop La valeur actuelle de cette propriété
278
+ def current_value(instance)
279
+ instance.send(prop)
280
+ end
281
+
282
+ # @prop La question à poser pour cette propriété
283
+ def question(instance)
284
+ if quest
285
+ quest % instance.data
286
+ else
287
+ "Nouvelle valeur pour #{name.inspect} : "
288
+ end
289
+ end
290
+
291
+ # --- Predicate Methods ---
292
+
293
+ def required?(instance)
294
+ if_able?(instance) || return
295
+ :TRUE == @isrequired ||= true_or_false(specs & REQUIRED > 0)
296
+ end
297
+
298
+ def displayable?(instance)
299
+ if_able?(instance) || return
300
+ :TRUE == @isdisplayable ||= true_or_false(specs & DISPLAYABLE > 0)
301
+ end
302
+
303
+ def editable?(instance)
304
+ if_able?(instance) || return
305
+ :TRUE == @iseditable ||= true_or_false(specs & EDITABLE > 0)
306
+ end
307
+
308
+ def removable?(instance)
309
+ if_able?(instance) || return
310
+ :TRUE == @isremovable ||= true_or_false(specs & REMOVABLE > 0)
311
+ end
312
+
313
+ # @return [true, false] true si la propriété doit être affichée
314
+ # dans une table Clir::Table
315
+ # @note
316
+ # Contrairement aux autres méthodes predicate de même type (cf
317
+ # ci-dessus), on ne teste pas le if_able car cette valeur doit
318
+ # être utilisée dans la table même pour les items qui ne la défi-
319
+ # nissent pas (sinon il y aurait des "trous" dans la table)
320
+ #
321
+ def tablizable?
322
+ specs || raise(ERRORS[:specs_undefined] % prop)
323
+ :TRUE == @isremovable ||= true_or_false(specs & TABLEIZABLE > 0)
324
+ end
325
+
326
+ # @return [Boolean] true si property can have multi_select values.
327
+ def multi?
328
+ :TRUE == @ismultiselect ||= true_or_false(data[:multi] == true)
329
+ end
330
+
331
+ # @return TRUE si la propriété :if n'est pas définie ou si elle
332
+ # retourne la valeur true (donc elle retourne true quand la
333
+ # propriété existe pour l'instance donnée)
334
+ def if_able?(instance)
335
+ specs || raise(ERRORS[:specs_undefined] % prop)
336
+ return true if if_attr.nil?
337
+ case if_attr
338
+ when Symbol
339
+ instance.send(if_attr)
340
+ when Proc
341
+ if_attr.call(instance)
342
+ when TrueClass, FalseClass
343
+ if_attr
344
+ else
345
+ raise ERRORS[:unknown_if_attribut] % "#{if_attr.inspect}:#{if_attr.class}"
346
+ end
347
+ end
348
+
349
+ # --- Hard Coded Properties ---
350
+
351
+ def index; @index ||= data[:index] end
352
+ def specs; @specs ||= data[:specs] end
353
+ def prop; @prop ||= data[:prop] end
354
+ def type; @type ||= data[:type] end
355
+ def quest; @quest ||= data[:quest] end
356
+ def if_attr; @ifattr ||= data[:if] end
357
+ def valid_if; @valid_if ||= data[:valid_if] end
358
+ def short_name; @short_name ||= data[:short_name] end
359
+ def name(instance = nil)
360
+ @name ||= data[:name]
361
+ if @name.is_a?(Proc)
362
+ @name.call(instance)
363
+ else
364
+ @name
365
+ end
366
+ end
367
+
368
+
369
+ def values(instance = nil)
370
+ vs =
371
+ vs = case (vs = data[:values])
372
+ when Symbol
373
+ if manager.classe.respond_to?(vs)
374
+ begin
375
+ manager.classe.send(vs)
376
+ rescue ArgumentError
377
+ manager.classe.send(vs, instance)
378
+ end
379
+ elsif manager.respond_to?(vs)
380
+ nargs = manager.method(d).arity
381
+ puts "Nombre d'arguments attendus par (#{vs}) : #{nargs}".orange
382
+ puts 10
383
+ if nargs == 0
384
+ manager.send(vs)
385
+ else
386
+ manager.send(vs, instance)
387
+ end
388
+ # begin
389
+ # manager.send(vs)
390
+ # rescue ArgumentError
391
+ # manager.send(vs, instance)
392
+ # end
393
+ else
394
+ raise ERRORS[:unknown_values_method] % vs.inspect
395
+ end
396
+ when Proc
397
+ vs.call(instance)
398
+ else
399
+ vs
400
+ end
401
+ uniq_name = "#{manager.classe.class.to_s.gsub(/::/,'-')}-#{prop}".downcase
402
+ return PrecedencedList.new(vs, uniq_name)
403
+ end
404
+
405
+ def default(instance)
406
+ d = data[:default]
407
+ d = d.call(instance) if d.is_a?(Proc)
408
+ if d.is_a?(Symbol)
409
+ if instance.respond_to?(d)
410
+ instance.send(d)
411
+ elsif instance.class.respond_to?(d)
412
+ nargs = instance.class.method(d).arity
413
+ puts "Nombre d'arguments attendus : #{nargs}".orange
414
+ puts 10
415
+ begin
416
+ d = instance.class.send(d)
417
+ rescue ArgumentError
418
+ d = instance.class.send(d, instance)
419
+ end
420
+ else
421
+ # La garder telle quelle
422
+ end
423
+ end
424
+ d
425
+ end
426
+ def values_filter; @values_filter ||= data[:values_filter] end
427
+ def itransform; @itransform ||= data[:itransform] end
428
+ def relative_class; @relative_class ||= data[:relative_class] end
429
+ def format_method; @format_method ||= data[:mformat]||data[:mformate]||data[:format_method] end
430
+
431
+ BOOLEAN_VALUES = [
432
+ {name: MSG[:yes] , value: true },
433
+ {name: MSG[:no] , value: false },
434
+ {name: MSG[:cancel] , value: nil }
435
+ ]
436
+ end #/class Property
437
+ end #/module DataManager
438
+ end #/module Clir
@@ -0,0 +1,157 @@
1
+ =begin
2
+ Clir::DataManager::Manager::Validator
3
+ -------------------------------------
4
+ To invalidate instance values
5
+
6
+ =end
7
+ module Clir
8
+ module DataManager
9
+ class Manager
10
+ class Validator
11
+
12
+ attr_reader :manager
13
+
14
+ def initialize(manager)
15
+ @manager = manager
16
+ end
17
+
18
+ # - main method -
19
+ #
20
+ # Quand l'éditeur (Manager::Editor) reçoit une nouvelle valeur pour
21
+ # la propriété +property+ ({Manager::Property}) il la checke ici
22
+ # pour savoir si elle est valide pour l'instance +instance+
23
+ #
24
+ # Noter que l'instance permet aussi de récupérer la classe de cette
25
+ # instance pour obtenir certaines valeurs. La classe doit par ex.
26
+ # répondre à la méthode ::get pour obtenir une autre instance.
27
+ #
28
+ def valid?(property, new_value, instance)
29
+ #
30
+ # Une propriété requise doit exister
31
+ #
32
+ if property.required?(instance) && (!new_value || new_value.to_s.empty?)
33
+ return ERRORS[:required_property] % property.name
34
+ end
35
+
36
+ if new_value
37
+
38
+ case property.type
39
+
40
+ when :email
41
+ #
42
+ # Un email
43
+ #
44
+ if not(mail_valid?(new_value))
45
+ return ERRORS[:invalid_mail] % new_value
46
+ end
47
+ when :date
48
+ #
49
+ # Une date
50
+ #
51
+ if not(date_valid?(new_value))
52
+ return ERRORS[:invalid_date] % new_value
53
+ end
54
+ when :url
55
+ #
56
+ # Une URL
57
+ #
58
+ if (err = url_invalid?(new_value))
59
+ return ERRORS[:invalid_url] % [new_value, err]
60
+ end
61
+ when :people
62
+ #
63
+ # Un ou des people
64
+ #
65
+ if (err = people_invalid?(new_value))
66
+ return ERRORS[:invalid_people] % [property.name, err]
67
+ end
68
+ end # suivant property.type
69
+
70
+ if property.valid_if
71
+ if (err = proceed_validation_propre(property, new_value, instance))
72
+ return ERRORS[:invalid_property] % [property.name, err]
73
+ end
74
+ end
75
+
76
+ end #/si la nouvelle valeur est défini
77
+
78
+ return nil # OK
79
+ end
80
+
81
+ # Quand les attributs de la propriété définissent :valid_if qui
82
+ # permet de procéder à une validation de la donnée +new_value+
83
+ #
84
+ # @return [NilClass|String] Return nil si aucune erreur n'est
85
+ # trouvée, sinon, retourne l'erreur rencontrée.
86
+ def proceed_validation_propre(property, new_value, instance)
87
+ meth = property.valid_if
88
+ case meth
89
+ when Symbol
90
+ if new_value.respond_to?(meth)
91
+ new_value.send(meth)
92
+ elsif instance.respond_to?(meth)
93
+ instance.send(meth, new_value)
94
+ elsif instance.class.respond_to?(meth)
95
+ instance.class.send(meth, new_value, instance)
96
+ end
97
+ when Proc
98
+ property.valid_if.call(new_value, instance)
99
+ else
100
+ raise ERRORS[:unknow_validate_method] % meth.inspect
101
+ end
102
+ end
103
+
104
+ # @return true si la donnée +people+ est une donnée de personne
105
+ # valide. Une donnée de personne valide correspond à
106
+ def people_invalid?(people)
107
+ people.split(',').each do |patro|
108
+ dpatro = patro.split(' ')
109
+ dpatro.count < 6 || raise(ERRORS[:too_long_name] % patro)
110
+ patro.match?(/[0-9?!_,;.…\/\\"]/) && raise(ERRORS[:bad_chars_in_name] % patro)
111
+ end
112
+ return nil # ok
113
+ rescue Exception => e
114
+ return e.message
115
+ end
116
+
117
+ # @return true si le mail +mail+ est valide
118
+ def mail_valid?(mail)
119
+ mail.match?(/^(.{6,40})@([a-z\-_\.0-9]+)\.([a-z]{2,6})$/i)
120
+ end
121
+
122
+ def date_valid?(date)
123
+ date.match?(MSG[:reg_date_format]) || return
124
+ begin
125
+ m, d, y = date.split('/').map {|n| n.to_i }
126
+ if LANG == 'fr'
127
+ Time.new(y, d, m)
128
+ else
129
+ Time.new(y, m, d)
130
+ end
131
+ rescue
132
+ return false
133
+ end
134
+ return true
135
+ end
136
+
137
+ # Noter que cette méthode fonctionne à l'inverse des autres : elle
138
+ # retourne un message d'erreur en cas d'invalidité et elle ne
139
+ # retourne rien si tout est OK
140
+ #
141
+ def url_invalid?(url)
142
+ require 'net/http'
143
+ uri = URI(url)
144
+ Net::HTTP.get(uri)
145
+ return nil # ok
146
+ rescue Exception => e
147
+ return e.message
148
+ # ensure
149
+ # puts "request: #{request.inspect}"
150
+ # sleep 10
151
+ # exit
152
+ end
153
+
154
+ end #/class Validator
155
+ end #/class Manager
156
+ end #/module DataManager
157
+ end #/module Clir
@@ -0,0 +1,14 @@
1
+
2
+ LANG = 'fr'
3
+ require_relative 'errors_and_messages'
4
+
5
+ module Clir
6
+ module DataManager
7
+ include ClirDataManagerConstants
8
+
9
+
10
+ CHOIX_RENONCER = {name: MSG[:cancel].orange, value:nil}
11
+ CHOIX_CREATE = {name: MSG[:create_new].bleu, value: :create}
12
+
13
+ end #/module DataManager
14
+ end #/module Clir