support importing article files as we do products
authorTony Cook <tony@develop-help.com>
Wed, 3 Apr 2013 03:15:49 +0000 (14:15 +1100)
committerTony Cook <tony@develop-help.com>
Mon, 8 Apr 2013 00:54:35 +0000 (10:54 +1000)
site/cgi-bin/modules/Article.pm
site/cgi-bin/modules/BSE/Importer.pm
site/cgi-bin/modules/BSE/Importer/Target/Article.pm
site/cgi-bin/modules/BSE/TB/ArticleFile.pm
site/cgi-bin/modules/BSE/TB/SiteCommon.pm
site/docs/bse_import.pod
t/130-importer/020-article.t

index a871bfa..b55fe4e 100644 (file)
@@ -8,7 +8,7 @@ use vars qw/@ISA/;
 @ISA = qw/Squirrel::Row BSE::TB::SiteCommon BSE::TB::TagOwner/;
 use Carp 'confess';
 
-our $VERSION = "1.018";
+our $VERSION = "1.019";
 
 =head1 NAME
 
@@ -349,9 +349,7 @@ sub remove {
 
   $self->remove_images($cfg);
 
-  for my $file ($self->files) {
-    $file->remove($cfg);
-  }
+  $self->remove_files($cfg);
   
   # remove any step(child|parent) links
   require OtherParents;
index 609a876..a1b5499 100644 (file)
@@ -2,7 +2,7 @@ package BSE::Importer;
 use strict;
 use Config;
 
-our $VERSION = "1.006";
+our $VERSION = "1.007";
 
 =head1 NAME
 
@@ -112,6 +112,10 @@ processing.
 
 C<listen> - a hashref of event handlers.
 
+=item *
+
+C<actor> - an actor name suitable for audit logging.
+
 =back
 
 If the profile is invalid, new() with die with a newline terminated
@@ -513,6 +517,16 @@ sub update_only {
   $_[0]{update_only};
 }
 
+=item actor
+
+The actor supplied to new.
+
+=cut
+
+sub actor {
+  $_[0]{actor} || "U";
+}
+
 1;
 
 =back
index fffef43..f1d56bc 100644 (file)
@@ -6,7 +6,7 @@ use Articles;
 use Products;
 use OtherParents;
 
-our $VERSION = "1.003";
+our $VERSION = "1.004";
 
 =head1 NAME
 
@@ -22,6 +22,7 @@ BSE::Importer::Target::Article - import target for articles.
   parent=-1
   ignore_missing=1
   reset_images=0
+  reset_files=0
   reset_steps=0
 
   # done by the importer
@@ -66,8 +67,8 @@ parent tree under.
 
 =item *
 
-C<ignore_missing> - set to 0 to error on missing image files.
-Default: 1.
+C<ignore_missing> - set to 0 to error on missing image or article
+files.  Default: 1.
 
 =item *
 
@@ -76,6 +77,11 @@ before adding the imported images.
 
 =item *
 
+C<reset_files> - set to true to delete all files from an article
+before adding the imported files.
+
+=item *
+
 C<reset_steps> - set to true to delete all step parents from an
 article before adding the imported steps.
 
@@ -160,6 +166,7 @@ sub new {
   }
   $self->{ignore_missing} = $importer->cfg_entry("ignore_missing", 1);
   $self->{reset_images} = $importer->cfg_entry("reset_images", 0);
+  $self->{reset_files} = $importer->cfg_entry("reset_files", 0);
   $self->{reset_steps} = $importer->cfg_entry("reset_steps", 0);
 
   return $self;
@@ -213,6 +220,10 @@ sub row {
       $leaf->remove_images($importer->cfg);
       $importer->info(" $leaf->{id}: Reset images");
     }
+    if ($self->{reset_files}) {
+      $leaf->remove_files($importer->cfg);
+      $importer->info(" $leaf->{id}: Reset files");
+    }
     if ($self->{reset_steps}) {
       my @steps = OtherParents->getBy(childId => $leaf->{id});
       for my $step (@steps) {
@@ -258,6 +269,7 @@ sub row {
       or die join(", ",map "$_: $errors{$_}", keys %errors), "\n";
     $importer->info(" $leaf->{id}: Add image '$file'");
   }
+  $self->_add_files($importer, $entry, $leaf);
   for my $step_index (1 .. 10) {
     my $step_id = $entry->{"step$step_index"};
     $step_id
@@ -280,6 +292,80 @@ sub row {
   $importer->event(endrow => { leaf => $leaf });
 }
 
+sub _add_files {
+  my ($self, $importer, $entry, $leaf) = @_;
+
+  my %named_files = map { $_->name => $_ } grep $_->name ne '', $leaf->files;
+
+  for my $file_index (1 .. 10) {
+    my %opts;
+
+    my $found = 0;
+    for my $key (qw/name displayName storage description forSale download requireUser notes hide_from_list category/) {
+      my $fkey = "file${file_index}_$key";
+      if (defined $entry->{$fkey}) {
+       $opts{$key} = $entry->{$fkey};
+       $found = 1;
+      }
+    }
+
+    my $filename = $entry->{"file${file_index}_file"};
+    if ($filename) {
+      my $full_file = $importer->find_file($filename);
+
+      unless ($full_file) {
+       $self->{ignore_missing}
+         and next;
+       die "File '$filename' not found for file$file_index\n";
+      }
+
+      $opts{filename} = $full_file;
+      $found = 1;
+    }
+
+    $found
+      or next;
+
+    my $file;
+    if ($opts{name}) {
+      $file = $named_files{$opts{name}};
+    }
+
+    if (!$file && !$opts{filename}) {
+      die "No file${file_index}_file supplied but other file${file_index}_* field supplied\n";
+    }
+
+    if ($filename && !$opts{displayName}) {
+      ($opts{displayName}) = $filename =~ /([^\\\/:]+)$/
+       or die "Cannot create displayName for $filename\n";
+    }
+
+    if ($file) {
+      my @warnings;
+      $file->update
+       (
+        _actor => $importer->actor,
+        _warnings => \@warnings,
+        %opts,
+       );
+
+      $importer->info(" $leaf->{id}: Update file '".$file->displayName ."'");
+    }
+    else {
+      # this dies on failure
+      $file = $leaf->add_file
+       (
+        $importer->cfg,
+        %opts,
+        store => 1,
+       );
+
+
+      $importer->info(" $leaf->{id}: Add file '$filename'");
+    }
+  }
+}
+
 =item xform_entry()
 
 Called by row() to perform an extra data transformation needed.
index 2d29a36..494657d 100644 (file)
@@ -6,7 +6,7 @@ use vars qw/@ISA/;
 @ISA = qw/Squirrel::Row/;
 use Carp 'confess';
 
-our $VERSION = "1.008";
+our $VERSION = "1.009";
 
 sub columns {
   return qw/id articleId displayName filename sizeInBytes description 
@@ -408,7 +408,7 @@ sub update {
        require DevHelp::FileUpload;
        my $msg;
        ($filename) = DevHelp::FileUpload->
-         make_img_copy($file_dir, $opts{displayName}, \$msg)
+         make_fh_copy($in_fh, $file_dir, $opts{displayName}, \$msg)
            or die "$msg\n";
       }
     }
@@ -439,6 +439,14 @@ sub update {
     $self->set_contentType($type);
   }
 
+  for my $field (qw(displayName description forSale download requireUser notes hide_from_list category)) {
+    my $value = delete $opts{$field};
+    if (defined $value) {
+      my $method = "set_$field";
+      $self->$method($value);
+    }
+  }
+
   my $name = $opts{name};
   $self->id != -1 || defined $name && $name =~ /\S/
     or die "name is required for global files\n";
index c9627e8..e35de85 100644 (file)
@@ -2,7 +2,7 @@ package BSE::TB::SiteCommon;
 use strict;
 use Carp qw(confess);
 
-our $VERSION = "1.010";
+our $VERSION = "1.011";
 
 =head1 NAME
 
@@ -249,6 +249,26 @@ sub remove_images {
   }
 }
 
+sub remove_files {
+  my ($self, $cfg) = @_;
+
+  $cfg ||= BSE::Cfg->single;
+  my @files = $self->files;
+  my $mgr;
+  require BSE::CfgInfo;
+  for my $file (@files) {
+    if ($file->storage ne 'local') {
+      unless ($mgr) {
+       require BSE::StorageMgr::Files;
+       $mgr = BSE::StorageMgr::Files->new(cfg => $cfg);
+      }
+      $mgr->unstore($file->filename, $file->storage);
+    }
+
+    $file->remove($cfg);
+  }
+}
+
 sub _copy_fh_to_fh {
   my ($in, $out) = @_;
 
index 355a43f..5d457a6 100644 (file)
@@ -31,12 +31,12 @@ to search for image files
 
 =item *
 
-source - type of input format, default and currently only XLS.
+source - type of input format, default and currently C<XLS> or C<CSV>.
 
 =item *
 
 target - type of output record, default Product, can currently be
-Article or Product.
+C<Article> or C<Product>.
 
 =item *
 
@@ -79,7 +79,15 @@ for articles.
 
 =item *
 
-ignore_missing - ignore missing image files.  Default: 1.
+update_only - if true, records are only updated, the summary, body and
+description fields are no longer populated from title, title is no
+longer required to be mapped, and codes defaults to true.  If
+C<code_field> isn't specified the first mapped field from C<id> or
+C<linkAlias> is selected (or C<product_code> for products)
+
+=item *
+
+ignore_missing - ignore missing image and article files.  Default: 1.
 
 =item *
 
@@ -87,6 +95,10 @@ reset_images - reset images for an article being imported.  Default: 0.
 
 =item *
 
+reset_files - reset files for an article being imported.  Default: 0.
+
+=item *
+
 reset_steps - reset step parents for an article being imported.  Default: 0.
 
 =back
@@ -106,6 +118,20 @@ the appropriate fields for an image
 
 =item *
 
+C<< file1_file >> .. C<< file10_file >> - name of an article file to
+attach to the article.
+
+=item *
+
+C<< file1I<N>_name >>, C<< file1I<N>_displayName >>, C<<
+file1I<N>_storage >>, C<< file1I<N>_description >>, C<<
+file1I<N>_forSale >>, C<< file1I<N>_download >>, C<<
+file1I<N>_requireUser >>, C<< file1I<N>_notes >>, C<<
+file1I<N>_hide_from_list >>, C<< file1I<N>_category >> - set other
+file fields.
+
+=item *
+
 step1 .. step10 - step parent id or linkAlias
 
 =item *
index 3ce249c..5370512 100644 (file)
@@ -4,7 +4,7 @@ use BSE::Test qw(base_url);
 use File::Spec;
 use File::Temp;
 
-use Test::More tests => 5;
+use Test::More tests => 41;
 
 BEGIN {
   unshift @INC, File::Spec->catdir(BSE::Test::base_dir(), "cgi-bin", "modules");
@@ -28,10 +28,56 @@ target=Article
 [import profile simpleupdate$when]
 map_linkAlias=1
 map_body=2
+map_file1_file=3
+map_image1_file=4
 source=CSV
 target=Article
 update_only=1
 sep_char=\\t
+file_path=t
+ignore_missing=0
+
+[import profile completefile$when]
+map_linkAlias=1
+map_file1_file=2
+map_file1_name=3
+map_file1_displayName=4
+map_file1_storage=5
+map_file1_description=6
+map_file1_forSale=7
+map_file1_download=8
+map_file1_requireUser=9
+map_file1_notes=10
+map_file1_hide_from_list=11
+skiplines=0
+file_path=t
+ignore_missing=0
+update_only=1
+source=CSV
+target=Article
+
+[import profile updatefile$when]
+map_linkAlias=1
+map_file1_name=2
+map_file1_description=3
+skiplines=0
+file_path=t
+ignore_missing=0
+update_only=1
+source=CSV
+target=Article
+
+[import profile updatefileb$when]
+map_linkAlias=1
+map_file1_name=2
+map_file1_file=3
+skiplines=0
+file_path=t/data
+ignore_missing=0
+update_only=1
+source=CSV
+target=Article
+
 CFG
 
 {
@@ -54,19 +100,111 @@ CFG
   my $testa = bse_make_article(cfg => $cfg, title => "test updates",
                               linkAlias => "alias$when");
 
-  my $fh = File::Temp->new;
-  my $filename = $fh->filename;
-  print $fh <<EOS;
-linkAlias\tbody
+  {
+    my $fh = File::Temp->new;
+    my $filename = $fh->filename;
+    print $fh <<EOS;
+linkAlias\tbody\tfile1_file\timage1_file
 "alias$when"\t"This is the body text with multiple lines
 
-Yes, multiple lines with CSV!"
+Yes, multiple lines with CSV!"\tt00smoke.t\tdata/t101.jpg
+EOS
+    close $fh;
+    my $imp = BSE::Importer->new(cfg => $cfg, profile => "simpleupdate$when", callback => sub { note @_ });
+    $imp->process($filename);
+    my $testb = Articles->getByPkey($testa->id);
+    like($testb->body, qr/This is the body/, "check the body is updated");
+    my @images = $testb->images;
+    is(@images, 1, "have an image");
+    like($images[0]->image, qr/t101\.jpg/, "check file name");
+    is(-s $images[0]->full_filename, -s "t/data/t101.jpg",
+       "check size matches source");
+
+    my @files = $testb->files;
+    is(@files, 1, "should be 1 file");
+    is($files[0]->displayName, "t00smoke.t", "check display name");
+    is(-s $files[0]->full_filename, -s "t/t00smoke.t", "check size");
+  }
+
+ SKIP:
+  {
+    my $fh = File::Temp->new;
+    my $filename = $fh->filename;
+    print $fh <<EOS;
+"alias$when",t00smoke.t,test,"A Test File.txt",local,"A test file from BSE",1,1,1,"Some Notes",1
 EOS
-  close $fh;
-  my $imp = BSE::Importer->new(cfg => $cfg, profile => "simpleupdate$when", callback => sub { note @_ });
-  $imp->process($filename);
-  my $testb = Articles->getByPkey($testa->id);
-  like($testb->body, qr/This is the body/, "check the body is updated");
+    close $fh;
+    my $imp = BSE::Importer->new(cfg => $cfg, profile => "completefile$when", callback => sub { note @_ });
+    $imp->process($filename);
+    my $testb = Articles->getByPkey($testa->id);
+
+    my ($file) = grep $_->name eq "test", $testb->files;
+    ok($file, "found the file with name 'test'")
+      or skip "File not found", 9;
+    is(-s $file->full_filename, -s "t/t00smoke.t", "check size");
+    is($file->displayName, "A Test File.txt", "displayName");
+    is($file->storage, "local", "storage");
+    is($file->description, "A test file from BSE", "description");
+    is($file->forSale, 1, "forSale");
+    is($file->download, 1, "download");
+    is($file->requireUser, 1, "requireUser");
+    is($file->notes, "Some Notes", "notes");
+    is($file->hide_from_list, 1, "hide_from_list");
+  }
+
+ SKIP:
+  {
+    my $fh = File::Temp->new;
+    my $filename = $fh->filename;
+    print $fh <<EOS;
+"alias$when",test,"New description"
+EOS
+    close $fh;
+    my $imp = BSE::Importer->new(cfg => $cfg, profile => "updatefile$when", callback => sub { note @_ });
+    $imp->process($filename);
+    my $testb = Articles->getByPkey($testa->id);
+
+    my ($file) = grep $_->name eq "test", $testb->files;
+    ok($file, "found the updated file with name 'test'")
+      or skip "File not found", 9;
+    is($file->description, "New description", "description");
+    # other fields should be unchanged
+    is(-s $file->full_filename, -s "t/t00smoke.t", "check size");
+    is($file->displayName, "A Test File.txt", "displayName");
+    is($file->storage, "local", "storage");
+    is($file->forSale, 1, "forSale");
+    is($file->download, 1, "download");
+    is($file->requireUser, 1, "requireUser");
+    is($file->notes, "Some Notes", "notes");
+    is($file->hide_from_list, 1, "hide_from_list");
+  }
+
+ SKIP:
+  {
+    my $fh = File::Temp->new;
+    my $filename = $fh->filename;
+    print $fh <<EOS;
+"alias$when",test,t101.jpg
+EOS
+    close $fh;
+    my $imp = BSE::Importer->new(cfg => $cfg, profile => "updatefileb$when", callback => sub { note @_ });
+    $imp->process($filename);
+    my $testb = Articles->getByPkey($testa->id);
+
+    my ($file) = grep $_->name eq "test", $testb->files;
+    ok($file, "found the updated file with name 'test'")
+      or skip "File not found", 9;
+    is(-s $file->full_filename, -s "t/data/t101.jpg", "check size");
+    is($file->displayName, "t101.jpg", "new displayName");
+    # other fields should be unchanged
+    is($file->storage, "local", "storage");
+    is($file->description, "New description", "description");
+    is($file->forSale, 1, "forSale");
+    is($file->download, 1, "download");
+    is($file->requireUser, 1, "requireUser");
+    is($file->notes, "Some Notes", "notes");
+    is($file->hide_from_list, 1, "hide_from_list");
+  }
 
   END {
     $testa->remove($cfg) if $testa;