allow the user to edit metadata (if the items are specified in config)
authorTony Cook <tony@develop-help.com>
Mon, 22 Feb 2010 05:00:18 +0000 (05:00 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Mon, 22 Feb 2010 05:00:18 +0000 (05:00 +0000)
17 files changed:
MANIFEST
site/cgi-bin/admin/admin.pl
site/cgi-bin/modules/BSE/Edit/Article.pm
site/cgi-bin/modules/BSE/FileHandler/Base.pm
site/cgi-bin/modules/BSE/FileHandler/Default.pm
site/cgi-bin/modules/BSE/FileHandler/FLV.pm
site/cgi-bin/modules/BSE/FileMetaMeta.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/ArticleFile.pm
site/cgi-bin/modules/BSE/TB/ArticleFileMeta.pm
site/cgi-bin/modules/BSE/TB/ArticleFiles.pm
site/cgi-bin/modules/Squirrel/Table.pm
site/cgi-bin/modules/Squirrel/Template.pm
site/data/db/sql_statements.data
site/docs/config.pod
site/htdocs/css/admin.css
site/templates/admin/file_edit.tmpl
site/templates/admin/filelist.tmpl

index 4df95d94e907ccc630a52a8318be4d2218ff1d57..e3e0db8ac3603a0f246e465b728e66ccb475880c 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -81,6 +81,7 @@ site/cgi-bin/modules/BSE/EmailRequests.pm
 site/cgi-bin/modules/BSE/FileHandler/Base.pm
 site/cgi-bin/modules/BSE/FileHandler/Default.pm
 site/cgi-bin/modules/BSE/FileHandler/FLV.pm
+site/cgi-bin/modules/BSE/FileMetaMeta.pm
 site/cgi-bin/modules/BSE/Formatter.pm
 site/cgi-bin/modules/BSE/Formatter/Article.pm
 site/cgi-bin/modules/BSE/Formatter/Subscription.pm
index 957ea26abc24014e2eb54d48a7bb7fd729dd4d6a..87ebb065815bc8952a96b1d0c304d63beb794a0d 100755 (executable)
@@ -3,8 +3,9 @@
 BEGIN { $ENV{DISPLAY} = '192.168.32.54:0.0' }
 use strict;
 use FindBin;
-use CGI::Carp 'fatalsToBrowser';
+#use CGI::Carp 'fatalsToBrowser';
 use Carp 'verbose'; # remove the 'verbose' in production
+use Carp 'confess';
 use lib "$FindBin::Bin/../modules";
 use Articles;
 use BSE::Request;
index b7d48580fd2fe5575d607670c8a31683221aeb30..946c33c964901ba934a604e63f821e1410d2100e 100644 (file)
@@ -135,6 +135,7 @@ sub article_actions {
      a_ajax_get => 'req_ajax_get',
      a_ajax_save_body => 'req_ajax_save_body',
      a_ajax_set => 'req_ajax_set',
+     a_filemeta => 'req_filemeta',
     );
 }
 
@@ -3503,12 +3504,165 @@ sub filesave {
   $self->_refresh_filelist($req, $article);
 }
 
+sub req_filemeta {
+  my ($self, $req, $article, $articles, $errors) = @_;
+
+  my $cgi = $req->cgi;
+
+  my $id = $cgi->param('file_id');
+
+  my ($file) = grep $_->{id} == $id, $self->get_files($article)
+    or return $self->edit_form($req, $article, $articles,
+                              "No such file");
+  $req->user_can(edit_files_save => $article)
+    or return $self->edit_form($req, $article, $articles,
+                              "You don't have access to save file information for this article");
+
+  my $name = $cgi->param('name');
+  $name && $name =~ /^\w+$/
+    or return $self->edit_form($req, $article, $articles,
+                              "Missing or invalid metadata name");
+
+  my $meta = $file->meta_by_name($name)
+    or return $self->edit_form($req, $article, $articles,
+                              "Metadata $name not defined for this file");
+
+  return
+    {
+     type => $meta->content_type,
+     content => $meta->value,
+    };
+}
+
 sub tag_old_checked {
   my ($errors, $cgi, $file, $key) = @_;
 
   return $errors ? $cgi->param($key) : $file->{$key};
 }
 
+sub tag_filemeta_value {
+  my ($file, $args, $acts, $funcname, $templater) = @_;
+
+  my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+    or return "* no meta name supplied *";
+
+  my $meta = $file->meta_by_name($name)
+    or return "";
+
+  $meta->content_type eq "text/plain"
+    or return "* $name has type " . $meta->content_type . " and cannot be displayed inline *";
+
+  return escape_html($meta->value);
+}
+
+sub tag_ifFilemeta_set {
+  my ($file, $args, $acts, $funcname, $templater) = @_;
+
+  my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+    or return "* no meta name supplied *";
+
+  my $meta = $file->meta_by_name($name)
+    or return 0;
+
+  return 1;
+}
+
+sub tag_filemeta_source {
+  my ($file, $args, $acts, $funcname, $templater) = @_;
+
+  my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+    or return "* no meta name supplied *";
+
+  return "$ENV{SCRIPT_NAME}?a_filemeta=1&amp;id=$file->{articleId}&amp;file_id=$file->{id}&amp;name=$name";
+}
+
+sub tag_filemeta_select {
+  my ($cgi, $allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
+
+  my $meta;
+  if ($args =~ /\S/) {
+    my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+      or return "* cannot parse *";
+    ($meta) = grep $_->name eq $name, @$allmeta
+      or return "* cannot find meta field *";
+  }
+  elsif ($$rcurr_meta) {
+    $meta = $$rcurr_meta;
+  }
+  else {
+    return "* use in filemeta iterator or supply a name *";
+  }
+
+  $meta->type eq "enum"
+    or return "* can only use filemeta_select on enum metafields *";
+
+  my %labels;
+  my @values = $meta->values;
+  @labels{@values} = $meta->labels;
+
+  my $field_name = "meta_" . $meta->name;
+  my ($def) = $cgi->param($field_name);
+  unless (defined $def) {
+    my $value = $file->meta_by_name($meta->name);
+    if ($value && $value->is_text) {
+      $def = $value->value;
+    }
+  }
+  defined $def or $def = $values[0];
+
+  return popup_menu
+    (
+     -name => $field_name,
+     -values => \@values,
+     -labels => \%labels,
+     -default => $def,
+    );
+}
+
+sub tag_filemeta_select_label {
+  my ($allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
+
+  my $meta;
+  if ($args =~ /\S/) {
+    my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+      or return "* cannot parse *";
+    ($meta) = grep $_->name eq $name, @$allmeta
+      or return "* cannot find meta field *";
+  }
+  elsif ($$rcurr_meta) {
+    $meta = $$rcurr_meta;
+  }
+  else {
+    return "* use in filemeta iterator or supply a name *";
+  }
+
+  $meta->type eq "enum"
+    or return "* can only use filemeta_select_label on enum metafields *";
+
+  my %labels;
+  my @values = $meta->values;
+  @labels{@values} = $meta->labels;
+
+  my $field_name = "meta_" . $meta->name;
+  my $value = $file->meta_by_name($meta->name);
+  if ($value) {
+    if ($value->is_text) {
+      if (exists $labels{$value->value}) {
+       return escape_html($labels{$value->value});
+      }
+      else {
+       return escape_html($value->value);
+      }
+    }
+    else {
+      return "* cannot display type " . $value->content_type . " inline *";
+    }
+  }
+  else {
+    return "* " . $meta->name . " not set *";
+  }
+}
+
 sub req_edit_file {
   my ($self, $req, $article, $articles, $errors) = @_;
 
@@ -3523,6 +3677,10 @@ sub req_edit_file {
     or return $self->edit_form($req, $article, $articles,
                               "You don't have access to save file information for this article");
 
+  my @metafields = $file->metafields($self->cfg);
+
+  my $it = BSE::Util::Iterate->new;
+  my $current_meta;
   my %acts;
   %acts =
     (
@@ -3532,6 +3690,23 @@ sub req_edit_file {
      error_img => [ \&tag_error_img, $req->cfg, $errors ],
      ifOldChecked =>
      [ \&tag_old_checked, $errors, $cgi, $file ],
+     $it->make
+     (
+      plural => "filemetas",
+      single => "filemeta",
+      data => \@metafields,
+      store => \$current_meta,
+     ),
+     filemeta_value =>
+     [ \&tag_filemeta_value, $file ],
+     ifFilemeta_set =>
+     [ \&tag_ifFilemeta_set, $file ],
+     filemeta_source =>
+     [ \&tag_filemeta_source, $file ],
+     filemeta_select =>
+     [ \&tag_filemeta_select, $cgi, \@metafields, \$current_meta, $file ],
+     filemeta_select_label =>
+     [ \&tag_filemeta_select_label, \@metafields, \$current_meta, $file ],
     );
 
   return $req->response('admin/file_edit', \%acts);
@@ -3591,6 +3766,71 @@ sub req_save_file {
     }
   }
 
+  my @meta;
+  my @meta_delete;
+  my @metafields = grep !$_->ro, $file->metafields($self->cfg);
+  my %current_meta = map { $_ => 1 } $file->metanames;
+  for my $meta (@metafields) {
+    my $name = $meta->name;
+    my $cgi_name = "meta_$name";
+    if ($cgi->param("delete_$cgi_name")) {
+      for my $metaname ($meta->metanames) {
+       push @meta_delete, $metaname
+         if $current_meta{$metaname};
+      }
+    }
+    else {
+      my $new;
+      if ($meta->is_text) {
+       my ($value) = $cgi->param($cgi_name);
+       if (defined $value && 
+           ($value =~ /\S/ || $current_meta{$meta->name})) {
+         my $error;
+         if ($meta->validate(value => $value, error => \$error)) {
+           push @meta,
+             {
+              name => $name,
+              value => $value,
+             };
+         }
+         else {
+           $errors{$cgi_name} = $error;
+         }
+       }
+      }
+      else {
+       my $im = $cgi->param($cgi_name);
+       my $up = $cgi->upload($cgi_name);
+       if (defined $im && $up) {
+         my $data = do { local $/; <$up> };
+         my ($width, $height, $type) = imgsize(\$data);
+
+         if ($width && $height) {
+           push @meta,
+             (
+              {
+               name => $meta->data_name,
+               value => $data,
+               content_type => "image/\L$type",
+              },
+              {
+               name => $meta->width_name,
+               value => $width,
+              },
+              {
+               name => $meta->height_name,
+               value => $height,
+              },
+             );
+         }
+         else {
+           $errors{$cgi_name} = $type;
+         }
+       }
+      }
+    }
+  }
+
   if ($cgi->param('save_file_flags')) {
     my $download = 0 + defined $cgi->param("download");
     if ($download ne $file->{download}) {
@@ -3681,6 +3921,13 @@ sub req_save_file {
       and $req->flash("Could not move $file->{displayName} to $storage: $@");
   }
 
+  for my $meta_delete (@meta_delete, map $_->{name}, @meta) {
+    $file->delete_meta_by_name($meta_delete);
+  }
+  for my $meta (@meta) {
+    $file->add_meta(%$meta, appdata => 1);
+  }
+
   # remove the replaced files
   if (my ($old_name, $old_storage) = @old_file) {
     $mgr->unstore($old_name, $old_storage);
index 96d56d7bc6f6cfc35141e6b6cf9693462bc46e0a..7356e23c50e07fae7ab0c6d3a8ef97c3965f0ea5 100644 (file)
@@ -2,6 +2,41 @@ package BSE::FileHandler::Base;
 use strict;
 use Carp qw(confess);
 
+=head1 NAME
+
+BSE::FileHandler::Base - base class for file handlers
+
+=head1 SYNOPSIS
+
+  my $fh = BSE::FileHandler::XXX->new($cfg);
+
+  # for derived classes to define:
+  my $cfg_section = $fh->section;
+  eval {
+    $fh->process_file($file);
+  };
+  my $html = $fh->inline($file, $parameters);
+  my $content = $fh->metacontent($file, $meta_name);
+  my @fields = $fh->metametadata;
+
+  # for derived classes to use:
+  my $cfg = $fh->cfg;
+  my $value = $fh->cfg_entry("key", $default);
+
+=head1 METHODS
+
+=over
+
+=item $class->new($cfg)
+
+Create a new file handler object.
+
+This is not connected to a single file, but should construct quickly.
+
+$cfg must be a BSE::Cfg compatible object.
+
+=cut
+
 sub new {
   my ($class, $cfg) = @_;
 
@@ -14,16 +49,43 @@ sub new {
     }, $class;
 }
 
+=item $obj->cfg
+
+Returns the $cfg value supplied to new().
+
+=cut
+
 sub cfg {
   $_[0]{cfg};
 }
 
+=item $obj->cfg_entry($key)
+
+=item $obj->cfg_entry($key, $default)
+
+Retrieve a single configuration value from the $cfg object supplied to
+new.
+
+Uses the section name returned by the section method, which the
+derived class must define.
+
+=cut
+
 sub cfg_entry {
   my ($self, $key, $def) = @_;
 
   return $self->cfg->entry($self->section, $key, $def);
 }
 
+=item $obj->process_file($file)
+
+Attempt to detect the type and extract metadata from the file
+specified by $file, a BSE::TB::ArticleFile object.
+
+If there is an error while extracting metadata, die with a message.
+
+=cut
+
 sub process_file {
   my ($self, $file) = @_;
 
@@ -31,8 +93,118 @@ sub process_file {
   confess "$class hasn't implemented process-file";
 }
 
+=item $obj->inline($file, $parameters)
+
+Called when handling:
+
+  <:filen - fieldname:>
+  <:filen name fieldname:>
+
+or:
+
+  file[id]
+  file[id|fieldname]
+
+where the $parameters field is set to fieldname.
+
+Only called when $parameters isn't "link", "meta.I<name>" or a field
+name found in an file record.
+
+The default implementation uses a template under the meta/ directory,
+constructed as:
+
+  meta/I<prefix>_I<metaname>
+
+where I<prefix> is the result of the metaprefix method.
+
+=item $obj->metacontent($file, $meta_name)
+
+Return generated meta data.
+
+=cut
+
 sub metacontent {
+  my ($self, $file, $meta_name) = @_;
+
+  require BSE::Template;
+  my %meta = map { $_->name => $_->value }
+    grep $_->content_type eq "text/plain", $file->metadata;
+
+  my @prefix = ( $self->metaprefix, "base" );
+  my $template;
+  my $found;
+  for my $prefix (@prefix) {
+    $template = "meta/${prefix}_$meta_name";
+    $found = BSE::Template->find_source($template)
+      and last;
+  }
+  unless ($found) {
+    return
+      {
+       headers =>
+       [
+       "Status: 404",
+       "Content-Type: text/plain",
+       ],
+       content =>
+       "No metadata template meta/prefix_name found"
+      };
+  }
+
+  my %acts =
+    (
+     BSE::Util::Tags->static(undef, $self->cfg),
+     meta => [ \&tag_hash, \%meta ],
+     file => [ \&tag_hash, $file ],
+     src => scalar(escape_html($file->url($self->cfg))),
+    );
+
+  return BSE::Template->get_response($template, $self->cfg, \%acts);
+}
+
+=item $obj->metametadata
+
+Return descriptions of the metadata set by $obj->process file.
+
+Should return a list of hashes, each hash can have the following keys:
+
+=over
+
+=item *
+
+name - the name of the metadata item (required).  Note: for image
+types this should be the base name of the metadata, without the
+_data/_width/_height suffix.
+
+=item *
+
+title - a descriptive name of the metadata item
+
+=item *
+
+type - one of "integer", "real", "image", "string", "text", "enum".
+If enum, I<values> should be set.  If string, I<size> should be set.
+
+=item *
+
+unit - text to display after the field, typically a unit of measure or
+a format display.
+
+=item *
+
+rules - validation rules as per DevHelp::Validate.
+
+=item *
+
+help - extended help text formatted as HTML
+
+=back
+
+=cut
+
+sub metametadata {
   return;
 }
 
 1;
+
index a4d5ec24d55fb39452efb4bd4337c2cb03511904..f75d1f83001f51a715b07362465d4f57e4a84922 100644 (file)
@@ -21,4 +21,8 @@ sub inline {
   return $html;
 }
 
+sub metaprefix {
+  "def"
+}
+
 1;
index 898ef011ae6159ccf58b40f05d4c746ced21c7b6..da3a04627df7d16e9711cff6452b529b007cbf23 100644 (file)
@@ -52,12 +52,12 @@ sub process_file {
   $file->add_meta(name => 'audio_type', value => $info{audio_type},
                  appdata => 0);
 
-  if ($self->cfg_entry("ffmpeg", 1)) {
-    my $raw_frame = $self->cfg_entry("raw_frame", 1);
+  if ($self->_have_ffmpeg) {
+    my $raw_frame = $self->_save_raw_frame;
     my $fmt = $self->cfg_entry("frame_fmt", "jpeg");
     my $content_type = $self->cfg_entry("frame_content_type", "image/$fmt");
     my $bin = $self->cfg_entry("ffmpeg_bin", "ffmpeg");
-    my @geo_names = split /,/, $self->cfg_entry("frame_thumbs", "");
+    my @geo_names = $self->_thumb_geometries;
     my @cvt_options = split /,/, $self->cfg_entry("ffmpeg_options", "");
     my $ss = 2+ rand(10);
     if ($ss > $dur_secs / 2) {
@@ -86,6 +86,12 @@ sub process_file {
                      value => $image_data,
                      content_type => $content_type,
                      appdata => 0);
+      $file->add_meta(name => "ph_width",
+                     value => $width,
+                     appdata => 0);
+      $file->add_meta(name => "ph_height",
+                     value => $height,
+                     appdata => 0);
     }
 
     if (@geo_names) {
@@ -184,24 +190,84 @@ sub inline {
   return BSE::Template->get_page($template, $self->cfg, \%acts);
 }
 
-sub metacontent {
-  my ($self, $file, $meta_name) = @_;
+sub metaprefix { "flv" }
 
-  require BSE::Template;
-  my %meta = map { $_->name => $_->value }
-    grep $_->content_type eq "text/plain", $file->metadata;
-
-  my $template = "meta/flv_$meta_name";
+sub metametadata {
+  my ($self) = @_;
 
-  my %acts =
+  my @fields =
     (
-     BSE::Util::Tags->static(undef, $self->cfg),
-     meta => [ \&tag_hash, \%meta ],
-     file => [ \&tag_hash, $file ],
-     src => scalar(escape_html($file->url($self->cfg))),
+     {
+      name => "width",
+      title => "Video Width",
+      unit => "pixels",
+      type => "integer",
+     },
+     {
+      name => "height",
+      title => "Video Height",
+      unit => "pixels",
+      type => "integer",
+     },
+     {
+      name => "duration",
+      title => "Video Duration",
+      unit => "seconds",
+      type => "real",
+     },
+     {
+      name => "duration_formatted",
+      title => "Formatted Video Duration",
+      unit => "(hh:mm:ss)",
+      type => "string",
+     },
+     {
+      name => "audio_type",
+      title => "Audio Format",
+      type => "enum",
+      values => "mono;stereo",
+      labels => "Mono;Stereo",
+     },
     );
+  if ($self->_have_ffmpeg) {
+    if ($self->_save_raw_frame) {
+      push @fields,
+       {
+        name => "ph",
+        title => "Placeholder",
+        type => "image"
+       };
+    }
+
+    for my $geo ($self->_thumb_geometries) {
+      push @fields,
+       {
+        name => "ph_${geo}",
+        title => "\u$geo thumbnail of placeholder",
+        type => "image",
+       };
+    }
+  }
+
+  return @fields;
+}
+
+sub _have_ffmpeg {
+  my $self = shift;
+
+  return $self->cfg_entry("ffmpeg", 1);
+}
+
+sub _save_raw_frame {
+  my $self = shift;
+
+  return $self->cfg_entry("raw_frame", 1);
+}
+
+sub _thumb_geometries {
+  my $self = shift;
 
-  return BSE::Template->get_response($template, $self->cfg, \%acts);
+  return split /,/, $self->cfg_entry("frame_thumbs", "")
 }
 
 1;
diff --git a/site/cgi-bin/modules/BSE/FileMetaMeta.pm b/site/cgi-bin/modules/BSE/FileMetaMeta.pm
new file mode 100644 (file)
index 0000000..c0473ee
--- /dev/null
@@ -0,0 +1,192 @@
+package BSE::FileMetaMeta;
+use strict;
+use Carp qw(confess);
+
+my %meta_rules =
+  (
+   meta_real =>
+   {
+    match => qr/^\s*[+-]?(?:\d+(?:\.\d+)|\.\d+)(?:[eE][+-]?\d+)?\s*\z/,
+    error => '$n must be a number',
+   },
+  );
+
+my %rule_map =
+  (
+   integer => "integer",
+   string => "dh_one_line",
+   real => "meta_real",
+   enum => "meta_enum", # generated
+  );
+
+sub new {
+  my $class = shift;
+  my %opts = 
+    (
+     rules => '',
+     ro => 0,
+     values => [],
+     cond => "1",
+     type => "string",
+     unit => '',
+     help => '',
+     @_
+    );
+
+  $opts{cfg} && $opts{cfg}->can("entry")
+    or confess "Missing or invalid cfg parameter";
+  $opts{name}
+    or confess "Missing name parameter";
+  $opts{name} =~ /^[a-z]\w*$/i
+    or confess "Invalid metadata name parameter";
+
+  my $name = $opts{name};
+  for my $subkey (qw/data width height/) {
+    my $key = $subkey . "_name";
+    defined $opts{$key} or $opts{$key} = $name . "_" . $subkey;
+  }
+  $opts{title} ||= $name;
+
+  if ($opts{type} eq "enum") {
+    if ($opts{values}) {
+      unless (ref $opts{values}) {
+       $opts{values} = [ split /;/, $opts{values} ];
+      }
+      @{$opts{values}}
+       or confess "$opts{name} has enum type but no values";
+    }
+    else {
+      confess "$opts{name} has enum type but no values";
+    }
+
+    if ($opts{labels}) {
+      unless (ref $opts{labels}) {
+       $opts{labels} = [ split /;/, $opts{labels} ];
+      }
+      @{$opts{labels}}
+       or confess "$opts{name} has enum type but no labels";
+    }
+    else {
+      $opts{labels} = $opts{values};
+    }
+  }
+
+  ref $opts{rules} or $opts{rules} = [ split /[,;]/, $opts{rules} ];
+
+  if ($opts{cond}) {
+    my $code = $opts{cond};
+    $opts{cond} = eval 'sub { my $file = shift; ' . $code . ' }'
+      or die "Cannot compile condition code <$code> for $opts{name}: $@";
+  }
+  else {
+    $opts{cond} = sub { 1 };
+  }
+
+  bless \%opts, $class;
+}
+
+sub name { $_[0]{name} }
+
+sub type { $_[0]{type} }
+
+sub title { $_[0]{title} }
+
+sub rules { @{$_[0]{rules}} }
+
+sub values { @{$_[0]{values}} }
+
+sub labels { @{$_[0]{labels}} }
+
+sub ro { $_[0]{ro} }
+
+sub unit { $_[0]{unit} }
+
+sub is_text {
+  $_[0]{type} ne "image";
+}
+
+sub cond {
+  my ($self, $file) = @_;
+
+  return $self->{cond}->($file);
+}
+
+sub validate {
+  my ($self, %opts) = @_;
+
+  my $value = delete $opts{value};
+  defined $value
+    or confess "value not supplied\n";
+  my $rerror = delete $opts{error}
+    or confess "error ref not supplied\n";
+
+  # kind of clumsy
+  require DevHelp::Validate;
+  my @field_rules = $self->rules;
+  $rule_map{$self->type} && unshift @field_rules, $rule_map{$self->type};
+  my %values =
+    (
+     value => $value
+    );
+  my %fields =
+    (
+     value =>
+     {
+      rules => \@field_rules,
+      description => $self->title,
+     },
+    );
+  my %rules = %meta_rules;
+  if ($self->type eq "enum") {
+    $rules{meta_enum} =
+      {
+       match => "^(?:" . join("|", map quotemeta, $self->values) . ")\\z",
+       error => '$n must be one of ' . join(", ", $self->values),
+      };
+  }
+
+  my $val = DevHelp::Validate::Hash->new
+    (
+     fields => \%fields,
+     rules => \%rules,
+     cfg => $self->{cfg},
+     section => "file metadata validation",
+    );
+  my %errors;
+  $val->validate(\%values, \%errors);
+  if (keys %errors) {
+    $$rerror = $errors{value};
+    return;
+  }
+
+  return 1;
+}
+
+sub metanames {
+  my ($self) = @_;
+
+  if ($self->type eq 'image') {
+    return ( $self->data_name, $self->width_name, $self->height_name );
+  }
+  else {
+    return $self->name;
+  }
+}
+
+sub data_name {
+  $_[0]{data_name}
+}
+
+sub width_name {
+  $_[0]{width_name}
+}
+
+sub height_name {
+  $_[0]{height_name}
+}
+
+sub keys {
+  qw/title help rules ro values labels type data_name width_name height_name cond unit/;
+}
+
+1;
index 89bf542738b49ca9b306bf0bb9abcd55c40c7a03..5eb8240e8169bfee2cc1fc7ddc0756eb290f84d5 100644 (file)
@@ -114,6 +114,12 @@ sub clear_sys_metadata {
   BSE::DB->run(bseClearArticleFileSysMetadata => $self->{id});
 }
 
+sub delete_meta_by_name {
+  my ($self, $name) = @_;
+
+  BSE::DB->run(bseDeleteArticleFileMetaByName => $self->{id}, $name);
+}
+
 sub set_handler {
   my ($self, $cfg) = @_;
 
@@ -270,4 +276,53 @@ sub apply_storage {
   }
 }
 
+=item metanames
+
+returns the names of each metadatum defined for the file.
+
+=cut
+
+sub metanames {
+  my ($self) = @_;
+
+  require BSE::TB::ArticleFileMetas;
+  return map $_->{name}, BSE::TB::ArticleFileMetas->getColumnsBy
+    (
+     [ "name" ],
+     file_id => $self->id,
+    );
+}
+
+=item metainfo
+
+Returns all but the value for metadata defined for the file.
+
+=cut
+
+sub metainfo {
+  my ($self) = @_;
+
+  require BSE::TB::ArticleFileMetas;
+  my @cols = grep $_ ne "value", BSE::TB::ArticleFileMeta->columns;
+  return BSE::TB::ArticleFileMetas->getColumnsBy
+    (
+     \@cols,
+     file_id => $self->id,
+    );
+}
+
+sub metafields {
+  my ($self, $cfg) = @_;
+
+  my %metanames = map { $_ => 1 } $self->metanames;
+
+  my @fields = grep $metanames{$_->name} || $_->cond($self), BSE::TB::ArticleFiles->all_metametadata($cfg);
+
+  my $handler = $self->handler($cfg);
+
+  my @handler_fields = map BSE::FileMetaMeta->new(%$_, ro => 1, cfg => $cfg), $handler->metametadata;
+
+  return ( @fields, @handler_fields );
+}
+
 1;
index 4a24af1f1c168aa8e1ed1bf1f5eb587ca0b806a8..d8f76c6707268237d1f06db083a0381b04c746fb 100644 (file)
@@ -15,4 +15,8 @@ sub defaults {
   appdata => 1,
 }
 
+sub is_text {
+  $_[0]->content_type eq "text/plain"
+}
+
 1;
index a18b4fb2602f227af2c23c436ba69b834797c99b..3470ef7e7fd596f84ecb7c0fb7408c2bfff35fb0 100644 (file)
@@ -69,4 +69,28 @@ sub file_manager {
   return BSE::StorageMgr::Files->new(cfg => $cfg);
 }
 
+sub all_metametadata {
+  my ($self, $cfg) = @_;
+
+  $cfg
+    or confess "Missing cfg parameter";
+
+  require BSE::FileMetaMeta;
+  my @metafields;
+  my @keys = $cfg->orderCS("global file metadata");
+  for my $name (@keys) {
+    my %opts = ( name => $name );
+    my $section = "file metadata $name";
+    for my $key (BSE::FileMetaMeta->keys) {
+      my $value = $cfg->entry($section, $key);
+      if (defined $value) {
+       $opts{$key} = $value;
+      }
+    }
+    push @metafields, BSE::FileMetaMeta->new(%opts, cfg => $cfg);
+  }
+
+  return @metafields;
+}
+
 1;
index 504667a759f937d345a42ecf83be734e1253eca8..3368b64365bc8c6a196850eb344f6024d1238e10 100644 (file)
@@ -247,6 +247,45 @@ sub _getBy_sth {
   return $sth;
 }
 
+sub getColumnsBy {
+  my ($self, $cols, %find) = @_;
+
+  my @db_cols = $self->rowClass->db_columns;
+  my @code_cols = $self->rowClass->columns;
+  my %map;
+  @map{@code_cols} = @db_cols;
+  
+  my @conds;
+  my @args;
+  for my $col (keys %find) {
+    my $db_col = $map{$col}
+      or confess "Cannot generate query: unknown column $col";
+    # this doesn't handle null, but that should use a "special"
+    push @conds, "$db_col = ?";
+    push @args, $find{$col};
+  }
+  my @result_cols = map $map{$_}, @$cols;
+
+  my $sql = "select " . join(",", @result_cols) .
+    " from " . $self->rowClass->table .
+      " where " . join(" and ", @conds);
+
+  my $sth = $dh->{dbh}->prepare($sql)
+    or confess "Cannot prepare generated $sql: ", $dh->{dbh}->errstr;
+
+  $sth->execute(@args)
+    or confess "Cannot execute $sql: ",$dh->{dbh}->errstr;
+
+  my @rows;
+  while (my $row = $sth->fetchrow_arrayref) {
+    my %row;
+    @row{@$cols} = @$row;
+    push @rows, \%row;
+  }
+
+  return wantarray ? @rows : \@rows;
+}
+
 sub getSpecial {
   my ($self, $name, @args) = @_;
 
index 5f3d0a9270ca26f8eea8501afa03f4e70365c13d..f11190e6385d01eebe893c930700672c0b17a454 100644 (file)
@@ -2,7 +2,7 @@ package Squirrel::Template;
 use vars qw($VERSION);
 use strict;
 use Carp qw/cluck confess/;
-use constant DEBUG => 0;
+use constant DEBUG => 1;
 
 $VERSION="0.09";
 
index 2facf9152338724229307ccff7ab527b571cee50..44bb61ed10fb3f3ca8ff9547709f7fb2bbb49d90 100644 (file)
@@ -213,3 +213,8 @@ delete from bse_article_file_meta
 where file_id = ? and appdata = 0
 SQL
 
+name: bseDeleteArticleFileMetaByName
+sql_statement: <<SQL
+delete from bse_article_file_meta
+where file_id = ? and name = ?
+SQL
index 5413408c7d16b17542465003b2e9ca017a3b403b..b70f3753962a0ce44040dc9dfc122bba0f317fbb 100644 (file)
@@ -1908,6 +1908,112 @@ eg.
 
 =back
 
+=head2 [global file metadata]
+
+Each key represents an item of metadata for files attached to
+articles.
+
+The values are ignored.
+
+For each key, extra information is defined in the [file metadata
+I<name>] section.
+
+=head2 [file metadata I<name>]
+
+Definition for the file metadata item I<name>.
+
+=over
+
+=item *
+
+title - descriptive name of the metadata.  Defaults to I<name>.
+
+=item *
+
+rules - validation rules, separated by ;.  Custom rules can be defined
+in [file metadata validation].
+
+=item *
+
+ro - if non-zero the metadata cannot be modified directly by the admin
+(applications can still generate it). Default: writable.
+
+=item *
+
+type - the data type of the metadata, any of string, text, enum,
+integer, real, image.  If this is enum values must be defined and
+labels should be.  Default: string.
+
+The types are:
+
+=over
+
+=item *
+
+string - single line of text
+
+=item *
+
+text - one or more lines of text
+
+=item *
+
+integer - whole number
+
+=item *
+
+real - number with decimal points
+
+=item *
+
+enum - select from a list of possible values.
+
+=item *
+
+image - image file.
+
+=back
+
+=item *
+
+values - semi-colon separated list of values for this metadata.
+
+=item *
+
+labels - semi-colon separated list of labels for the values
+
+=item *
+
+help - help html to display for the metadata
+
+=item *
+
+data_name - (images only) the key to use to store the image data.
+Default: I<name>_data.
+
+=item *
+
+width_name - (images only) the key to use to store the image width.
+Default: I<name>_width.
+
+=item *
+
+height_name - (images only) the key to use to store the image height.
+Default: I<name>_height.
+
+=item *
+
+cond - a perl expression indicating whether the metadata should be
+prompted for, for this file.  $file is the file object.  Default: 1.
+
+=item *
+
+unit - text displayed after the entry box for the metadata.  Default:
+empty.  Useful for including a unit ("pixels") or format help
+("hh:mm").
+
+=back
+
 =head1 AUTHOR
 
 Tony Cook <tony@develop-help.com>
index 3d593d51ea0279de44a6a2e205c310bc41b398e2..98b9a9672aa82a8d2face9d5799149580723b1f6 100644 (file)
@@ -260,6 +260,25 @@ img.bse_image_thumb {
   text-align: center;
 }
 
+.help_display { display: none }
+
+.help_display_trigger { 
+  position: relative;
+}
+
+.help_display_trigger:hover .help_display { 
+  display: block;
+  position: absolute;
+  width: 200px;
+  left: 0px;
+  border: 1px solid #888;
+  background-color: #fff;
+  z-index: 10;
+  padding: 2px;
+  text-align: left;
+  font-weight: normal;
+}
+
 #bse_video_stage { 
   position: fixed;
   width: 100%;
@@ -292,3 +311,6 @@ img.bse_image_thumb {
   border: 1px solid #CCC;
 }
 
+.meta_string { width: 95%; }
+.meta_real { width: 10em; }
+.meta_integer { width: 5em; }
index 8184dea3dd4db57f7af58b1e96bb74fb670b55d9..8b9c9c2e5727d367867ba0f27a69602873c16b58 100644 (file)
@@ -17,7 +17,7 @@
 <input type="hidden" name="file_id" value="<:efile id:>" />
 <input type="hidden" name="_t" value="file" />
 <input type="hidden" name="save_file_flags" value="1" />
-        <table class="editform editformsmall">
+        <table class="editform editformsmall" id="filelist">
           <tr> 
             <th align="left">Replacement file:</th>
             <td> 
             </td>
             <td nowrap="nowrap"><:help file hide_from_list:> <:error_img hide_from_list:></td>
           </tr>
+<:if Filemetas:>
+<tr>
+  <th colspan="3">File metadata</th>
+</tr>
+<:iterator begin filemetas:>
+<tr>
+  <th><:filemeta title:></th>
+  <td>
+<:if Filemeta ro:>
+<:switch:>
+<:case Match [filemeta type] "^(integer|string|real)$" |x:>
+<:filemeta_value [filemeta name] :> <:filemeta unit:>
+<:case Eq [filemeta type] "text":>
+<textarea name="meta_<:filemeta name:>" class="meta_text_ro"><:filemeta_value [filemeta name]:></textarea>
+<:case Eq [filemeta type] "enum":>
+<:filemeta_select_label:> <:filemeta unit:>
+<:case Eq [filemeta type] "image":>
+<:ifFilemeta_set [filemeta data_name]:><span class="file_display_trigger">Show
+<div class="file_display"><img src="<:filemeta_source [filemeta data_name]:>" width="<:filemeta_value [filemeta width_name]:>" height="<:filemeta_value [filemeta height_name]:>" /></div></span><:or:>(No image set)<:eif:>
+<:case default:>
+Unknown metadata type <:filemeta type:>
+<:endswitch:>
+<:or Filemeta:>
+<:switch:>
+<:case Match [filemeta type] "^(integer|string|real)$" |x:>
+<input type="text" name="meta_<:filemeta name:>" value="<:filemeta_value [filemeta name] :>" class="meta_<:filemeta type:>" /> <:filemeta unit:>
+<:case Eq [filemeta type] "text":>
+<textarea name="meta_<:filemeta name:>" class="meta_text"><:filemeta_value [filemeta name]:></textarea>
+<:case Eq [filemeta type] "enum":>
+<:filemeta_select:> <:filemeta unit:>
+<:case Eq [filemeta type] "image":>
+<input type="file" name="meta_<:filemeta name:>" />
+<:ifFilemeta_set [filemeta data_name]:><span class="file_display_trigger">Show
+<div class="file_display"><img src="<:filemeta_source [filemeta data_name]:>" width="<:filemeta_value [filemeta width_name]:>" height="<:filemeta_value [filemeta height_name]:>" /></div></span>
+<input type="checkbox" name="delete_meta_<:filemeta name:>" value="1" />&nbsp;Delete
+<:or:>(No image set)<:eif:>
+<:case default:>
+Unknown metadata type <:filemeta type:>
+<:endswitch:>
+<:if Match [filemeta type] "image":><:or Match:>
+<:ifFilemeta_set [filemeta name]:><input type="checkbox" name="delete_meta_<:filemeta name:>" value="1" />&nbsp;Delete<:or:><:eif:>
+<:eif Match:>
+<:eif Filemeta:>
+  </td>
+  <td>
+<:if Filemeta help:>
+<span class="help_display_trigger"><img src="/images/admin/help.gif" />
+<div class="help_display"><:filemeta help |z:></div></span>
+<:or Filemeta:><:eif Filemeta:>
+  </td>
+</tr>
+<:iterator end filemetas:>
+<:or Filemetas:><:eif Filemetas:>
           <tr> 
             <td colspan="3" align="right"> 
               <input type="submit" name="a_save_file" value="Save File" />
index 4dbceb561191b2ce5cfb3aa6e274279965aa4fc3..9d1590f875b0aff34ac4c0df75bcbb6e22f8f439 100644 (file)
   
 <h2>Manage files</h2>
 
-<form method="post" action="<:script:>" enctype="multipart/form-data">
+<form method="post" action="<:script:>" enctype="multipart/form-data" id="filelist">
 <input type="hidden" name="id" value="<: article id :>" />
 <input type="hidden" name="_t" value="file" />
 <input type="hidden" name="save_file_flags" value="1" />