allow fetching products by linkAlias
[bse.git] / site / cgi-bin / modules / BSE / Importer / Target / Article.pm
CommitLineData
d415d0ba 1package BSE::Importer::Target::Article;
3709451d 2use strict;
d415d0ba 3use base 'BSE::Importer::Target::Base';
3709451d
TC
4use BSE::API qw(bse_make_article bse_add_image bse_add_step_parent);
5use Articles;
6use Products;
7use OtherParents;
8
d415d0ba
TC
9our $VERSION = "1.002";
10
11=head1 NAME
12
13BSE::Importer::Target::Article - import target for articles.
14
15=head1 SYNOPSIS
16
17 [import profile foo]
18 ...
19 ; these are the defaults
20 codes=0
21 code_field=linkAlias
22 parent=-1
23 ignore_missing=1
24 reset_images=0
25 reset_steps=0
26
27 # done by the importer
28 my $target = BSE::Importer::Target::Article->new
29 (importer => $importer, opts => \%opts)
30 ...
31 $target->start($imp);
32 # for each row:
33 $target->row($imp, \%entry, \@parents);
34
35
36=head1 DESCRIPTION
37
38Provides a target for importing BSE articles.
39
40The import profile must provide a C<title> mapping.
41
42=head1 CONFIGURATION
43
44The following extra configuration can be set in the import profile:
45
46=over
47
48=item *
49
50C<codes> - set to true to use the configured C<code_field> to update
51existing articles rather than creating new articles.
52
53=item *
54
55C<code_field> - the field to use to identify existing articles.
56Default: C<linkAlias> for article imports.
57
58=item *
59
60C<parent> - the base of the tree of parent articles to create the
61parent tree under.
62
63=item *
64
65C<ignore_missing> - set to 0 to error on missing image files.
66Default: 1.
67
68=item *
69
70C<reset_images> - set to true to delete all images from an article
71before adding the imported images.
72
73=item *
74
75C<reset_steps> - set to true to delete all step parents from an
76article before adding the imported steps.
77
78=back
79
80=head1 SPECIAL FIELDS
81
82The following fields are used to import extra information into
83articles:
84
85=over
86
87=item *
88
89C<< imageI<index>_I<field> >> - used to import images,
90eg. C<image1_file> to specify the image file. Note: images are not
91replaced unless C<reset_images> is set. I<index> is a number from 1
92to 10, I<field> can be any of C<file>, C<alt>, C<name>, C<url>,
93C<storage>, with the C<file> entry being required.
94
95=item *
96
97C<< stepI<index> >> - specify step parents for the article. This can
98either be the article id or the article link alias.
99
100=item *
101
102C<tags> - this is split on C</> to set the tags for the article.
103
104=back
105
106=head1 METHODS
107
108=over
109
110=item new()
111
112Create a new article import target. Follows the protocol specified by
113L<BSE::Importer::Target::Base>.
114
115=cut
cb7fd78d 116
3709451d
TC
117sub new {
118 my ($class, %opts) = @_;
119
120 my $self = $class->SUPER::new(%opts);
121
122 my $importer = delete $opts{importer};
123
124 my $map = $importer->maps;
125 defined $map->{title}
126 or die "No title mapping found\n";
127
128 $self->{use_codes} = $importer->cfg_entry('codes', 0);
129 $self->{code_field} = $importer->cfg_entry("code_field", $self->default_code_field);
130
131 $self->{parent} = $importer->cfg_entry("parent", $self->default_parent);
132
133 if ($self->{use_codes} && !defined $map->{$self->{code_field}}) {
4bfc78d4 134 die "No $self->{code_field} mapping found with 'codes' enabled\n";
3709451d
TC
135 }
136 $self->{ignore_missing} = $importer->cfg_entry("ignore_missing", 1);
137 $self->{reset_images} = $importer->cfg_entry("reset_images", 0);
138 $self->{reset_steps} = $importer->cfg_entry("reset_steps", 0);
139
140 return $self;
141}
142
d415d0ba
TC
143=item start()
144
145Start import processing.
146
147=cut
148
3709451d
TC
149sub start {
150 my ($self) = @_;
151
152 $self->{parent_cache} = {};
153 $self->{leaves} = [];
154 $self->{parents} = [];
155}
156
d415d0ba 157=item row()
3709451d 158
d415d0ba 159Process a row of data.
3709451d 160
d415d0ba 161=cut
0cca6ce6 162
3709451d
TC
163sub row {
164 my ($self, $importer, $entry, $parents) = @_;
165
0cca6ce6 166 $self->xform_entry($importer, $entry);
3709451d
TC
167
168 $entry->{parentid} = $self->_find_parent($importer, $self->{parent}, @$parents);
169 my $leaf;
170 if ($self->{use_codes}) {
171 my $leaf_id = $entry->{$self->{code_field}};
172
173 $leaf = $self->find_leaf($leaf_id);
174 }
175 if ($leaf) {
176 @{$leaf}{keys %$entry} = values %$entry;
177 $leaf->save;
178 $importer->info("Updated $leaf->{id}: $entry->{title}");
179 if ($self->{reset_images}) {
180 $leaf->remove_images($importer->cfg);
181 $importer->info(" $leaf->{id}: Reset images");
182 }
183 if ($self->{reset_steps}) {
184 my @steps = OtherParents->getBy(childId => $leaf->{id});
185 for my $step (@steps) {
186 $step->remove;
187 }
188 }
189 }
190 else {
191 $leaf = $self->make_leaf
192 (
193 $importer,
194 cfg => $importer->cfg,
195 %$entry
196 );
197 $importer->info("Added $leaf->{id}: $entry->{title}");
198 }
199 for my $image_index (1 .. 10) {
200 my $file = $entry->{"image${image_index}_file"};
201 $file
202 or next;
203 my $full_file = $importer->find_file($file);
204
205 unless ($full_file) {
206 $self->{ignore_missing}
207 and next;
208 die "File '$file' not found for image$image_index\n";
209 }
210
211 my %opts = ( file => $full_file );
212 for my $key (qw/alt name url storage/) {
213 my $fkey = "image${image_index}_$key";
214 $entry->{$fkey}
215 and $opts{$key} = $entry->{$fkey};
216 }
217
218 my %errors;
219 my $im = bse_add_image($importer->cfg, $leaf, %opts,
220 errors => \%errors);
221 $im
222 or die join(", ",map "$_: $errors{$_}", keys %errors), "\n";
223 $importer->info(" $leaf->{id}: Add image '$file'");
224 }
225 for my $step_index (1 .. 10) {
226 my $step_id = $entry->{"step$step_index"};
227 $step_id
228 or next;
229 my $step;
230 if ($step_id =~ /^\d+$/) {
231 $step = Articles->getByPkey($step_id);
232 }
233 else {
234 $step = Articles->getBy(linkAlias => $step_id);
235 }
236 $step
237 or die "Cannot find stepparent with id $step_id\n";
238
239 bse_add_step_parent($importer->cfg, child => $leaf, parent => $step);
240 }
0cca6ce6 241 $self->fill_leaf($importer, $leaf, %$entry);
3709451d
TC
242 push @{$self->{leaves}}, $leaf;
243}
244
d415d0ba
TC
245=item xform_entry()
246
247Called by row() to perform an extra data transformation needed.
248
249Currently this forces a non-blank, non-newline title, and defaults the
250values of C<summary>, C<description> and C<body> to the title.
251
252=cut
253
254sub xform_entry {
255 my ($self, $importer, $entry) = @_;
256
257 $entry->{title} =~ /\S/
258 or die "title blank\n";
259
260 $entry->{title} =~ /\n/
261 and die "Title may not contain newlines";
262 $entry->{summary}
263 or $entry->{summary} = $entry->{title};
264 $entry->{description}
265 or $entry->{description} = $entry->{title};
266 $entry->{body}
267 or $entry->{body} = $entry->{title};
268}
269
270=item children_of()
271
272Utility method to find the children of a given article.
273
274=cut
275
276sub children_of {
277 my ($self, $parent) = @_;
278
279 Articles->children($parent);
280}
281
282=item make_parent()
283
284Create a parent article.
285
286Overridden in the product importer to create catalogs.
287
288=cut
289
290sub make_parent {
291 my ($self, $importer, %entry) = @_;
292
293 return bse_make_article(%entry);
294}
295
296=item find_leaf()
297
298Find a leave article based on the supplied code.
299
300=cut
301
302sub find_leaf {
303 my ($self, $leaf_id) = @_;
304
305 $leaf_id =~ tr/A-Za-z0-9_/_/cds;
306
307 my ($leaf) = Articles->getBy($self->{code_field}, $leaf_id)
308 or return;
309
310 return $leaf;
311}
312
313=item make_leaf()
314
315Create an article based on the imported data.
316
317Overridden in the product importer to create products.
318
319=cut
320
321sub make_leaf {
322 my ($self, $importer, %entry) = @_;
323
324 return bse_make_article(%entry);
325}
326
327=item fill_leaf()
328
329Fill the article some more.
330
331Currently sets the tags.
332
333Overridden by the product target to set product options and tiered
334pricing.
335
336=cut
337
338sub fill_leaf {
339 my ($self, $importer, $leaf, %entry) = @_;
340
341 if ($entry{tags}) {
342 my @tags = split '/', $entry{tags};
343 my $error;
344 unless ($leaf->set_tags(\@tags, \$error)) {
345 die "Error setting tags: $error";
346 }
347 }
348
349 return 1;
350}
351
352=item _find_parent()
353
354Find a parent article.
355
356This method calls itself recursively to work down a tree of parents.
357
358=cut
359
3709451d
TC
360sub _find_parent {
361 my ($self, $importer, $parent, @parents) = @_;
362
363 @parents
364 or return $parent;
365 my $cache = $self->{parent_cache};
366 unless ($cache->{$parent}) {
367 my @kids = $self->children_of($parent);
368 $cache->{$parent} = \@kids;
369 }
370
371 my $title = shift @parents;
372 my ($cat) = grep lc $_->{title} eq lc $title, @{$cache->{$parent}};
373 unless ($cat) {
374 my %opts =
375 (
376 cfg => $importer->cfg,
377 parentid => $parent,
378 title => $title,
379 body => $title,
380 );
381 $self->{catalog_template}
382 and $opts{template} = $self->{catalog_template};
383 $cat = $self->make_parent($importer, %opts);
384 $importer->info("Add parent $cat->{id}: $title");
385 push @{$cache->{$parent}}, $cat;
386 }
387
388 unless ($self->{catseen}{$cat->{id}}) {
389 $self->{catseen}{$cat->{id}} = 1;
390 push @{$self->{parents}}, $cat;
391 }
392
393 return $self->_find_parent($importer, $cat->{id}, @parents);
394}
395
d415d0ba
TC
396=item default_parent()
397
398Return the default parent id.
399
400Overridden by the product target to return the shop id.
401
402=cut
403
3709451d
TC
404sub default_parent { -1 }
405
d415d0ba
TC
406=item default_code_field()
407
408Return the default code field.
409
410Overridden by the produuct target to return the C<product_code> field.
411
412=cut
413
3709451d
TC
414sub default_code_field { "linkAlias" }
415
d415d0ba
TC
416=item leaves()
417
418Return the leaf articles created or modified by the import run.
419
420=cut
421
3709451d
TC
422sub leaves {
423 return @{$_[0]{leaves}}
424}
425
d415d0ba
TC
426=item parents()
427
428Return the parent articles created or used by the import run.
429
430=cut
431
3709451d
TC
432sub parents {
433 return @{$_[0]{parents}}
434}
435
4361;
d415d0ba
TC
437
438=back
439
440=head1 AUTHOR
441
442Tony Cook <tony@develop-help.com>
443
444=cut