Revision c8473408
Von Bernd Blessmann vor fast 11 Jahren hinzugefügt
SL/Controller/CsvImport/Base.pm | ||
---|---|---|
31 | 31 |
my $profile = $self->profile; |
32 | 32 |
$self->csv(SL::Helper::Csv->new(file => $self->file->file_name, |
33 | 33 |
encoding => $self->controller->profile->get('charset'), |
34 |
class => $self->class, |
|
35 |
profile => $profile, |
|
34 |
profile => { profile => $profile, class => $self->class }, |
|
36 | 35 |
ignore_unknown_columns => 1, |
37 | 36 |
strict_profile => 1, |
38 | 37 |
case_insensitive_header => 1, |
SL/Helper/Csv.pm | ||
---|---|---|
9 | 9 |
use Params::Validate qw(:all); |
10 | 10 |
use Text::CSV_XS; |
11 | 11 |
use Rose::Object::MakeMethods::Generic scalar => [ qw( |
12 |
file encoding sep_char quote_char escape_char header profile class
|
|
12 |
file encoding sep_char quote_char escape_char header profile |
|
13 | 13 |
numberformat dateformat ignore_unknown_columns strict_profile _io _csv |
14 | 14 |
_objects _parsed _data _errors all_cvar_configs case_insensitive_header |
15 | 15 |
) ]; |
... | ... | |
29 | 29 |
profile => { type => HASHREF, optional => 1 }, |
30 | 30 |
file => 1, |
31 | 31 |
encoding => 0, |
32 |
class => 0, |
|
33 | 32 |
numberformat => 0, |
34 | 33 |
dateformat => 0, |
35 | 34 |
ignore_unknown_columns => 0, |
... | ... | |
71 | 70 |
|
72 | 71 |
sub get_objects { |
73 | 72 |
my ($self, %params) = @_; |
74 |
croak 'no class given' unless $self->class; |
|
75 | 73 |
croak 'must parse first' unless $self->_parsed; |
76 | 74 |
|
77 | 75 |
$self->_make_objects unless $self->_objects; |
... | ... | |
188 | 186 |
my ($self, %params) = @_; |
189 | 187 |
my @objs; |
190 | 188 |
|
191 |
eval "require " . $self->class; |
|
192 | 189 |
local $::myconfig{numberformat} = $self->numberformat if $self->numberformat; |
193 | 190 |
local $::myconfig{dateformat} = $self->dateformat if $self->dateformat; |
194 | 191 |
|
195 | 192 |
for my $line (@{ $self->_data }) { |
196 |
my $tmp_obj = $self->class->new; |
|
197 |
$self->dispatcher->dispatch($tmp_obj, $line); |
|
193 |
my $tmp_obj = $self->dispatcher->dispatch($line); |
|
198 | 194 |
push @objs, $tmp_obj; |
199 | 195 |
} |
200 | 196 |
|
... | ... | |
248 | 244 |
quote_char => '\'', # default '"' |
249 | 245 |
escape_char => '"', # default '"' |
250 | 246 |
header => [qw(id text sellprice word)], # see later |
251 |
profile => { sellprice => 'sellprice_as_number' }, |
|
252 |
class => 'SL::DB::CsvLine', # if present, map lines to this |
|
247 |
profile => { profile => { sellprice => 'sellprice_as_number'}, class => SL::DB::Part }, |
|
253 | 248 |
); |
254 | 249 |
|
255 | 250 |
my $status = $csv->parse; |
... | ... | |
348 | 343 |
Can be an array of columns, in this case the first line is not used as a |
349 | 344 |
header. Empty header fields will be ignored in objects. |
350 | 345 |
|
351 |
=item C<profile> \%ACCESSORS
|
|
346 |
=item C<profile> {profile => \%ACCESSORS, class => class}
|
|
352 | 347 |
|
353 |
May be used to map header fields to custom accessors. Example:
|
|
348 |
This is a HASHREF to hash which may contain the keys C<profile> and C<class>.
|
|
354 | 349 |
|
355 |
{ listprice => listprice_as_number } |
|
350 |
The C<profile> is a HASHREF which may be used to map header fields to custom |
|
351 |
accessors. Example: |
|
352 |
|
|
353 |
{profile => { listprice => listprice_as_number }} |
|
356 | 354 |
|
357 | 355 |
In this case C<listprice_as_number> will be used to read in values from the |
358 | 356 |
C<listprice> column. |
... | ... | |
360 | 358 |
In case of a One-To-One relationsship these can also be set over |
361 | 359 |
relationsships by sparating the steps with a dot (C<.>). This will work: |
362 | 360 |
|
363 |
{ customer => 'customer.name' }
|
|
361 |
{profile => { customer => 'customer.name' }}
|
|
364 | 362 |
|
365 | 363 |
And will result in something like this: |
366 | 364 |
|
... | ... | |
373 | 371 |
will have to do that for yourself. Since you provided the profile, it is |
374 | 372 |
assumed you know what to do in this case. |
375 | 373 |
|
376 |
If no profile is given, any header field found will be taken as is. |
|
377 |
|
|
378 |
If the path in a profile entry is empty, the field will be subjected to |
|
379 |
C<strict_profile> and C<case_insensitive_header> checking, will be parsed into |
|
380 |
C<get_data>, but will not be attempted to be dispatched into objects. |
|
381 |
|
|
382 |
=item C<class> |
|
383 |
|
|
384 |
If present, the line will be handed to the new sub of this class, |
|
374 |
If C<class> is present, the line will be handed to the new sub of this class, |
|
385 | 375 |
and the return value used instead of the line itself. |
386 | 376 |
|
387 | 377 |
=item C<ignore_unknown_columns> |
... | ... | |
442 | 432 |
Dispatch to child objects, like this: |
443 | 433 |
|
444 | 434 |
$csv = SL::Helper::Csv->new( |
445 |
file => ... |
|
446 |
class => SL::DB::Part, |
|
447 |
profile => [ |
|
448 |
makemodel => { |
|
449 |
make_1 => make, |
|
450 |
model_1 => model, |
|
451 |
}, |
|
452 |
makemodel => { |
|
453 |
make_2 => make, |
|
454 |
model_2 => model, |
|
455 |
}, |
|
456 |
] |
|
435 |
file => ... |
|
436 |
profile => { |
|
437 |
profile => [ |
|
438 |
makemodel => { |
|
439 |
make_1 => make, |
|
440 |
model_1 => model, |
|
441 |
}, |
|
442 |
makemodel => { |
|
443 |
make_2 => make, |
|
444 |
model_2 => model, |
|
445 |
}, |
|
446 |
], |
|
447 |
class => SL::DB::Part, |
|
448 |
} |
|
457 | 449 |
); |
458 | 450 |
|
459 | 451 |
=head1 AUTHOR |
SL/Helper/Csv/Dispatcher.pm | ||
---|---|---|
22 | 22 |
} |
23 | 23 |
|
24 | 24 |
sub dispatch { |
25 |
my ($self, $obj, $line) = @_; |
|
25 |
my ($self, $line) = @_; |
|
26 |
|
|
27 |
eval "require " . $self->_csv->profile->{class}; |
|
28 |
my $obj = $self->_csv->profile->{class}->new; |
|
26 | 29 |
|
27 | 30 |
for my $spec (@{ $self->_specs }) { |
28 | 31 |
$self->apply($obj, $spec, $line->{$spec->{key}}); |
29 | 32 |
} |
33 |
|
|
34 |
return $obj; |
|
30 | 35 |
} |
31 | 36 |
|
32 | 37 |
sub apply { |
... | ... | |
66 | 71 |
my ($self, %params) = @_; |
67 | 72 |
|
68 | 73 |
my $header = $self->_csv->header; |
69 |
my $profile = $self->_csv->profile; |
|
74 |
my $profile = $self->_csv->profile->{profile};
|
|
70 | 75 |
my @specs; |
71 | 76 |
|
72 | 77 |
for my $col (@$header) { |
... | ... | |
98 | 103 |
|
99 | 104 |
return unless $path; |
100 | 105 |
|
101 |
my $cur_class = $self->_csv->class;
|
|
106 |
my $cur_class = $self->_csv->profile->{class};
|
|
102 | 107 |
|
103 | 108 |
return unless $cur_class; |
104 | 109 |
|
t/helper/csv.t | ||
---|---|---|
11 | 11 |
Support::TestSetup::login(); |
12 | 12 |
|
13 | 13 |
my $csv = SL::Helper::Csv->new( |
14 |
file => \"Kaffee\n", |
|
15 |
header => [ 'description' ], |
|
16 |
class => 'SL::DB::Part',
|
|
14 |
file => \"Kaffee\n",
|
|
15 |
header => [ 'description' ],
|
|
16 |
profile => { class => 'SL::DB::Part', },
|
|
17 | 17 |
); |
18 | 18 |
|
19 | 19 |
isa_ok $csv->_csv, 'Text::CSV_XS'; |
... | ... | |
28 | 28 |
$::myconfig{dateformat} = 'dd.mm.yyyy'; |
29 | 29 |
|
30 | 30 |
$csv = SL::Helper::Csv->new( |
31 |
file => \"Kaffee;0.12;12,2;1,5234\n", |
|
32 |
header => [ 'description', 'sellprice', 'lastcost_as_number', 'listprice' ], |
|
33 |
profile => { listprice => 'listprice_as_number' }, |
|
34 |
class => 'SL::DB::Part',
|
|
31 |
file => \"Kaffee;0.12;12,2;1,5234\n",
|
|
32 |
header => [ 'description', 'sellprice', 'lastcost_as_number', 'listprice' ],
|
|
33 |
profile => {profile => { listprice => 'listprice_as_number' },
|
|
34 |
class => 'SL::DB::Part',},
|
|
35 | 35 |
); |
36 | 36 |
$csv->parse; |
37 | 37 |
|
... | ... | |
49 | 49 |
EOL |
50 | 50 |
sep_char => ',', |
51 | 51 |
quote_char => "'", |
52 |
profile => { listprice => 'listprice_as_number' }, |
|
53 |
class => 'SL::DB::Part',
|
|
52 |
profile => {profile => { listprice => 'listprice_as_number' },
|
|
53 |
class => 'SL::DB::Part',}
|
|
54 | 54 |
); |
55 | 55 |
$csv->parse; |
56 | 56 |
is scalar @{ $csv->get_objects }, 1, 'auto header works'; |
... | ... | |
64 | 64 |
;;description;sellprice;lastcost_as_number; |
65 | 65 |
#####;Puppy;Kaffee;0.12;12,2;1,5234 |
66 | 66 |
EOL |
67 |
class => 'SL::DB::Part',
|
|
67 |
profile => {class => 'SL::DB::Part'},
|
|
68 | 68 |
); |
69 | 69 |
$csv->parse; |
70 | 70 |
is scalar @{ $csv->get_objects }, 1, 'bozo header doesn\'t blow things up'; |
... | ... | |
77 | 77 |
Kaffee;;0.12;12,2;1,5234 |
78 | 78 |
Beer;1123245;0.12;12,2;1,5234 |
79 | 79 |
EOL |
80 |
class => 'SL::DB::Part',
|
|
80 |
profile => {class => 'SL::DB::Part'},
|
|
81 | 81 |
); |
82 | 82 |
$csv->parse; |
83 | 83 |
is scalar @{ $csv->get_objects }, 2, 'multiple objects work'; |
... | ... | |
93 | 93 |
Beer;1123245;0.12;1.5234 |
94 | 94 |
EOL |
95 | 95 |
numberformat => '1,000.00', |
96 |
class => 'SL::DB::Part',
|
|
96 |
profile => {class => 'SL::DB::Part'},
|
|
97 | 97 |
); |
98 | 98 |
$csv->parse; |
99 | 99 |
is $csv->get_objects->[0]->lastcost, '1221.52', 'formatnumber'; |
... | ... | |
107 | 107 |
Beer;1123245;0.12;1.5234 |
108 | 108 |
EOL |
109 | 109 |
numberformat => '1,000.00', |
110 |
class => 'SL::DB::Part',
|
|
110 |
profile => {class => 'SL::DB::Part'},
|
|
111 | 111 |
); |
112 | 112 |
is $csv->parse, undef, 'broken csv header won\'t get parsed'; |
113 | 113 |
|
... | ... | |
120 | 120 |
Beer;1123245;0.12;1.5234 |
121 | 121 |
EOL |
122 | 122 |
numberformat => '1,000.00', |
123 |
class => 'SL::DB::Part',
|
|
123 |
profile => {class => 'SL::DB::Part'},
|
|
124 | 124 |
); |
125 | 125 |
is $csv->parse, undef, 'broken csv content won\'t get parsed'; |
126 | 126 |
is_deeply $csv->errors, [ '"Kaf"fee";;0.12;1,221.52'."\n", 2023, 'EIQ - QUO character not allowed', 5, 2 ], 'error'; |
... | ... | |
136 | 136 |
EOL |
137 | 137 |
numberformat => '1,000.00', |
138 | 138 |
ignore_unknown_columns => 1, |
139 |
class => 'SL::DB::Part',
|
|
139 |
profile => {class => 'SL::DB::Part'},
|
|
140 | 140 |
); |
141 | 141 |
$csv->parse; |
142 | 142 |
is $csv->get_objects->[0]->lastcost, '1221.52', 'ignore_unkown_columns works'; |
... | ... | |
150 | 150 |
Beer;1123245;0.12;1.5234;16 % |
151 | 151 |
EOL |
152 | 152 |
numberformat => '1,000.00', |
153 |
class => 'SL::DB::Part', |
|
154 | 153 |
profile => { |
155 |
buchungsgruppe => "buchungsgruppen.description", |
|
154 |
profile => {buchungsgruppe => "buchungsgruppen.description"}, |
|
155 |
class => 'SL::DB::Part', |
|
156 | 156 |
} |
157 | 157 |
); |
158 | 158 |
$csv->parse; |
... | ... | |
169 | 169 |
Beer;1123245;0.12;1.5234; |
170 | 170 |
EOL |
171 | 171 |
numberformat => '1,000.00', |
172 |
class => 'SL::DB::Part', |
|
173 | 172 |
profile => { |
174 |
make_1 => "makemodels.0.make", |
|
175 |
model_1 => "makemodels.0.model", |
|
176 |
} |
|
173 |
profile => { |
|
174 |
make_1 => "makemodels.0.make", |
|
175 |
model_1 => "makemodels.0.model", |
|
176 |
}, |
|
177 |
class => 'SL::DB::Part', |
|
178 |
}, |
|
177 | 179 |
); |
178 | 180 |
$csv->parse; |
179 | 181 |
my @mm = $csv->get_objects->[0]->makemodel; |
... | ... | |
189 | 191 |
Kaffee;;0.12;1,221.52;213;Chair 0815;523;Table 15 |
190 | 192 |
EOL |
191 | 193 |
numberformat => '1,000.00', |
192 |
class => 'SL::DB::Part', |
|
193 | 194 |
profile => { |
194 |
make_1 => "makemodels.0.make", |
|
195 |
model_1 => "makemodels.0.model", |
|
196 |
make_2 => "makemodels.1.make", |
|
197 |
model_2 => "makemodels.1.model", |
|
195 |
profile => { |
|
196 |
make_1 => "makemodels.0.make", |
|
197 |
model_1 => "makemodels.0.model", |
|
198 |
make_2 => "makemodels.1.make", |
|
199 |
model_2 => "makemodels.1.model", |
|
200 |
}, |
|
201 |
class => 'SL::DB::Part', |
|
198 | 202 |
} |
199 | 203 |
); |
200 | 204 |
$csv->parse; |
... | ... | |
215 | 219 |
description;partnumber;sellprice;lastcost_as_number;buchungsgruppe; |
216 | 220 |
EOL |
217 | 221 |
numberformat => '1,000.00', |
218 |
class => 'SL::DB::Part', |
|
219 | 222 |
profile => { |
220 |
buchungsgruppe => "buchungsgruppen.1.description", |
|
223 |
profile => {buchungsgruppe => "buchungsgruppen.1.description"}, |
|
224 |
class => 'SL::DB::Part', |
|
221 | 225 |
} |
222 | 226 |
); |
223 | 227 |
is $csv->parse, undef, 'wrong profile gets rejected'; |
... | ... | |
235 | 239 |
numberformat => '1,000.00', |
236 | 240 |
ignore_unknown_columns => 1, |
237 | 241 |
strict_profile => 1, |
238 |
class => 'SL::DB::Part', |
|
239 | 242 |
profile => { |
240 |
lastcost => 'lastcost_as_number', |
|
243 |
profile => {lastcost => 'lastcost_as_number'}, |
|
244 |
class => 'SL::DB::Part', |
|
241 | 245 |
} |
242 | 246 |
); |
243 | 247 |
$csv->parse; |
... | ... | |
254 | 258 |
EOL |
255 | 259 |
numberformat => '1,000.00', |
256 | 260 |
strict_profile => 1, |
257 |
class => 'SL::DB::Part', |
|
258 | 261 |
profile => { |
259 |
lastcost => 'lastcost_as_number', |
|
262 |
profile => {lastcost => 'lastcost_as_number'}, |
|
263 |
class => 'SL::DB::Part', |
|
260 | 264 |
} |
261 | 265 |
); |
262 | 266 |
$csv->parse; |
... | ... | |
268 | 272 |
$csv = SL::Helper::Csv->new( |
269 | 273 |
file => \"Kaffee", |
270 | 274 |
header => [ 'description' ], |
271 |
class => 'SL::DB::Part',
|
|
275 |
profile => {class => 'SL::DB::Part'},
|
|
272 | 276 |
); |
273 | 277 |
$csv->parse; |
274 | 278 |
is_deeply $csv->get_data, [ { description => 'Kaffee' } ], 'eol bug at the end of files'; |
... | ... | |
277 | 281 |
|
278 | 282 |
$csv = SL::Helper::Csv->new( |
279 | 283 |
file => \"Description\nKaffee", |
280 |
class => 'SL::DB::Part', |
|
281 | 284 |
case_insensitive_header => 1, |
282 |
profile => { description => 'description' },
|
|
285 |
profile => {profile => { description => 'description' }, class => 'SL::DB::Part'},
|
|
283 | 286 |
); |
284 | 287 |
$csv->parse; |
285 | 288 |
is_deeply $csv->get_data, [ { description => 'Kaffee' } ], 'case insensitive header from csv works'; |
... | ... | |
289 | 292 |
$csv = SL::Helper::Csv->new( |
290 | 293 |
file => \"Kaffee", |
291 | 294 |
header => [ 'Description' ], |
292 |
class => 'SL::DB::Part', |
|
293 | 295 |
case_insensitive_header => 1, |
294 |
profile => { description => 'description' },
|
|
296 |
profile => {profile => { description => 'description' }, class => 'SL::DB::Part'},
|
|
295 | 297 |
); |
296 | 298 |
$csv->parse; |
297 | 299 |
is_deeply $csv->get_data, [ { description => 'Kaffee' } ], 'case insensitive header as param works'; |
... | ... | |
300 | 302 |
|
301 | 303 |
$csv = SL::Helper::Csv->new( |
302 | 304 |
file => \"\x{EF}\x{BB}\x{BF}description\nKaffee", |
303 |
class => 'SL::DB::Part',
|
|
305 |
profile => {class => 'SL::DB::Part'},
|
|
304 | 306 |
encoding => 'utf8', |
305 | 307 |
); |
306 | 308 |
$csv->parse; |
... | ... | |
356 | 358 |
is_deeply $csv->get_data, [ { cvar_Groundhog => 'Phil' } ], 'using empty path to get cvars working'; |
357 | 359 |
ok $csv->get_objects->[0], '...and not destorying the objects'; |
358 | 360 |
|
361 |
$csv = SL::Helper::Csv->new( |
|
362 |
file => \"description\nKaffee", |
|
363 |
); |
|
364 |
$csv->parse; |
|
365 |
is_deeply $csv->get_data, [ { description => 'Kaffee' } ], 'without profile and class works'; |
|
366 |
|
|
359 | 367 |
# vim: ft=perl |
Auch abrufbar als: Unified diff
SL::Helper::Csv bekommt Klasse im Profil mitgeteilt.