1 package BSE::Importer::Target::Article;
3 use base 'BSE::Importer::Target::Base';
4 use BSE::API qw(bse_make_article bse_add_image bse_add_step_parent);
7 use BSE::TB::OtherParents;
9 our $VERSION = "1.011";
13 BSE::Importer::Target::Article - import target for articles.
19 ; these are the defaults
28 # done by the importer
29 my $target = BSE::Importer::Target::Article->new
30 (importer => $importer, opts => \%opts)
34 $target->row($imp, \%entry, \@parents);
39 Provides a target for importing BSE articles.
41 C<update_only> profiles must provide a mapping for one of C<id> or
44 Non-C<update_only> profiles must provide a mapping for C<title>.
48 The following extra configuration can be set in the import profile:
54 C<codes> - set to true to use the configured C<code_field> to update
55 existing articles rather than creating new articles. This is forced
56 on when the import profile enables C<update_only>.
60 C<code_field> - the field to use to identify existing articles.
61 Default: C<linkAlias> for article imports.
65 C<parent> - the base of the tree of parent articles to create the
70 C<ignore_missing> - set to 0 to error on missing image or article
75 C<reset_images> - set to true to delete all images from an article
76 before adding the imported images.
80 C<reset_files> - set to true to delete all files from an article
81 before adding the imported files.
85 C<reset_steps> - set to true to delete all step parents from an
86 article before adding the imported steps.
92 The following fields are used to import extra information into
99 C<< imageI<index>_I<field> >> - used to import images,
100 eg. C<image1_file> to specify the image file. Note: images are not
101 replaced unless C<reset_images> is set. I<index> is a number from 1
102 to 10, I<field> can be any of C<file>, C<alt>, C<name>, C<url>,
103 C<storage>, with the C<file> entry being required.
107 C<< stepI<index> >> - specify step parents for the article. This can
108 either be the article id or the article link alias.
112 C<tags> - this is split on C</> to set the tags for the article.
122 Create a new article import target. Follows the protocol specified by
123 L<BSE::Importer::Target::Base>.
128 my ($class, %opts) = @_;
130 my $self = $class->SUPER::new(%opts);
132 my $importer = delete $opts{importer};
134 $self->{use_codes} = $importer->cfg_entry('codes', 0);
135 my $map = $importer->maps;
136 if ($importer->update_only) {
140 for my $key ($self->key_fields) {
148 or die "No key field (", join(",", $self->key_fields),
151 $self->{code_field} = $importer->cfg_entry("code_field", $def_code);
152 $self->{use_codes} = 1;
155 defined $map->{title}
156 or die "No title mapping found\n";
158 $self->{code_field} = $importer->cfg_entry("code_field", $self->default_code_field);
162 $self->{parent} = $importer->cfg_entry("parent", $self->default_parent);
164 if ($self->{use_codes} && !defined $map->{$self->{code_field}}) {
165 die "No $self->{code_field} mapping found with 'codes' enabled\n";
167 $self->{ignore_missing} = $importer->cfg_entry("ignore_missing", 1);
168 $self->{reset_images} = $importer->cfg_entry("reset_images", 0);
169 $self->{reset_files} = $importer->cfg_entry("reset_files", 0);
170 $self->{reset_steps} = $importer->cfg_entry("reset_steps", 0);
177 Start import processing.
184 $self->{parent_cache} = {};
185 $self->{leaves} = [];
186 $self->{parents} = [];
191 Process a row of data.
196 my ($self, $importer, $entry, $parents) = @_;
198 $self->xform_entry($importer, $entry);
200 if (!$importer->update_only || @$parents) {
201 $entry->{parentid} = $self->_find_parent($importer, $self->{parent}, @$parents);
205 if ($self->{use_codes}) {
206 my $leaf_id = $entry->{$self->{code_field}};
208 if ($importer->{update_only}) {
210 or die "$self->{code_field} blank for update_only profile\n";
213 $leaf = $self->find_leaf($leaf_id, $importer);
216 @{$leaf}{keys %$entry} = values %$entry;
217 $leaf->mark_modified(actor => $importer->actor);
219 $importer->info("Updated $leaf->{id}: ".$leaf->title);
220 if ($self->{reset_images}) {
221 $leaf->remove_images($importer->cfg);
222 $importer->info(" $leaf->{id}: Reset images");
224 if ($self->{reset_files}) {
225 $leaf->remove_files($importer->cfg);
226 $importer->info(" $leaf->{id}: Reset files");
228 if ($self->{reset_steps}) {
229 my @steps = BSE::TB::OtherParents->getBy(childId => $leaf->{id});
230 for my $step (@steps) {
235 elsif (!$importer->update_only) {
236 $entry->{createdBy} ||= ref $importer->actor ? $importer->actor->logon : "";
237 $entry->{lastModifiedBy} ||= ref $importer->actor ? $importer->actor->logon : "";
238 $self->validate_make_leaf($importer, $entry);
239 $leaf = $self->make_leaf
242 cfg => $importer->cfg,
245 $importer->info("Added $leaf->{id}: $entry->{title}");
248 die "No leaf found for $entry->{$self->{code_field}} for update_only profile\n";
250 for my $image_index (1 .. 10) {
251 my $file = $entry->{"image${image_index}_file"};
254 my $full_file = $importer->find_file($file);
256 unless ($full_file) {
257 $self->{ignore_missing}
259 die "File '$file' not found for image$image_index\n";
262 my %opts = ( file => $full_file );
263 for my $key (qw/alt name url storage/) {
264 my $fkey = "image${image_index}_$key";
266 and $opts{$key} = $entry->{$fkey};
270 my $im = bse_add_image($importer->cfg, $leaf, %opts,
273 or die join(", ",map "$_: $errors{$_}", keys %errors), "\n";
274 $importer->info(" $leaf->{id}: Add image '$file'");
276 $self->_add_files($importer, $entry, $leaf);
277 for my $step_index (1 .. 10) {
278 my $step_id = $entry->{"step$step_index"};
282 if ($step_id =~ /^\d+$/) {
283 $step = BSE::TB::Articles->getByPkey($step_id);
286 $step = BSE::TB::Articles->getBy(linkAlias => $step_id);
289 or die "Cannot find stepparent with id $step_id\n";
291 bse_add_step_parent($importer->cfg, child => $leaf, parent => $step);
293 $self->fill_leaf($importer, $leaf, %$entry);
294 push @{$self->{leaves}}, $leaf;
296 $importer->event(endrow => { leaf => $leaf });
300 my ($self, $importer, $entry, $leaf) = @_;
302 my %named_files = map { $_->name => $_ } grep $_->name ne '', $leaf->files;
304 for my $file_index (1 .. 10) {
308 for my $key (qw/name displayName storage description forSale download requireUser notes hide_from_list category/) {
309 my $fkey = "file${file_index}_$key";
310 if (defined $entry->{$fkey}) {
311 $opts{$key} = $entry->{$fkey};
316 my $filename = $entry->{"file${file_index}_file"};
318 my $full_file = $importer->find_file($filename);
320 unless ($full_file) {
321 $self->{ignore_missing}
323 die "File '$filename' not found for file$file_index\n";
326 $opts{filename} = $full_file;
335 $file = $named_files{$opts{name}};
338 if (!$file && !$opts{filename}) {
339 $importer->warn("No file${file_index}_file supplied but other file${file_index}_* field supplied");
343 if ($filename && !$opts{displayName}) {
344 unless (($opts{displayName}) = $filename =~ /([^\\\/:]+)$/) {
345 $importer->warn("Cannot create displayName for $filename");
355 _actor => $importer->actor,
356 _warnings => \@warnings,
360 $importer->info(" $leaf->{id}: Update file '".$file->displayName ."'");
363 # this dies on failure
364 $file = $leaf->add_file
371 $importer->info(" $leaf->{id}: Add file '$filename'");
382 Called by row() to perform an extra data transformation needed.
384 Currently this forces a non-blank, non-newline title, and defaults the
385 values of C<summary>, C<description> and C<body> to the title.
390 my ($self, $importer, $entry) = @_;
392 if (exists $entry->{title}) {
393 $entry->{title} =~ /\S/
394 or die "title blank\n";
396 $entry->{title} =~ /\n/
397 and die "Title may not contain newlines";
399 unless ($importer->update_only) {
401 or $entry->{summary} = $entry->{title};
402 $entry->{description}
403 or $entry->{description} = $entry->{title};
405 or $entry->{body} = $entry->{title};
408 if (defined $entry->{linkAlias}) {
409 $entry->{linkAlias} =~ tr/A-Za-z0-9_-//cd;
415 Utility method to find the children of a given article.
420 my ($self, $parent) = @_;
422 BSE::TB::Articles->children($parent);
427 Create a parent article.
429 Overridden in the product importer to create catalogs.
434 my ($self, $importer, %entry) = @_;
436 return bse_make_article(%entry);
441 Find a leave article based on the supplied code.
446 my ($self, $leaf_id, $importer) = @_;
448 $leaf_id =~ s/\A\s+//;
449 $leaf_id =~ s/\s+\z//;
451 my ($leaf) = BSE::TB::Articles->getBy($self->{code_field}, $leaf_id)
454 $importer->event(find_leaf => { id => $leaf_id, leaf => $leaf });
461 Create an article based on the imported data.
463 Overridden in the product importer to create products.
468 my ($self, $importer, %entry) = @_;
470 my $leaf = bse_make_article(%entry);
472 $importer->event(make_leaf => { leaf => $leaf });
479 Fill the article some more.
481 Currently sets the tags.
483 Overridden by the product target to set product options and tiered
489 my ($self, $importer, $leaf, %entry) = @_;
492 my @tags = split '/', $entry{tags};
494 unless ($leaf->set_tags(\@tags, \$error)) {
495 die "Error setting tags: $error";
504 Find a parent article.
506 This method calls itself recursively to work down a tree of parents.
511 my ($self, $importer, $parent, @parents) = @_;
515 my $cache = $self->{parent_cache};
516 unless ($cache->{$parent}) {
517 my @kids = $self->children_of($parent);
518 $cache->{$parent} = \@kids;
521 my $title = shift @parents;
522 my ($cat) = grep lc $_->{title} eq lc $title, @{$cache->{$parent}};
526 cfg => $importer->cfg,
531 $self->{catalog_template}
532 and $opts{template} = $self->{catalog_template};
533 $cat = $self->make_parent($importer, %opts);
534 $importer->info("Add parent $cat->{id}: $title");
535 push @{$cache->{$parent}}, $cat;
538 unless ($self->{catseen}{$cat->{id}}) {
539 $self->{catseen}{$cat->{id}} = 1;
540 push @{$self->{parents}}, $cat;
543 return $self->_find_parent($importer, $cat->{id}, @parents);
546 =item default_parent()
548 Return the default parent id.
550 Overridden by the product target to return the shop id.
554 sub default_parent { -1 }
556 =item default_code_field()
558 Return the default code field.
560 Overridden by the product target to return the C<product_code> field.
564 sub default_code_field { "linkAlias" }
568 Return the leaf articles created or modified by the import run.
573 return @{$_[0]{leaves}}
578 Return the parent articles created or used by the import run.
583 return @{$_[0]{parents}}
588 Columns that can act as keys.
593 return qw(id linkAlias);
596 =item validate_make_leaf
598 Perform validation only needed on creation
602 sub validate_make_leaf {
603 my ($self, $importer, $entry) = @_;
605 if (defined $entry->{linkAlias} && $entry->{linkAlias} ne '') {
606 my $other = BSE::TB::Articles->getBy(linkAlias => $entry->{linkAlias});
608 and die "Duplicate linkAlias value with article ", $other->id, "\n";
618 Tony Cook <tony@develop-help.com>