user/group attached files
authorTony Cook <tony@develop-help.com>
Mon, 26 Oct 2009 03:37:31 +0000 (03:37 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Mon, 26 Oct 2009 03:37:31 +0000 (03:37 +0000)
47 files changed:
MANIFEST
schema/bse.sql
site/cgi-bin/bse.cfg
site/cgi-bin/modules/BSE/API.pm
site/cgi-bin/modules/BSE/AdminSiteUsers.pm
site/cgi-bin/modules/BSE/DB.pm
site/cgi-bin/modules/BSE/TB/FileAccessLog.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/FileAccessLogEntry.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/OwnedFile.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/OwnedFiles.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/SiteUserGroup.pm
site/cgi-bin/modules/BSE/TB/SiteUserGroups.pm
site/cgi-bin/modules/BSE/Template.pm
site/cgi-bin/modules/BSE/UI/User.pm
site/cgi-bin/modules/BSE/UI/UserCommon.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/UserReg.pm
site/cgi-bin/modules/BSE/Util/Tags.pm
site/cgi-bin/modules/DevHelp/Date.pm
site/cgi-bin/modules/DevHelp/Tags/Iterate.pm
site/cgi-bin/modules/DevHelp/Validate.pm
site/cgi-bin/modules/SiteUser.pm
site/cgi-bin/modules/Squirrel/Table.pm
site/data/db/sql_statements.data
site/docs/config.pod
site/htdocs/css/admin.css
site/htdocs/js/dragdrop.js
site/templates/admin/users/add_group_file.tmpl [new file with mode: 0644]
site/templates/admin/users/add_user_file.tmpl [new file with mode: 0644]
site/templates/admin/users/delete_group_file.tmpl [new file with mode: 0644]
site/templates/admin/users/delete_user_file.tmpl [new file with mode: 0644]
site/templates/admin/users/edit.tmpl
site/templates/admin/users/edit_files.tmpl [new file with mode: 0644]
site/templates/admin/users/edit_group_file.tmpl [new file with mode: 0644]
site/templates/admin/users/edit_user_file.tmpl [new file with mode: 0644]
site/templates/admin/users/fileaccess.tmpl [new file with mode: 0644]
site/templates/admin/users/groupedit.tmpl
site/templates/admin/users/groupedit_files.tmpl [new file with mode: 0644]
site/templates/admin/users/grouplist.tmpl
site/templates/admin/users/inc_add_user_file.tmpl [new file with mode: 0644]
site/templates/admin/users/inc_group_menu.tmpl [new file with mode: 0644]
site/templates/admin/users/inc_user_menu.tmpl [new file with mode: 0644]
site/templates/include/usermenu.tmpl
site/templates/user/base_userpage_files.tmpl [new file with mode: 0644]
site/templates/user/base_userpage_orders.tmpl [new file with mode: 0644]
site/templates/user/options_base.tmpl
site/util/mysql.str
t/t011dhdates.t

index 30b895d..9c87551 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -94,6 +94,7 @@ site/cgi-bin/modules/BSE/Mail/Sendmail.pm
 site/cgi-bin/modules/BSE/Message.pm
 site/cgi-bin/modules/BSE/NLFilter/SQL.pm
 site/cgi-bin/modules/BSE/Permissions.pm
+site/cgi-bin/modules/BSE/ProductImportXLS.pm
 site/cgi-bin/modules/BSE/Report.pm
 site/cgi-bin/modules/BSE/Request.pm
 site/cgi-bin/modules/BSE/Request/Base.pm
@@ -129,6 +130,8 @@ site/cgi-bin/modules/BSE/TB/AdminPerm.pm
 site/cgi-bin/modules/BSE/TB/AdminPerms.pm
 site/cgi-bin/modules/BSE/TB/AdminUser.pm
 site/cgi-bin/modules/BSE/TB/AdminUsers.pm
+site/cgi-bin/modules/BSE/TB/FileAccessLog.pm
+site/cgi-bin/modules/BSE/TB/FileAccessLogEntry.pm
 site/cgi-bin/modules/BSE/TB/Location.pm
 site/cgi-bin/modules/BSE/TB/Locations.pm
 site/cgi-bin/modules/BSE/TB/Order.pm
@@ -137,7 +140,8 @@ site/cgi-bin/modules/BSE/TB/OrderItem.pm
 site/cgi-bin/modules/BSE/TB/OrderItemOption.pm
 site/cgi-bin/modules/BSE/TB/OrderItemOptions.pm
 site/cgi-bin/modules/BSE/TB/OrderItems.pm
-site/cgi-bin/modules/BSE/ProductImportXLS.pm
+site/cgi-bin/modules/BSE/TB/OwnedFile.pm
+site/cgi-bin/modules/BSE/TB/OwnedFiles.pm
 site/cgi-bin/modules/BSE/TB/Seminar.pm
 site/cgi-bin/modules/BSE/TB/ProductOption.pm
 site/cgi-bin/modules/BSE/TB/ProductOptions.pm
@@ -180,6 +184,7 @@ site/cgi-bin/modules/BSE/UI/SubAdmin.pm
 site/cgi-bin/modules/BSE/UI/Tellafriend.pm
 site/cgi-bin/modules/BSE/UI/Thumb.pm
 site/cgi-bin/modules/BSE/UI/User.pm
+site/cgi-bin/modules/BSE/UI/UserCommon.pm
 site/cgi-bin/modules/BSE/UserReg.pm
 site/cgi-bin/modules/BSE/Util/ContentType.pm
 site/cgi-bin/modules/BSE/Util/DynSort.pm
@@ -344,6 +349,7 @@ site/htdocs/images/titles/perl.gif
 site/htdocs/images/titles/the_shop.gif
 site/htdocs/images/titles/your_site.gif
 site/htdocs/images/trans_pixel.gif
+site/htdocs/js/admin_dragdrop.js
 site/htdocs/js/admin_prodopts.js
 site/htdocs/js/builder.js
 site/htdocs/js/controls.js
@@ -376,7 +382,9 @@ site/templates/admin/edit_1.tmpl
 # site/templates/admin/edit_4.tmpl
 # site/templates/admin/edit_5.tmpl
 site/templates/admin/edit_catalog.tmpl
+site/templates/admin/edit_dragdrop.tmpl
 site/templates/admin/edit_groups.tmpl
+site/templates/admin/edit_kidsof.tmpl
 site/templates/admin/edit_prodopts.tmpl
 site/templates/admin/edit_product.tmpl
 site/templates/admin/edit_seminar.tmpl
@@ -451,14 +459,26 @@ site/templates/admin/subscr/edit.tmpl
 site/templates/admin/subscr/list.tmpl
 site/templates/admin/userlist.tmpl
 site/templates/admin/users/add.tmpl
+site/templates/admin/users/add_group_file.tmpl
+site/templates/admin/users/add_user_file.tmpl
+site/templates/admin/users/delete_group_file.tmpl
+site/templates/admin/users/delete_user_file.tmpl
 site/templates/admin/users/edit.tmpl
+site/templates/admin/users/edit_files.tmpl
 site/templates/admin/users/edit_groups.tmpl
+site/templates/admin/users/edit_group_file.tmpl
 site/templates/admin/users/edit_orders.tmpl
+site/templates/admin/users/edit_user_file.tmpl
+site/templates/admin/users/fileaccess.tmpl
 site/templates/admin/users/groupadd.tmpl
 site/templates/admin/users/groupdelete.tmpl
 site/templates/admin/users/groupedit.tmpl
+site/templates/admin/users/groupedit_files.tmpl
 site/templates/admin/users/grouplist.tmpl
 site/templates/admin/users/groupmembers.tmpl
+site/templates/admin/users/inc_add_user_file.tmpl
+site/templates/admin/users/inc_group_menu.tmpl
+site/templates/admin/users/inc_user_menu.tmpl
 site/templates/admin/users/list.tmpl
 site/templates/admin/users/list_low.tmpl
 site/templates/admin/users/view.tmpl
@@ -554,6 +574,8 @@ site/templates/user/base_cancelbooking.tmpl
 site/templates/user/base_editbooking.tmpl
 site/templates/user/base_orderdetail.tmpl
 site/templates/user/base_redirect.tmpl
+site/templates/user/base_userpage_files.tmpl
+site/templates/user/base_userpage_orders.tmpl
 site/templates/user/base_userpage_wishlist.tmpl
 site/templates/user/base_wishlist.tmpl
 site/templates/user/blacklistdone_base.tmpl
index 9d8a457..56d1df2 100644 (file)
@@ -875,3 +875,68 @@ create table bse_order_item_options (
   display_order integer not null,
   index item_order(order_item_id, display_order)
 ) type=innodb;
+
+drop table if exists bse_owned_files;
+create table bse_owned_files (
+  id integer not null auto_increment primary key,
+
+  -- owner type, either 'U' or 'G'
+  owner_type char not null,
+
+  -- siteuser_id when owner_type is 'U'
+  -- group_id when owner_type is 'G'
+  owner_id integer not null,
+
+  category varchar(20) not null,
+  filename varchar(255) not null,
+  display_name varchar(255) not null,
+  content_type varchar(80) not null,
+  download integer not null,
+  title varchar(255) not null,
+  body text not null,
+  modwhen datetime not null,
+  size_in_bytes integer not null,
+  index by_owner_category(owner_type, owner_id, category)
+);
+
+drop table if exists bse_file_subscriptions;
+create table bse_file_subscriptions (
+  id integer not null,
+  siteuser_id integer not null,
+  category varchar(20) not null,
+
+  index by_siteuser(siteuser_id),
+  index by_category(category)
+);
+
+drop table if exists bse_file_notifies;
+create table bse_file_notifies (
+  id integer not null auto_increment primary key,
+  siteuser_id integer not null,
+  file_id integer not null,
+  index by_siteuser(siteuser_id)
+);
+
+drop table if exists bse_file_access_log;
+create table bse_file_access_log (
+  id integer not null auto_increment primary key,
+  when_at datetime not null,
+  siteuser_id integer not null,
+  siteuser_logon varchar(40) not null,
+
+  file_id integer not null,
+  owner_type char not null,
+  owner_id integer not null,
+  category varchar(20) not null,
+  filename varchar(255) not null,
+  display_name varchar(255) not null,
+  content_type varchar(80) not null,
+  download integer not null,
+  title varchar(255) not null,
+  modwhen datetime not null,
+  size_in_bytes integer not null,
+
+  index by_when_at(when_at),
+  index by_file(file_id),
+  index by_user(siteuser_id, when_at)
+);
index 9977dd1..a66faa0 100644 (file)
@@ -45,6 +45,8 @@ user/unsuball.tmpl = user,user/unsuball_base.tmpl
 user/unsubone.tmpl = user,user/unsubone_base.tmpl
 user/userpage.tmpl = user,user/userpage_base.tmpl
 user/userpage_wishlist.tmpl = user,user/base_userpage_wishlist.tmpl
+user/userpage_files.tmpl = user,user/base_userpage_files.tmpl
+user/userpage_orders.tmpl = user,user/base_userpage_orders.tmpl
 user/wishlist.tmpl = user,user/base_wishlist.tmpl
 user/bookseminar.tmpl = user,user/base_bookseminar.tmpl
 user/bookconfirm.tmpl = user,user/base_bookconfirm.tmpl
index 3d52445..337f11a 100644 (file)
@@ -5,8 +5,9 @@ use BSE::Util::SQL qw(sql_datetime now_sqldatetime);
 use BSE::Cfg;
 require Exporter;
 @ISA = qw(Exporter);
-@EXPORT_OK = qw(bse_cfg bse_make_product bse_make_catalog bse_encoding bse_add_image bse_add_step_child);
+@EXPORT_OK = qw(bse_cfg bse_make_product bse_make_catalog bse_encoding bse_add_image bse_add_step_child bse_add_owned_file bse_delete_owned_file bse_replace_owned_file);
 use Carp qw(confess croak);
+use Fcntl qw(:seek);
 
 my %acticle_defaults =
   (
@@ -271,4 +272,100 @@ sub _load_editor_class {
   return BSE::Edit::Base->article_class($article, 'Articles', $cfg);
 }
 
+# File::Copy doesn't like CGI.pm's fake fhs
+sub _copy_file_from_fh {
+  my ($in_fh, $out_fh) = @_;
+
+  binmode $out_fh;
+  binmode $in_fh;
+  seek $in_fh, 0, SEEK_SET;
+  my $data;
+  local $/ = \8192;
+  while (defined ($data = <$in_fh>)) {
+    print $out_fh $data;
+  }
+
+  1;
+}
+
+sub bse_add_owned_file {
+  my ($cfg, $owner, %opts) = @_;
+
+  defined $opts{display_name} && $opts{display_name} =~ /\S/
+    or croak "bse_add_owned_file: display_name must be non-blank";
+
+  defined $opts{title} && $opts{title} =~ /\S/
+    or $opts{title} = $opts{display_name};
+  
+  unless ($opts{content_type}) {
+    require BSE::Util::ContentType;
+    $opts{content_type} = BSE::Util::ContentType::content_type($cfg, $opts{display_name});
+  }
+
+  my $file = delete $opts{file}
+    or die "No source file provided\n";;
+
+  # copy the file to the right place
+  require DevHelp::FileUpload;
+  my $file_dir = $cfg->entryVar('paths', 'downloads');
+  my $msg;
+  my ($saved_name, $out_fh) = DevHelp::FileUpload->
+    make_img_filename($file_dir, $opts{display_name}, \$msg)
+    or die "$msg\n";
+  _copy_file_from_fh($file, $out_fh)
+    or die "$!\n";
+  unless (close $out_fh) {
+    die "Error saving file: $!\n";
+  }
+
+  $opts{owner_type} = $owner->file_owner_type;
+  $opts{size_in_bytes} = -s "$file_dir/$saved_name";
+  $opts{owner_id} = $owner->id;
+  $opts{category} ||= '';
+  $opts{filename} = $saved_name;
+  
+  require BSE::TB::OwnedFiles;
+  return BSE::TB::OwnedFiles->make(%opts);
+}
+
+sub bse_delete_owned_file {
+  my ($cfg, $owned_file) = @_;
+
+  my $file_dir = $cfg->entryVar('paths', 'downloads');
+  unlink "$file_dir/$owned_file->{filename}";
+  $owned_file->remove;
+}
+
+sub bse_replace_owned_file {
+  my ($cfg, $owned_file, %opts) = @_;
+
+  my $file_dir = $cfg->entryVar('paths', 'downloads');
+  my $old_name;
+  if ($opts{file}) {
+    my $msg;
+    require DevHelp::FileUpload;
+    my ($saved_name, $out_fh) = DevHelp::FileUpload->
+      make_img_filename($file_dir, $opts{display_name}, \$msg)
+       or die "$msg\n";
+    _copy_file_from_fh($opts{file}, $out_fh)
+       or die "$!\n";
+    unless (close $out_fh) {
+      die "Error saving file: $!\n";
+    }
+    $old_name = $owned_file->{filename};
+    $owned_file->{filename} = $saved_name;
+    $owned_file->{size_in_bytes} = -s "$file_dir/$saved_name";
+  }
+
+  for my $field (qw/category display_name content_type download title body modwhen size_in_bytes/) {
+    defined $opts{$field}
+      and $owned_file->{$field} = $opts{$field};
+  }
+  $owned_file->save;
+  $old_name
+    and unlink "$file_dir/$old_name";
+
+  1;
+}
+
 1;
index e57febd..ada13ee 100644 (file)
@@ -2,7 +2,7 @@ package BSE::AdminSiteUsers;
 use strict;
 use base qw(BSE::UI::AdminDispatch BSE::UI::SiteuserCommon);
 use BSE::Util::Tags qw(tag_error_img tag_hash);
-use DevHelp::HTML;
+use DevHelp::HTML qw(:default popup_menu);
 use SiteUsers;
 use BSE::Util::Iterate;
 use BSE::Util::DynSort qw(sorter tag_sorthelp);
@@ -11,6 +11,7 @@ use BSE::SubscriptionTypes;
 use BSE::CfgInfo qw(custom_class);
 use constant SITEUSER_GROUP_SECT => 'BSE Siteuser groups validation';
 use BSE::Template;
+use DevHelp::Date qw(dh_parse_date_sql dh_parse_time_sql);
 
 my %actions =
   (
@@ -30,6 +31,20 @@ my %actions =
    groupmemberform   => 'bse_members_user_edit',
    savegroupmembers  => 'bse_members_user_edit',
    confirm           => 'bse_members_confirm',
+   adduserfile       => 'bse_members_user_add_file',
+   adduserfileform   => 'bse_members_user_add_file',
+   edituserfile      => 'bse_members_user_edit_file',
+   saveuserfile      => 'bse_members_user_edit_file',
+   deluserfileform   => 'bse_members_user_del_file',
+   deluserfile       => 'bse_members_user_del_file',
+
+   addgroupfile      => 'bse_members_group_add_file',
+   addgroupfileform  => 'bse_members_group_add_file',
+   editgroupfile     => 'bse_members_group_edit_file',
+   savegroupfile     => 'bse_members_group_edit_file',
+   delgroupfileform  => 'bse_members_group_del_file',
+   delgroupfile      => 'bse_members_group_del_file',
+   fileaccesslog     => 'bse_members_file_log',
   );
 
 my @donttouch = qw(id userId password email confirmed confirmSecret waitingForConfirmation flags affiliate_name previousLogon); # flags is saved separately
@@ -232,18 +247,24 @@ sub _display_user {
     $msg = $req->message($errors);
   }
 
+  require BSE::TB::OwnedFiles;
+  my @file_cats = BSE::TB::OwnedFiles->categories($req->cfg);
+  my %subbed = map { $_ => 1 } $siteuser->subscribed_file_categories;
+  for my $cat (@file_cats) {
+    $cat->{subscribed} = exists $subbed{$cat->{id}} ? 1 : 0;
+  }
+
   my @subs = grep $_->{visible}, BSE::SubscriptionTypes->all;
   my $sub_index;
   require BSE::SubscribedUsers;
   my @usersubs = BSE::SubscribedUsers->getBy(userId=>$siteuser->{id});
   my %usersubs = map { $_->{subId}, $_ } @usersubs;
   my $current_group;
+  my $current_file;
   my %acts;
   %acts =
     (
-     BSE::Util::Tags->basic(undef, $req->cgi, $req->cfg),
-     BSE::Util::Tags->secure($req),
-     BSE::Util::Tags->admin(undef, $req->cfg),
+     $req->admin_tags,
      message => $msg,
      siteuser => [ \&tag_hash, $siteuser ],
      error_img => [ \&tag_error_img, $req->cfg, $errors ],
@@ -262,11 +283,48 @@ sub _display_user {
      ifMember => [ \&tag_ifUserMember, $siteuser, \$current_group ],
      $it->make_iterator([ \&iter_seminar_bookings, $siteuser],
                        'booking', 'bookings'),
+     $it->make
+     (
+      code => [ files => $siteuser ],
+      single => "userfile",
+      plural => "userfiles",
+      store => \$current_file,
+     ),
+     userfile_category => [ tag_userfile_category => $class, $req, \$current_file ],
+     $it->make
+     (
+      data => \@file_cats,
+      single => "filecat",
+      plural => "filecats"
+     ),
     );  
 
   return $req->dyn_response($template, \%acts);
 }
 
+sub tag_userfile_category {
+  my ($self, $req, $rfile) = @_;
+
+  my ($current) = $req->cgi->param("category");
+  unless (defined $current) {
+    if ($rfile && $$rfile) {
+      $current = $$rfile->category;
+    }
+  }
+  defined $current
+    or $current = "";
+
+  require BSE::TB::OwnedFiles;
+  my @all = BSE::TB::OwnedFiles->categories($req->cfg);
+  return popup_menu
+    (
+     -name => "category",
+     -default => $current,
+     -values => [ map $_->{id}, @all ],
+     -labels => { map { $_->{id} => $_->{name} } @all },
+    );
+}
+
 sub iter_seminar_bookings {
   my ($siteuser) = @_;
 
@@ -429,6 +487,11 @@ sub req_save {
     $class->save_subs($req, $user);
   }
 
+  if ($cgi->param('save_file_subs')) {
+    my @new_subs = $cgi->param("file_subscriptions");
+    $user->set_subscribed_file_categories($cfg, @new_subs);
+  }
+
   $custom->siteusers_changed($cfg);
   $custom->can('siteuser_edit')
     and $custom->siteuser_edit($user, 'admin', $cfg);
@@ -746,10 +809,17 @@ sub _get_group {
   my ($req, $msg) = @_;
 
   my $id = $req->cgi->param('id');
-  defined $id && $id =~ /^\d+$/
+  defined $id && $id =~ /^-?\d+$/
     or do { $$msg = "Missing or invalid group id"; return };
+
+  my $group;
   require BSE::TB::SiteUserGroups;
-  my $group = BSE::TB::SiteUserGroups->getByPkey($id);
+  if ($id < 0) {
+    $group = BSE::TB::SiteUserGroups->getQueryGroup($req->cfg, $id);
+  }
+  else {
+    $group = BSE::TB::SiteUserGroups->getByPkey($id);
+  }
   $group
     or do { $$msg = "Unknown group id"; return };
 
@@ -760,7 +830,7 @@ sub req_grouplist {
   my ($class, $req, $errors) = @_;
 
   require BSE::TB::SiteUserGroups;
-  my @groups = BSE::TB::SiteUserGroups->all;
+  my @groups = BSE::TB::SiteUserGroups->admin_and_query_groups($req->cfg);
 
   my $msg = $req->message($errors);
 
@@ -865,7 +935,8 @@ sub _common_group {
     or return $class->req_grouplist($req, { id=> $msg });
 
   $msg = $req->message($errors);
-
+  my $it = BSE::Util::Iterate->new;
+  my $current_file;
   my %acts;
   %acts =
     (
@@ -876,6 +947,13 @@ sub _common_group {
      message=>$msg,
      error_img => [ \&tag_error_img, $req->cfg, $errors ],
      group => [ \&tag_hash, $group ],
+     $it->make
+     (
+      code => [ files => $group ],
+      single => "groupfile",
+      plural => "groupfiles",
+      store => \$current_file,
+     ),
     );
 
   return $req->dyn_response($template, \%acts);
@@ -1005,4 +1083,852 @@ sub req_confirm {
   return BSE::Template->get_refresh($r, $req->cfg);
 }
 
+my %file_fields =
+  (
+   content_type => 
+   {
+    description => "Content type",
+    rules => "dh_one_line",
+   },
+   category =>
+   {
+    description => "Category",
+    rules => "dh_one_line",
+   },
+   modwhen_date =>
+   {
+    description => "Last Modified date",
+    rules => "date",
+    requried_if => "modwhen_time",
+   },
+   modwhen_time =>
+   {
+    description => "Last Modified time",
+    rules => "time",
+    required_if => "modwhen_date",
+   },
+   title =>
+   {
+    description => "Title",
+    rules => "dh_one_line",
+   },
+   body =>
+   {
+    description => "Body",
+   },
+  );
+
+my %save_file_fields =
+  (
+   content_type => 
+   {
+    description => "Content type",
+    rules => "dh_one_line",
+   },
+   category =>
+   {
+    description => "Category",
+    rules => "dh_one_line",
+   },
+   modwhen_date =>
+   {
+    description => "Last Modified date",
+    rules => "date",
+    requried => 1,
+   },
+   modwhen_time =>
+   {
+    description => "Last Modified time",
+    rules => "time",
+    required => 1,
+   },
+   title =>
+   {
+    description => "Title",
+    rules => "dh_one_line",
+    required => 1,
+   },
+   body =>
+   {
+    description => "Body",
+   },
+  );
+
+sub req_adduserfileform {
+  my ($self, $req, $errors) = @_;
+
+  my $msg;
+  my $siteuser = _get_user($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my %acts =
+    (
+     $req->admin_tags,
+     message => $msg,
+     siteuser => [ \&tag_hash, $siteuser ],
+     error_img => [ \&tag_error_img, $req->cfg, $errors ],
+     userfile_category => [ tag_userfile_category => $self, $req, undef ],
+    );
+
+  return $req->dyn_response("admin/users/add_user_file", \%acts);
+}
+
+sub req_adduserfile {
+  my ($self, $req) = @_;
+
+  my $msg;
+  my $user = _get_user($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $cgi = $req->cgi;
+
+  $req->check_csrf("admin_user_add_file")
+    or return $self->csrf_error($req, "admin_user_add_file", "Add Member File");
+
+  my %errors;
+  $req->validate(fields => \%file_fields,
+                errors => \%errors);
+
+  my $file = $cgi->param("file");
+  my $file_fh = $cgi->upload("file");
+  unless ($file) {
+    $errors{file} = "Please select a file";
+  }
+  if ($file && -z $file) {
+    $errors{file} = "File is empty";
+  }
+  if (!$errors{$file} && !$file_fh) {
+    $errors{file} = "Something is wrong with the upload form or your file wasn't found";
+  }
+
+  keys %errors
+    and return $self->req_adduserfileform($req, undef, \%errors);
+
+  require BSE::API;
+  BSE::API->import("bse_add_owned_file");
+
+  my %file;
+  $file{file} = $file_fh;
+  for my $field (qw/content_type category title body/) {
+    my ($value) = $cgi->param($field);
+    defined $value or $value = "";
+    $file{$field} = $value;
+  }
+  $file{download} = $cgi->param('download') ? 1 : 0;
+  my $mod_date = $cgi->param("modwhen_date");
+  my $mod_time = $cgi->param("modwhen_time");
+  if ($mod_date && $mod_time) {
+    $file{modwhen} = dh_parse_date_sql($mod_date) . " " 
+      . dh_parse_time_sql($mod_time);
+  }
+  $file{display_name} = $file . "";
+  my $upload_info = $cgi->uploadInfo($file);
+# some content types come through strangely
+#  if (!$file{content_type} && $upload_info->{"Content-Type"}) {
+#    $file{content_type} = $upload_info->{"Content-Type"}
+#  }
+  for my $key (keys %$upload_info) {
+    print STDERR "uploadinfo: $key: $upload_info->{$key}\n";
+  }
+  local $SIG{__DIE__};
+  my $owned_file = eval { bse_add_owned_file($req->cfg, $user, %file) };
+  unless ($owned_file) {
+    $errors{file} = $@;
+    return $self->req_edit($req, undef, \%errors);
+  }
+
+  my $r = $cgi->param('r');
+  unless ($r) {
+    $r = $req->url('siteusers', { a_edit => 1, _t => "files", id => $user->id, m => "File created" });
+  }
+
+  return BSE::Template->get_refresh($r, $req->cfg);
+}
+
+sub _get_user_file {
+  my ($req, $user, $msg) = @_;
+
+  my $file_id = $req->cgi->param("file_id");
+  unless (defined $file_id && $file_id =~ /^\d+$/) {
+    $$msg = "Missing or invalid file id";
+    return;
+  }
+  require BSE::TB::OwnedFiles;
+  my ($file) = BSE::TB::OwnedFiles->getBy
+    (
+     owner_type => $user->file_owner_type,
+     owner_id => $user->id,
+     id => $file_id
+    );
+  unless ($file) {
+    $$msg = "No such file found";
+    return;
+  }
+
+  return $file;
+}
+
+sub _show_userfile {
+  my ($self, $req, $template, $siteuser, $file, $errors) = @_;
+
+  my $message = $req->message($errors);
+
+  my %acts =
+    (
+     $req->admin_tags,
+     userfile => [ \&tag_hash, $file ],
+     message => $message,
+     siteuser => [ \&tag_hash, $siteuser ],
+     error_img => [ \&tag_error_img, $req->cfg, $errors ],
+     userfile_category => [ tag_userfile_category => $self, $req, \$file ],
+    );
+
+  return $req->dyn_response($template, \%acts);
+}
+
+sub req_edituserfile {
+  my ($self, $req, $errors) = @_;
+
+  my $msg;
+  my $siteuser = _get_user($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_user_file($req, $siteuser, \$msg)
+    or return $self->req_list($req, $msg);
+
+  return $self->_show_userfile($req, "admin/users/edit_user_file", $siteuser, $file, $errors);
+}
+
+sub req_deluserfileform {
+  my ($self, $req, $errors) = @_;
+
+  my $msg;
+  my $siteuser = _get_user($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_user_file($req, $siteuser, \$msg)
+    or return $self->req_list($req, $msg);
+
+  return $self->_show_userfile($req, "admin/users/delete_user_file", $siteuser, $file, $errors);
+}
+
+sub req_saveuserfile {
+  my ($self, $req) = @_;
+
+  $req->check_csrf("admin_user_edit_file")
+    or return $self->csrf_error($req, "admin_user_edit_file", "Edit Member File");
+
+  my $msg;
+  my $siteuser = _get_user($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_user_file($req, $siteuser, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my %errors;
+  $req->validate(fields => \%file_fields,
+                errors => \%errors);
+
+  my %changes;
+  my $cgi = $req->cgi;
+  my $new_file = $cgi->param("file");
+  my $new_fh = $cgi->upload("file");
+
+  if ($new_file) {
+    if (!$new_fh) {
+      $errors{file} = "Something is wrong with the upload form or your file wasn't found";
+    }
+  }
+  unless ($errors{file}) {
+    -z $new_file
+      and $errors{file} = "File is empty";
+  }
+
+  keys %errors
+    and return $self->req_edituserfile($req, \%errors);
+
+  for my $field (qw/content_type category title body/) {
+    my ($value) = $cgi->param($field);
+    defined $value
+      and $changes{$field} = $value;
+  }
+  if ($new_file && $new_fh) {
+    $changes{file} = $new_fh;
+    $changes{display_name} = $new_file;
+    my $upload_info = $cgi->uploadInfo($new_file);
+# some content types come through strangely
+#    if (!$changes{content_type} && $upload_info->{"Content-Type"}) {
+#      $changes{content_type} = $upload_info->{"Content-Type"}
+#    }
+  }
+  if (defined $changes{content_type} && !$changes{content_type} =~ /\S/) {
+    $errors{content_type} = "Content type must be set";
+  }
+  $changes{download} = $cgi->param('download') ? 1 : 0;
+  my $mod_date = $cgi->param("modwhen_date");
+  my $mod_time = $cgi->param("modwhen_time");
+  if ($mod_date && $mod_time) {
+    $changes{modwhen} = dh_parse_date_sql($mod_date) . " " 
+      . dh_parse_time_sql($mod_time);
+  }
+
+  require BSE::API;
+  BSE::API->import("bse_replace_owned_file");
+  my $good = eval { bse_replace_owned_file($req->cfg, $file, %changes); };
+
+  $good
+    or return $self->req_edituserfile($req, { _ => $@ });
+  
+  my $r = $cgi->param('r');
+  unless ($r) {
+    $r = $req->url('siteusers', { a_edit => 1, _t => "files", id => $siteuser->id, m => "File saved" });
+  }
+
+  return BSE::Template->get_refresh($r, $req->cfg);
+}
+
+sub req_deluserfile {
+  my ($self, $req) = @_;
+
+  $req->check_csrf("admin_user_del_file")
+    or return $self->csrf_error($req, "admin_user_del_file", "Delete Member File");
+
+  my $msg;
+  my $siteuser = _get_user($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_user_file($req, $siteuser, \$msg)
+    or return $self->req_list($req, $msg);
+
+  require BSE::API;
+  BSE::API->import("bse_delete_owned_file");
+  my $good = eval { bse_delete_owned_file($req->cfg, $file); };
+
+  $good
+    or return $self->req_deluserfileform($req, { _ => $@ });
+
+  my $r = $req->cgi->param('r');
+  unless ($r) {
+    $r = $req->url('siteusers', { a_edit => 1, _t => "files", id => $siteuser->id, m => "File removed" });
+  }
+
+  return BSE::Template->get_refresh($r, $req->cfg);
+}
+
+sub req_addgroupfileform {
+  my ($self, $req, $errors) = @_;
+
+  my $msg;
+  my $group = _get_group($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my %acts =
+    (
+     $req->admin_tags,
+     message => $msg,
+     group => [ \&tag_hash, $group ],
+     error_img => [ \&tag_error_img, $req->cfg, $errors ],
+     userfile_category => [ tag_userfile_category => $self, $req, undef ],
+    );
+
+  return $req->dyn_response("admin/users/add_group_file", \%acts);
+}
+
+sub req_addgroupfile {
+  my ($self, $req) = @_;
+
+  my $msg;
+  my $group = _get_group($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $cgi = $req->cgi;
+
+  $req->check_csrf("admin_group_add_file")
+    or return $self->csrf_error($req, "admin_group_add_file", "Add Member File");
+
+  my %errors;
+  $req->validate(fields => \%file_fields,
+                errors => \%errors);
+
+  my $file = $cgi->param("file");
+  my $file_fh = $cgi->upload("file");
+  unless ($file) {
+    $errors{file} = "Please select a file";
+  }
+  if ($file && -z $file) {
+    $errors{file} = "File is empty";
+  }
+  if (!$errors{$file} && !$file_fh) {
+    $errors{file} = "Something is wrong with the upload form or your file wasn't found";
+  }
+
+  keys %errors
+    and return $self->req_addgroupfileform($req, undef, \%errors);
+
+  require BSE::API;
+  BSE::API->import("bse_add_owned_file");
+
+  my %file;
+  $file{file} = $file_fh;
+  for my $field (qw/content_type category title body/) {
+    my ($value) = $cgi->param($field);
+    defined $value or $value = "";
+    $file{$field} = $value;
+  }
+  $file{download} = $cgi->param('download') ? 1 : 0;
+  my $mod_date = $cgi->param("modwhen_date");
+  my $mod_time = $cgi->param("modwhen_time");
+  if ($mod_date && $mod_time) {
+    $file{modwhen} = dh_parse_date_sql($mod_date) . " " 
+      . dh_parse_time_sql($mod_time);
+  }
+  $file{display_name} = $file . "";
+  my $upload_info = $cgi->uploadInfo($file);
+# some content types come through strangely
+#  if (!$file{content_type} && $upload_info->{"Content-Type"}) {
+#    $file{content_type} = $upload_info->{"Content-Type"}
+#  }
+  for my $key (keys %$upload_info) {
+    print STDERR "uploadinfo: $key: $upload_info->{$key}\n";
+  }
+  local $SIG{__DIE__};
+  my $owned_file = eval { bse_add_owned_file($req->cfg, $group, %file) };
+  unless ($owned_file) {
+    $errors{file} = $@;
+    return $self->req_edit($req, undef, \%errors);
+  }
+
+  my $r = $cgi->param('r');
+  unless ($r) {
+    $r = $req->url('siteusers', { a_editgroup => 1, _t => "files", id => $group->id, m => "File created" });
+  }
+
+  return BSE::Template->get_refresh($r, $req->cfg);
+}
+
+sub _get_group_file {
+  my ($req, $group, $msg) = @_;
+
+  my $file_id = $req->cgi->param("file_id");
+  unless (defined $file_id && $file_id =~ /^\d+$/) {
+    $$msg = "Missing or invalid file id";
+    return;
+  }
+  require BSE::TB::OwnedFiles;
+  my ($file) = BSE::TB::OwnedFiles->getBy
+    (
+     owner_type => $group->file_owner_type,
+     owner_id => $group->id,
+     id => $file_id
+    );
+  unless ($file) {
+    $$msg = "No such file found";
+    return;
+  }
+
+  return $file;
+}
+
+sub _show_groupfile {
+  my ($self, $req, $template, $group, $file, $errors) = @_;
+
+  my $message = $req->message($errors);
+
+  my %acts =
+    (
+     $req->admin_tags,
+     groupfile => [ \&tag_hash, $file ],
+     message => $message,
+     group => [ \&tag_hash, $group ],
+     error_img => [ \&tag_error_img, $req->cfg, $errors ],
+     userfile_category => [ tag_userfile_category => $self, $req, \$file ],
+    );
+
+  return $req->dyn_response($template, \%acts);
+}
+
+sub req_editgroupfile {
+  my ($self, $req, $errors) = @_;
+
+  my $msg;
+  my $group = _get_group($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_group_file($req, $group, \$msg)
+    or return $self->req_list($req, $msg);
+
+  return $self->_show_groupfile($req, "admin/users/edit_group_file", $group, $file, $errors);
+}
+
+sub req_delgroupfileform {
+  my ($self, $req, $errors) = @_;
+
+  my $msg;
+  my $group = _get_group($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_group_file($req, $group, \$msg)
+    or return $self->req_list($req, $msg);
+
+  return $self->_show_groupfile($req, "admin/users/delete_group_file", $group, $file, $errors);
+}
+
+sub req_savegroupfile {
+  my ($self, $req) = @_;
+
+  $req->check_csrf("admin_group_edit_file")
+    or return $self->csrf_error($req, "admin_group_edit_file", "Edit Member File");
+
+  my $msg;
+  my $group = _get_group($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_group_file($req, $group, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my %errors;
+  $req->validate(fields => \%file_fields,
+                errors => \%errors);
+
+  my %changes;
+  my $cgi = $req->cgi;
+  my $new_file = $cgi->param("file");
+  my $new_fh = $cgi->upload("file");
+
+  if ($new_file) {
+    if (!$new_fh) {
+      $errors{file} = "Something is wrong with the upload form or your file wasn't found";
+    }
+  }
+  unless ($errors{file}) {
+    -z $new_file
+      and $errors{file} = "File is empty";
+  }
+
+  keys %errors
+    and return $self->req_editgroupfile($req, \%errors);
+
+  for my $field (qw/content_type category title body/) {
+    my ($value) = $cgi->param($field);
+    defined $value
+      and $changes{$field} = $value;
+  }
+  if ($new_file && $new_fh) {
+    $changes{file} = $new_fh;
+    $changes{display_name} = $new_file;
+    my $upload_info = $cgi->uploadInfo($new_file);
+# some content types come through strangely
+#    if (!$changes{content_type} && $upload_info->{"Content-Type"}) {
+#      $changes{content_type} = $upload_info->{"Content-Type"}
+#    }
+  }
+  if (defined $changes{content_type} && !$changes{content_type} =~ /\S/) {
+    $errors{content_type} = "Content type must be set";
+  }
+  $changes{download} = $cgi->param('download') ? 1 : 0;
+  my $mod_date = $cgi->param("modwhen_date");
+  my $mod_time = $cgi->param("modwhen_time");
+  if ($mod_date && $mod_time) {
+    $changes{modwhen} = dh_parse_date_sql($mod_date) . " " 
+      . dh_parse_time_sql($mod_time);
+  }
+
+  require BSE::API;
+  BSE::API->import("bse_replace_owned_file");
+  my $good = eval { bse_replace_owned_file($req->cfg, $file, %changes); };
+
+  $good
+    or return $self->req_editgroupfile($req, { _ => $@ });
+  
+  my $r = $cgi->param('r');
+  unless ($r) {
+    $r = $req->url('siteusers', { a_editgroup => 1, _t => "files", id => $group->id, m => "File saved" });
+  }
+
+  return BSE::Template->get_refresh($r, $req->cfg);
+}
+
+sub req_delgroupfile {
+  my ($self, $req) = @_;
+
+  $req->check_csrf("admin_group_del_file")
+    or return $self->csrf_error($req, "admin_group_del_file", "Delete Member File");
+
+  my $msg;
+  my $group = _get_group($req, \$msg)
+    or return $self->req_list($req, $msg);
+
+  my $file = _get_group_file($req, $group, \$msg)
+    or return $self->req_list($req, $msg);
+
+  require BSE::API;
+  BSE::API->import("bse_delete_owned_file");
+  my $good = eval { bse_delete_owned_file($req->cfg, $file); };
+
+  $good
+    or return $self->req_delgroupfileform($req, { _ => $@ });
+
+  my $r = $req->cgi->param('r');
+  unless ($r) {
+    $r = $req->url('siteusers', { a_editgroup => 1, _t => "files", id => $group->id, m => "File removed" });
+  }
+
+  return BSE::Template->get_refresh($r, $req->cfg);
+}
+
+sub _get_user {
+  my ($req, $msg) = @_;
+
+  my $id = $req->cgi->param('id');
+  defined $id && $id =~ /^\d+$/
+    or do { $$msg = "Missing or invalid user id"; return };
+  require BSE::TB::SiteUserGroups;
+  my $group = SiteUsers->getByPkey($id);
+  $group
+    or do { $$msg = "Unknown user id"; return };
+
+  $group;
+}
+
+sub csrf_error {
+  my ($self, $req, $name, $description) = @_;
+
+  my %errors;
+  my $msg = $req->csrf_error;
+  $errors{_csrfp} = $msg;
+  return $self->req_list($req, "$description: $msg ($name)");
+}
+
+sub tag_page_args {
+  my ($self, $page_args, $args) = @_;
+
+  my %args = %$page_args;
+  if ($args) {
+    delete @args{split ' ', $args};
+  }
+
+  return join "&amp;", map { "$_=" . escape_uri($args{$_}) } keys %args;
+}
+
+sub tag_page_argsh {
+  my ($self, $page_args, $args) = @_;
+
+  my %args = %$page_args;
+  if ($args) {
+    delete @args{split ' ', $args};
+  }
+
+  return join "", map 
+    { 
+      my $value = escape_html($args{$_});
+      qq(<input type="hidden" name="$_" value="$value" />);
+    } keys %args;
+}
+
+sub tag_fileaccess_user {
+  my ($rcurrent, $cache) = @_;
+  
+  $$rcurrent
+    or return '';
+  my $id = $$rcurrent->siteuser_id;
+  exists $cache->{$id}
+    or $cache->{$id} = SiteUsers->getByPkey($id);
+
+  $cache->{$id}
+    or return "** No user $id";
+
+  return escape_html($cache->{$id}->userId);
+}
+
+sub tag_ifFileuser {
+  my ($rcurrent, $cache) = @_;
+  
+  $$rcurrent
+    or return '';
+  my $id = $$rcurrent->siteuser_id;
+  exists $cache->{$id}
+    or $cache->{$id} = SiteUsers->getByPkey($id);
+
+  return defined $cache->{$id};
+}
+
+sub _find_file_owner {
+  my ($owner_type, $owner_id, $cfg, $cache) = @_;
+
+  require BSE::TB::SiteUserGroups;
+  my $owner;
+  if ($owner_type eq SiteUser->file_owner_type) {
+    if ($cache->{$owner_id} ||= SiteUsers->getByPkey($owner_id)) {
+      $owner = $cache->{$owner_id}->data_only;
+      $owner->{desc} = "User: " . $owner->{userId};
+    }
+    else {
+      return;
+    }
+  }
+  elsif ($owner_type eq BSE::TB::SiteUserGroup->file_owner_type) {
+    my $group;
+    if ($owner_id < 0) {
+      $group = BSE::TB::SiteUserGroups->getQueryGroup($cfg, $owner_id);
+    }
+    else {
+      $group = BSE::TB::SiteUserGroups->getByPkey($owner_id);
+    }
+    $group
+      or return;
+    $owner = $group->data_only;
+    $owner->{desc} = "Group: " . $group->{name};
+  }
+  else {
+    print STDERR "** Unknown file owner type $owner_type\n";
+    return;
+  }
+
+  return $owner;
+}
+
+sub tag_fileowner {
+  my ($rcurrent, $cache, $cfg, $args) = @_;
+
+  $$rcurrent or return "";
+
+  my $owner = _find_file_owner($$rcurrent->{owner_type}, $$rcurrent->{owner_id}, $cfg, $cache)
+    or return "Unknown";
+
+  return tag_hash($owner, $args);
+}
+
+sub tag_filecat {
+  my ($rcurrent, $cats) = @_;
+
+  $$rcurrent
+    or return '';
+
+  $cats->{$$rcurrent->{category}};
+}
+
+sub req_fileaccesslog {
+  my ($self, $req) = @_;
+
+  my @filters;
+  my $cgi = $req->cgi;
+  my %page_args;
+  my $file_id = $cgi->param("file_id");
+  my $file;
+  if ($file_id && $file_id =~ /^\d+$/) {
+    require BSE::TB::OwnedFiles;
+    $file = BSE::TB::OwnedFiles->getByPkey($file_id);
+    if ($file) {
+      push @filters, [ '=', file_id => $file_id ];
+      $page_args{file_id} = $file_id;
+    }
+  }
+  my $siteuser_id = $cgi->param('siteuser_id');
+  my $user;
+  if ($siteuser_id && $siteuser_id =~ /^\d+$/) {
+    $user = SiteUsers->getByPkey($siteuser_id);
+    if ($user) {
+      push @filters, [ '=', siteuser_id => $siteuser_id ];
+      $page_args{siteuser_id} = $siteuser_id;
+    }
+  }
+  my $owner_id = $cgi->param("owner_id");
+  my $owner_type = $cgi->param("owner_type") || "U";
+  my $owner;
+  my $owner_desc = '';
+  my %user_cache;
+  if (defined $owner_id) {
+    push @filters,
+      (
+       [ '=', owner_id => $owner_id ],
+       [ '=', owner_type => $owner_type ],
+      );
+    $owner = _find_file_owner($owner_type, $owner_id, $req->cfg, \%user_cache);
+    if ($owner) {
+      $owner_desc = $owner->{desc};
+    }
+    if ($owner) {
+      $page_args{owner_type} = $owner_type;
+      $page_args{owner_id} = $owner_id;
+    }
+  }
+
+  require BSE::TB::OwnedFiles;
+  my %categories = map { $_->{id} => escape_html($_->{name}) } 
+    BSE::TB::OwnedFiles->categories($req->cfg);
+
+  my $category_id = $cgi->param("category");
+  my $category;
+  if (defined $category_id && $categories{$category_id}) {
+    $category = $categories{$category_id};
+    push @filters,
+      [ "=", category => $category_id ];
+    $page_args{category} = $category_id;
+  }
+  use POSIX qw(strftime);
+  my %errors;
+  my $from = $cgi->param("from") || strftime("%d/%m/%Y", localtime(time()-30*86400));
+  my $to = $cgi->param("to") || strftime("%d/%m/%Y", localtime);
+  my $from_sql = dh_parse_date_sql($from)
+    or $errors{from_sql} = "Invalid from date";
+  my $to_sql = dh_parse_date_sql($to)
+    or $errors{to_sql} = "Invalid to date";
+
+  require BSE::TB::FileAccessLog;
+  my @entries;
+  unless (keys %errors) {
+    push @filters, [ between => when_at => $from_sql, $to_sql ];
+    $cgi->param(from => $from);
+    $cgi->param(to => $to);
+    $page_args{from} = $from;
+    $page_args{to} = $to;
+    @entries = map $_->{id}, BSE::TB::FileAccessLog->query
+      (
+       [ qw/id/ ],
+       \@filters,
+       {
+       order => 'when_at desc'
+       },
+      );
+  }
+
+  my $it = BSE::Util::Iterate->new;
+  my $current_access;
+  my %acts =
+    (
+     $req->admin_tags,
+     $it->make_paged
+     (
+      data => \@entries,
+      fetch => [ getByPkey => 'BSE::TB::FileAccessLog' ],
+      cgi => $req->cgi,
+      single => "fileaccess",
+      plural => "fileaccesses",
+      store => \$current_access,
+      name => "fileaccesses",
+      session => $req->session,
+      perpage_parm => "pp=100",
+     ),
+     ifOwner => defined $owner,
+     owner => [ \&tag_hash, $owner ],
+     owner_type => (defined $owner_type ? $owner_type : ''),
+     owner_desc => escape_html($owner_desc),
+     ifSiteuser => defined $user,
+     siteuser => [ \&tag_hash, $user ],
+     ifFile => defined $file,
+     file => [ \&tag_hash, $file ],
+     page_args => [ tag_page_args => $self, \%page_args ],
+     page_argsh => [ tag_page_argsh => $self, \%page_args ],
+     user => [ \&tag_fileaccess_user, \$current_access, \%user_cache ],
+     ifFileuser => [ \&tag_ifFileuser, \$current_access, \%user_cache ],
+     fileowner => [ \&tag_fileowner, \$current_access, \%user_cache, $req->cfg ],
+     filecat => [ \&tag_filecat, \$current_access, \%categories ],
+     error_img =>[ \&tag_error_img, $req->cfg, \%errors ],
+     ifCategory => $category,
+     category => escape_html($category),
+    );
+
+  return $req->dyn_response("admin/users/fileaccess", \%acts);
+}
+
 1;
index 77af919..faf87d6 100644 (file)
@@ -59,16 +59,22 @@ sub _query_expr {
     return '(' . join (" $op ", map _query_expr($args, $map, $table_name, @$_), @terms) . ')';
   }
   else {
-    my ($column, $value) = @terms;
+    my ($column, @values) = @terms;
     my $db_col = $map->{$column}
       or confess "No column '$column' in $table_name";
-    push @$args, $value;
-    return "$db_col $op ?";
+    if ($op eq "between") {
+      push @$args, @values[0, 1];
+      return "$db_col $op ? and ?";
+    }
+    else {
+      push @$args, $values[0];
+      return "$db_col $op ?";
+    }
   }
 }
 
 sub generate_query {
-  my ($self, $row_class, $columns, $query) = @_;
+  my ($self, $row_class, $columns, $query, $opts) = @_;
 
   my %trans;
   @trans{$row_class->columns} = $row_class->db_columns;
@@ -79,9 +85,12 @@ sub generate_query {
     {; $trans{$_} or confess "No column '$_' in $table_name" } @$columns;
   my $sql = 'select ' . join(',', @out_columns) . ' from ' . $table_name;
   my @args;
-  if ($query) {
+  if ($query && @$query) {
     $sql .= ' where ' . _query_expr(\@args, \%trans, $table_name, 'and', @$query,);
   }
+  if ($opts->{order}) {
+    $sql .= "order by " . $opts->{order};
+  }
 
   #print STDERR "generated sql >$sql<\n";
   my $sth = $self->{dbh}->prepare($sql)
diff --git a/site/cgi-bin/modules/BSE/TB/FileAccessLog.pm b/site/cgi-bin/modules/BSE/TB/FileAccessLog.pm
new file mode 100644 (file)
index 0000000..afcc86e
--- /dev/null
@@ -0,0 +1,34 @@
+package BSE::TB::FileAccessLog;
+use strict;
+use base 'Squirrel::Table';
+use BSE::TB::FileAccessLogEntry;
+use Carp qw(confess);
+
+sub rowClass {
+  'BSE::TB::FileAccessLogEntry';
+}
+
+sub log_download {
+  my ($self, %opts) = @_;
+
+  my $user = delete $opts{user}
+    or confess "No user parameter to log_download()";
+
+  my $file = delete $opts{file}
+    or confess "No file parameter to log_download()";
+  
+  my %args;
+  for my $field (BSE::TB::FileAccessLogEntry->columns) {
+    $args{$field} = $file->{$field}
+      if exists $file->{$field};
+  }
+  delete $args{id};
+  $args{file_id} = $file->id;
+  $args{siteuser_id} = $user->id;
+  $args{siteuser_logon} = $user->userId;
+  defined $opts{download} and $args{download} = $opts{download};
+
+  return BSE::TB::FileAccessLog->make(%args);
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/TB/FileAccessLogEntry.pm b/site/cgi-bin/modules/BSE/TB/FileAccessLogEntry.pm
new file mode 100644 (file)
index 0000000..6063fa7
--- /dev/null
@@ -0,0 +1,21 @@
+package BSE::TB::FileAccessLogEntry;
+use strict;
+use base 'Squirrel::Row';
+use BSE::Util::SQL qw(now_sqldatetime);
+
+sub columns {
+  return qw/id when_at siteuser_id siteuser_logon file_id owner_type owner_id category filename display_name content_type download title modwhen size_in_bytes/;
+}
+
+sub table {
+  "bse_file_access_log";
+}
+
+sub defaults {
+  return
+    (
+     when_at => now_sqldatetime(),
+    );
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/TB/OwnedFile.pm b/site/cgi-bin/modules/BSE/TB/OwnedFile.pm
new file mode 100644 (file)
index 0000000..cf9ecfc
--- /dev/null
@@ -0,0 +1,23 @@
+package BSE::TB::OwnedFile;
+use strict;
+use base 'Squirrel::Row';
+use BSE::Util::SQL qw(now_sqldatetime);
+
+sub columns {
+  return qw/id owner_type owner_id category filename display_name content_type download title body modwhen size_in_bytes/;
+}
+
+sub table {
+  "bse_owned_files";
+}
+
+sub defaults {
+  return
+    (
+     download => 0,
+     body => '',
+     modwhen => now_sqldatetime(),
+    );
+}
+
+1;
diff --git a/site/cgi-bin/modules/BSE/TB/OwnedFiles.pm b/site/cgi-bin/modules/BSE/TB/OwnedFiles.pm
new file mode 100644 (file)
index 0000000..2c5a617
--- /dev/null
@@ -0,0 +1,28 @@
+package BSE::TB::OwnedFiles;
+use strict;
+use base 'Squirrel::Table';
+use BSE::TB::OwnedFile;
+
+sub rowClass {
+  'BSE::TB::OwnedFile';
+}
+
+sub categories {
+  my ($class, $cfg) = @_;
+
+  my @cat_ids = split /,/, $cfg->entry("file categories", "ids", "");
+  grep $_ eq "", @cat_ids
+    or unshift @cat_ids, "";
+
+  my @cats;
+  for my $id (@cat_ids) {
+    my $section = "file category $id";
+    my $def_name = length $id ? ucfirst $id : "(None)";
+    my $name = $cfg->entry($section, "name", $def_name);
+    push @cats, +{ id => $id, name => $name };
+  }
+
+  return @cats;
+}
+
+1;
index e21484a..bf3c225 100644 (file)
@@ -1,6 +1,7 @@
 package BSE::TB::SiteUserGroup;
 use strict;
 use base 'Squirrel::Row';
+use constant OWNER_TYPE => "G";
 
 sub columns {
   qw(id name);
@@ -79,4 +80,16 @@ sub contains_user {
   return scalar @membership;
 }
 
+sub file_owner_type {
+  return OWNER_TYPE;
+}
+
+sub files {
+  my ($self) = @_;
+
+  require BSE::TB::OwnedFiles;
+  return BSE::TB::OwnedFiles->getBy(owner_type => OWNER_TYPE,
+                                   owner_id => $self->id);
+}
+
 1;
index 7e8a75a..be9aff1 100644 (file)
@@ -8,11 +8,10 @@ use constant SECT_QUERY_GROUP_PREFIX => 'Query group ';
 
 sub rowClass { 'BSE::TB::SiteUserGroup' }
 
-sub admin_and_query_groups {
+sub query_groups {
   my ($class, $cfg) = @_;
 
-  my @groups = $class->all;
-
+  my @groups;
   my $id = 1;
   my $name;
   while ($name = $cfg->entry(SECT_QUERY_GROUPS, $id)) {
@@ -22,7 +21,17 @@ sub admin_and_query_groups {
     ++$id;
   }
 
-  @groups;
+  return @groups;
+}
+
+sub admin_and_query_groups {
+  my ($class, $cfg) = @_;
+
+  return
+    (
+     $class->all,
+     $class->query_groups($cfg),
+    );
 }
 
 sub getQueryGroup {
@@ -58,6 +67,11 @@ sub getByName {
 }
 
 package BSE::TB::SiteUserQueryGroup;
+use constant OWNER_TYPE => "G";
+
+sub id { $_[0]{id} }
+
+sub name { $_[0]{name} }
 
 sub contains_user {
   my ($self, $user) = @_;
@@ -71,4 +85,20 @@ sub contains_user {
   return 0;
 }
 
+sub file_owner_type {
+  return OWNER_TYPE;
+}
+
+sub files {
+  my ($self) = @_;
+
+  require BSE::TB::OwnedFiles;
+  return BSE::TB::OwnedFiles->getBy(owner_type => OWNER_TYPE,
+                                   owner_id => $self->id);
+}
+
+sub data_only {
+  return +{ %{$_[0]} };
+}
+
 1;
index 0b2b3ed..397580e 100644 (file)
@@ -203,6 +203,19 @@ sub output_result {
       push @{$result->{headers}}, "Cache-Control: no-cache";
     }
   }
+  if (!grep /^content-length:/, @{$result->{headers}}) {
+    my $length;
+    if (defined $result->{content}) {
+      $length = length $result->{content};
+    }
+    elsif (defined $result->{content_fh}) {
+      # this may need to change if we support byte ranges
+      $length = -s $result->{content_fh};
+    }
+    if (defined $length) {
+      push @{$result->{headers}}, "Content-Length: $length";
+    }
+  }
   if (exists $ENV{GATEWAY_INTERFACE}
       && $ENV{GATEWAY_INTERFACE} =~ /^CGI-Perl\//) {
     require Apache;
@@ -213,7 +226,22 @@ sub output_result {
     print "$_\n" for @{$result->{headers}};
     print "\n";
   }
-  print $result->{content};
+  if (defined $result->{content}) {
+    print $result->{content};
+  }
+  elsif ($result->{content_fh}) {
+    # in the future this could be updated to support byte ranges
+    local $/ = \8192;
+    my $fh = $result->{content_fh};
+    binmode $fh;
+    while (my $data = <$fh>) {
+      print $data;
+    }
+  }
+  else {
+    print STDERR "$ENV{SCRIPT_NAME}: ** No content supplied\n";
+    print "** Internal error\n";
+  }
 }
 
 1;
index 1c67701..70c3455 100644 (file)
@@ -6,6 +6,7 @@ use DevHelp::HTML qw(:default popup_menu);
 use BSE::Util::Iterate;
 use BSE::Util::SQL qw/now_datetime/;
 use DevHelp::Date qw(dh_strftime_sql_datetime);
+use base 'BSE::UI::UserCommon';
 
 my %actions =
   (
@@ -112,6 +113,12 @@ sub req_info {
                        'subscription', 'subscriptions'),
      $it->make_iterator([ \&iter_sembookings, $user ],
                        'booking', 'bookings'),
+     $it->make
+     (
+      code => [ iter_userfiles => $self, $user, $req ],
+      single => 'userfile',
+      plural => 'userfiles',
+     ),
     );
 
   return $req->dyn_response('user/userpage', \%acts);
diff --git a/site/cgi-bin/modules/BSE/UI/UserCommon.pm b/site/cgi-bin/modules/BSE/UI/UserCommon.pm
new file mode 100644 (file)
index 0000000..924cddd
--- /dev/null
@@ -0,0 +1,83 @@
+package BSE::UI::UserCommon;
+use strict;
+
+# code common to both BSE::UserReg and BSE::UI::User
+# see also BSE::UI::SiteuserCommon
+
+my %num_file_fields = map { $_=> 1 }
+  qw/id owner_id size_in_bytes/;
+
+sub iter_userfiles {
+  my ($self, $user, $req, $args) = @_;
+
+  my @files = map $_->data_only, $user->visible_files($req->cfg);
+
+  # produce a url for each file
+  my $base = '/cgi-bin/user.pl?a_downufile=1&id=';
+  for my $file (@files) {
+    $file->{url} = $base . $file->{id};
+  }
+  defined $args or $args = '';
+
+  my $sort;
+  if ($args =~ s/\bsort:\s?(-?\w+(?:,-?\w+)*)\b//) {
+    $sort = $1;
+  }
+  my $cgi_sort = $req->cgi->param('userfile_sort');
+  $cgi_sort
+    and $sort = $cgi_sort;
+  if ($sort && @files > 1) {
+    my @fields = map 
+      {
+       my $work = $_;
+       my $rev = $work =~ s/^-//;
+       [ $rev, $work ]
+      } split /,/, $sort;
+
+    @fields = grep exists $files[0]{$_->[1]}, @fields;
+
+    @files = sort
+      {
+       for my $field (@fields) {
+         my $name = $field->[1];
+         my $diff = $num_file_fields{$name}
+           ? $a->{$name} <=> lc $b->{$name}
+             : $a->{$name} cmp lc $b->{$name};
+         if ($diff) {
+           return $field->[0] ? -$diff : $diff;
+         }
+       }
+       return 0;
+      } @files;
+  }
+
+  $args =~ /\S/
+    or return @files;
+
+  if ($args =~ /^\s*filter:(.*)$/) {
+    my $expr = $1;
+    my $func = eval 'sub { my $file = $_[0];' .  $expr . '}';
+    unless ($func) {
+      print STDERR "** Cannot compile userfile filter $expr: $@\n";
+      return;
+    }
+    return grep $func->($_), @files;
+  }
+
+  if ($args =~ /^\s*(!)?(\w+(?:,\w+)*)\s*$/) {
+    my ($not, $cats) = ( $1, $2 );
+    my %matches = map { $_ => 1 } split ',', $cats, -1;
+    if ($not) {
+      return grep !$matches{$_->{category}}, @files;
+    }
+    else {
+      return grep $matches{$_->{category}}, @files;
+    }
+  }
+
+  print STDERR "** unparsable arguments to userfile: $args\n";
+
+  return;
+}
+
+1;
index 5b78eb8..8dfcd33 100644 (file)
@@ -15,6 +15,7 @@ use DevHelp::HTML;
 use BSE::CfgInfo qw(custom_class);
 use BSE::WebUtil qw/refresh_to/;
 use BSE::Util::Iterate;
+use base 'BSE::UI::UserCommon';
 
 use constant MAX_UNACKED_CONF_MSGS => 3;
 use constant MIN_UNACKED_CONF_GAP => 2 * 24 * 60 * 60;
@@ -42,6 +43,7 @@ my %actions =
    image => 'req_image',
    orderdetail => 'req_orderdetail',
    wishlist => 'req_wishlist',
+   downufile => 'req_downufile',
   );
 
 sub actions { \%actions }
@@ -476,7 +478,14 @@ sub req_show_opts {
       $message = '';
     }
   }
+  require BSE::TB::OwnedFiles;
+  my @file_cats = BSE::TB::OwnedFiles->categories($cfg);
+  my %subbed = map { $_ => 1 } $user->subscribed_file_categories;
+  for my $cat (@file_cats) {
+    $cat->{subscribed} = exists $subbed{$cat->{id}} ? 1 : 0;
+  }
 
+  my $it = BSE::Util::Iterate->new;
   my %acts;
   %acts =
     (
@@ -502,6 +511,12 @@ sub req_show_opts {
      $self->_edit_tags($user, $cfg),
      ifSubscribedTo => [ \&tag_ifSubscribedTo, $user ],
      partial_logon => $partial_logon,
+     $it->make
+     (
+      data => \@file_cats,
+      single => "filecat",
+      plural => "filecats"
+     ),
     );
 
   my $base = 'user/options';
@@ -709,6 +724,10 @@ sub req_saveopts {
       if $subs && !$user->{confirmed};
   }
 
+  if ($cgi->param('save_file_subs')) {
+    my @new_subs = $cgi->param("file_subscriptions");
+    $user->set_subscribed_file_categories($cfg, @new_subs);
+  }
 
   if ($partial_logon) {
     $user->{previousLogon} = $user->{lastLogon};
@@ -1072,6 +1091,12 @@ sub req_userpage {
                        'subscription', 'subscriptions'),
      $it->make_iterator([ \&iter_sembookings, $user ],
                        'booking', 'bookings'),
+     $it->make
+     (
+      code => [ iter_userfiles => $self, $user, $req ],
+      single => 'userfile',
+      plural => 'userfiles',
+     ),
     );
   my $base_template = 'user/userpage';
   my $template = $base_template;
@@ -1926,4 +1951,90 @@ sub req_wishlist {
   BSE::Template->show_page($template, $req->cfg, \%acts);
 }
 
+=item req_downufile
+
+=target a_downufile
+
+Download a user file.
+
+=cut
+
+sub req_downufile {
+  my ($self, $req) = @_;
+
+  require BSE::TB::OwnedFiles;
+  my $cgi = $req->cgi;
+  my $cfg = $req->cfg;
+  my $id = $cgi->param("id");
+  defined $id && $id =~ /^\d+$/
+    or return $self->error($req, "Invalid or missing file id");
+
+  # return the same error to avoid giving someone a mechanism to find
+  # which files are in use
+  my $file = BSE::TB::OwnedFiles->getByPkey($id)
+    or return $self->error($req, "Invalid or missing file id");
+
+  my $user = $self->_get_user($req, 'downufile')
+    or return;
+
+  require BSE::TB::SiteUserGroups;
+  my $accessible = 0;
+  if ($file->owner_type eq $user->file_owner_type) {
+    $accessible = $user->id == $file->owner_id;
+  }
+  elsif ($file->owner_type eq BSE::TB::SiteUserGroup->file_owner_type) {
+    my $owner_id = $file->owner_id;
+    my $group = $owner_id < 0
+      ? BSE::TB::SiteUserGroups->getQueryGroup($cfg, $owner_id)
+      : BSE::TB::SiteUserGroups->getByPkey($owner_id);
+    if ($group) {
+      $accessible = $group->contains_user($user);
+    }
+    else {
+      print STDERR "** downufile: unknown group id ", $file->owner_id, " in file ", $file->id, "\n";
+    }
+  }
+  else {
+    print STDERR "** downufile: Unknown file owner type ", $file->owner_type, " in file ", $file->id, "\n";
+    $accessible = 0;
+  }
+
+  $accessible
+    or return $self->error($req, "Sorry, you don't have access to this file");
+
+  my $filebase = $cfg->entryVar('paths', 'downloads');
+  require IO::File;
+  my $fh = IO::File->new("$filebase/" . $file->filename, "r")
+    or return $self->error($req, "Cannot open stored file: $!");
+
+  my @headers;
+  my %result =
+    (
+     content_fh => $fh,
+     headers => \@headers,
+    );
+  my $download = $cgi->param("force_download") || $file->download;
+  if ($download) {
+    push @headers, "Content-Disposition: attachment; filename=".$file->display_name;
+    $result{type} = "application/octet-stream";
+  }
+  else {
+    push @headers, "Content-Disposition: inline; filename=" . $file->display_name;
+    $result{type} = $file->content_type;
+  }
+  if ($cfg->entry("download", "log_downuload", 0)) {
+    my $max_age = $cfg->entry("download", "log_downuload_maxage", 30);
+    BSE::DB->run(bseDownloadLogAge => $max_age);
+    require BSE::TB::FileAccessLog;
+    BSE::TB::FileAccessLog->log_download
+       (
+        user => $user,
+        file => $file,
+        download => $download,
+       );
+  }
+
+  BSE::Template->output_result($req, \%result);
+}
+
 1;
index 5dbe55f..7225e62 100644 (file)
@@ -121,7 +121,7 @@ sub static {
        --$month;
        # passing the isdst as 0 seems to provide a more accurate result than
        # -1 on glibc.
-       return bse_strftime($cfg, $fmt, $sec, $min, $hour, $day, $month, $year, -1, -1, 0);
+       return bse_strftime($cfg, $fmt, $sec, $min, $hour, $day, $month, $year, -1, -1, -1);
      },
      today => [ \&tag_today, $cfg ],
      money =>
index 1719ff0..55d984b 100644 (file)
@@ -76,9 +76,9 @@ sub dh_parse_date_sql {
 sub dh_parse_time {
   my ($time, $rmsg) = @_;
 
-  if ($time =~ /^\s*(\d+)[:. ]?(\d{2})\s*$/) {
+  if ($time =~ /^\s*(\d+)[:. ]?(\d{2})(?:[:.](\d{2}))?\s*$/) {
     # 24 hour time
-    my ($hour, $min) = ($1, $2);
+    my ($hour, $min, $sec) = ($1, $2, $3);
 
     if ($hour > 23) {
       $$rmsg = "Hour must be from 0 to 23 for 24-hour time";
@@ -88,19 +88,27 @@ sub dh_parse_time {
       $$rmsg = "Minutes must be from 0 to 59";
       return;
     }
+    defined $sec or $sec = 0;
+    if ($sec > 59) {
+      $$rmsg = "Seconds must be from 0 to 59";
+      return;
+    }
 
-    return (0+$hour, 0+$min, 0);
+    return (0+$hour, 0+$min, 0+$sec);
   }
   else {
     # try for 12 hour time
-    my ($hour, $min, $ampm);
+    my ($hour, $min, $sec, $ampm);
 
     if ($time =~ /^\s*(\d+)\s*(?:([ap])m?)\s*$/i) {
       # "12am", "2pm", etc
-      ($hour, $min, $ampm) = ($1, 0, $2);
+      ($hour, $min, $sec, $ampm) = ($1, 0, 0, $2);
     }
     elsif ($time =~ /^\s*(\d+)[.: ](\d{2})\s*(?:([ap])m?)\s*$/i) {
-      ($hour, $min, $ampm) = ($1, $2, $3);
+      ($hour, $min, $sec, $ampm) = ($1, $2, 0, $3);
+    }
+    elsif ($time =~ /^\s*(\d+)[.: ](\d{2})[:.](\d{2})\s*(?:([ap])m?)\s*$/i) {
+      ($hour, $min, $sec, $ampm) = ($1, $2, $3, $4);
     }
     else {
       $$rmsg = "Unknown time format";
@@ -114,10 +122,14 @@ sub dh_parse_time {
       $$rmsg = "Minutes must be from 0 to 59";
       return;
     }
+    if ($sec > 59) {
+      $$rmsg = "Seconds must be from 0 to 59";
+      return;
+    }
     $hour = 0 if $hour == 12;
     $hour += 12 if lc $ampm eq 'p';
 
-    return (0+$hour, 0+$min, 0);
+    return (0+$hour, 0+$min, 0+$sec);
   }
 }
 
index ac9bebf..c7f2399 100644 (file)
@@ -29,11 +29,33 @@ sub _iter_iterate {
 
   if (++${$state->{index}} < @{$state->{data}}) {
     my $item = $state->{data}[${$state->{index}}];
+    if ($state->{fetch}) {
+      my $fetch = $state->{fetch};
+      my $code = $fetch;
+      my @args;
+      if (ref $fetch eq 'ARRAY') {
+       ($code, @args) = @$fetch;
+      }
+      if ($state->{state}) {
+       push @args, $state->{state};
+      }
+      push @args, $item;
+      if (ref $code) {
+       ($item) = $code->(@args);
+      }
+      else {
+       my $object = shift @args;
+       ($item) = $object->$code(@args);
+      }
+    }
+    $state->{item} = $item;
     ${$state->{store}} = $item if $state->{store};
     $self->next_item($item, $state->{single});
     return 1;
   }
   else {
+    $state->{item} = undef;
+    ${$state->{store}} = undef if $state->{store};
     $self->next_item(undef, $state->{single});
   }
   return;
@@ -51,10 +73,10 @@ sub item {
 sub _iter_item {
   my ($self, $state, $args) = @_;
 
-  ${$state->{index}} >= 0 && ${$state->{index}} < @{$state->{data}}
+  $state->{item}
     or return "** $state->{single} should only be used inside iterator $state->{plural} **";
 
-  return $self->item($state->{data}[${$state->{index}}], $args);
+  return $self->item($state->{item}, $args);
 }
 
 sub _iter_number_paged {
index 60afec0..418886a 100644 (file)
@@ -143,10 +143,13 @@ my %built_ins =
                    (?:[01]?\d|2[0-3])  # hour 0-23
                       [:.]             # separator
                       [0-5]\d          # minute
+                      (?:[:.][0-5]\d)? # optional seconds
                   |                    # or 12 hour time:
                    (?:0?[1-9]|1[012]) # hour 1-12
                     (?:[:.]           # optionally separator followed
-                     [0-5]\d)?        # by minutes
+                     [0-5]\d          # by minutes
+                      (?:[:.][0-5]\d)? # optionall by seconds
+                    )? 
                    [ap]m?             # followed by afternoon/morning
                   )$!ix,
     error=>'Invalid time $n',
index a88597e..eb18b16 100644 (file)
@@ -9,6 +9,7 @@ use BSE::Util::SQL qw/now_datetime now_sqldate sql_normal_date sql_add_date_days
 
 use constant MAX_UNACKED_CONF_MSGS => 3;
 use constant MIN_UNACKED_CONF_GAP => 2 * 24 * 60 * 60;
+use constant OWNER_TYPE => "U";
 
 sub columns {
   return qw/id userId password email keepAddress whenRegistered lastLogon
@@ -531,4 +532,71 @@ sub move_up_wishlist {
   $self->_set_wishlist_order($order->[$index-1]{product_id}, $order->[$index]{display_order});
 }
 
+# files owned specifically by this user
+sub files {
+  my ($self) = @_;
+
+  require BSE::TB::OwnedFiles;
+  return BSE::TB::OwnedFiles->getBy(owner_type => OWNER_TYPE,
+                                   owner_id => $self->id);
+}
+
+sub admin_group_files {
+  my ($self) = @_;
+
+  require BSE::TB::OwnedFiles;
+  return BSE::TB::OwnedFiles->getSpecial(userVisibleGroupFiles => $self->{id});
+}
+
+sub query_group_files {
+  my ($self, $cfg) = @_;
+
+  require BSE::TB::SiteUserGroups;
+  return
+    (
+     map $_->files, BSE::TB::SiteUserGroups->query_groups($cfg)
+    );
+}
+
+# files the user can see, both owned and owned by groups
+sub visible_files {
+  my ($self, $cfg) = @_;
+
+  return
+    (
+     $self->files,
+     $self->admin_group_files,
+     $self->query_group_files($cfg)
+    );
+}
+
+sub file_owner_type {
+  return OWNER_TYPE;
+}
+
+sub subscribed_file_categories {
+  my ($self) = @_;
+
+  return map $_->{category}, BSE::DB->query(siteuserSubscribedFileCategories => $self->{id});
+}
+
+sub set_subscribed_file_categories {
+  my ($self, $cfg, @new) = @_;
+
+  require BSE::TB::OwnedFiles;
+  my %current = map { $_ => 1 } $self->subscribed_file_categories;
+  my %new = map { $_ => 1 } @new;
+  my @all = BSE::TB::OwnedFiles->categories($cfg);
+  for my $cat (@all) {
+    if ($new{$cat->{id}} && !$current{$cat->{id}}) {
+      eval {
+       BSE::DB->run(siteuserAddFileCategory => $self->{id}, $cat->{id});
+      }; # a race condition might cause a duplicate key error here
+    }
+    elsif (!$new{$cat->{id}} && $current{$cat->{id}}) {
+      BSE::DB->run(siteuserRemoveFileCategory => $self->{id}, $cat->{id});
+    }
+  }
+}
+
 1;
index 1c53183..504667a 100644 (file)
@@ -307,7 +307,7 @@ sub all {
 sub query {
   my ($self, $columns, $query, $opts) = @_;
 
-  $dh->generate_query($self, $columns, $query, $opts);
+  $dh->generate_query($self->rowClass, $columns, $query, $opts);
 }
 
 sub make {
index 4d562b6..afe31b7 100644 (file)
@@ -92,3 +92,35 @@ select ab.*, ag.*
 where am.user_id = ? and am.group_id = ab.id and ab.id = ag.base_id
 SQL
 
+name: siteuserAddFileCategory
+sql_statement: <<SQL
+insert into bse_file_subscriptions(siteuser_id, category)
+ values(?, ?)
+SQL
+
+name: siteuserRemoveFileCategory
+sql_statement: <<SQL
+delete from bse_file_subscriptions where siteuser_id = ? and category = ?
+SQL
+
+name: siteuserSubscribedFileCategories
+sql_statement: <<SQL
+select category
+from bse_file_subscriptions
+where siteuser_id = ?
+SQL
+
+name: OwnedFiles.userVisibleGroupFiles
+sql_statement: <<SQL
+select of.*
+from bse_owned_files of, bse_siteuser_membership sm
+where sm.siteuser_id = ?
+  and sm.group_id = of.owner_id
+  and of.owner_type = 'G'
+SQL
+
+name: bseDownloadLogAge
+sql_statement: <<SQL
+delete from bse_file_access_log
+ where when_at < date_sub(now(), interval ? day)
+SQL
\ No newline at end of file
index 10842ef..1434b07 100644 (file)
@@ -546,6 +546,15 @@ downloaded.
 if non-zero the user must be registered/logged on to download I<any>
 file.
 
+=item log_downufile
+
+if non-zero, downloads of userfiles will be logged.  Default: 0
+
+=item log_downufile_maxage
+
+the maximum age of entries in the user file download log, in days.
+Default: 30.
+
 =back
 
 =head2 [confirmations]
index 7558ddb..377b119 100644 (file)
@@ -138,6 +138,14 @@ table.editform td.check  {
   width: 20px;
 }
 
+table.editform td textarea {
+  width: 95%;
+}
+
+table.editform td input.wide {
+  width: 95%;
+}
+
 table.editform td.check { 
   text-align: center;
 }
@@ -166,4 +174,35 @@ div.menu {
 .inplaceeditor-saving {
   padding-right: 20px;
   background: url(/images/admin/busy.gif) bottom right no-repeat;
+}
+
+#sitedragdrop li { 
+  margin-top: 0px;
+  margin-bottom: 0px;
+  padding-bottom: 0px;
+  padding-top: 2px;
+}
+
+#sitedragdrop ul {
+  padding-top: 6px;
+  padding-bottom: 0px;
+}
+
+#filelist div.fileentry {
+  border: 1px solid #000;
+}
+
+#filelist div.fileentry .title { 
+  background-color: #fff;
+  border-bottom: 1px solid black;
+}
+
+#fileaccesslog .col_size { 
+  text-align: right;
+}
+
+#fileaccesslog a:link,
+#fileaccesslog a:visited,
+#fileaccesslog a:active { 
+  font-weight: normal;
 }
\ No newline at end of file
index 07c98e2..c71802d 100644 (file)
@@ -698,7 +698,8 @@ var Sortable = {
       onHover:      Sortable.onEmptyHover,
       overlap:      options.overlap,
       containment:  options.containment,
-      hoverclass:   options.hoverclass
+      hoverclass:   options.hoverclass,
+      tree:         options.tree
     };
 
     // fix for gecko engine
diff --git a/site/templates/admin/users/add_group_file.tmpl b/site/templates/admin/users/add_group_file.tmpl
new file mode 100644 (file)
index 0000000..2f8b4b2
--- /dev/null
@@ -0,0 +1,46 @@
+<:wrap admin/xbase.tmpl title=>"Group Files - Add File":>
+<h1>Add Group File</h1>
+
+<:include admin/users/inc_group_menu.tmpl:>
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="addfile">
+<form enctype="multipart/form-data" method="post">
+<input type="hidden" name="id" value="<:group id:>" />
+<:csrfp admin_group_add_file hidden:>
+<table class="editform editformsmall">
+  <tr>
+    <th>File:</th>
+    <td><input type="file" name="file" /><:error_img file:></td>
+  </tr>
+  <tr>
+   <th>Content-type:</th>
+   <td><input type="text" name="content_type" value="<:old content_type:>" /><:error_img content_type:></td>
+  </tr>
+  <tr>
+   <th>Download:</th>
+   <td><input type="checkbox" name="download" <:ifOld download:>checked="checked" <:or:><:eif:>/><:error_img download:></td>
+  </tr>
+  <tr>
+   <th>Category:</th>
+   <td><:userfile_category:><:error_img category:></td>
+  </tr>
+  <tr>
+    <th>Last modified:</th>
+    <td>Date: <input type="text" name="modwhen_date" value="<:old modwhen_date:>" size="10" /><:error_img modwhen_date:> dd/mm/yyyy<br />Time: <input type="text" name="modwhen_time" value="<:old modwhen_time:>" size="10" /><:error_img modwhen_time:> HH:MM:SS</td>
+  </tr>
+  <tr>
+   <th>Title:</th>
+   <td><input type="text" name="title" value="<:old title:>" class="wide" /><:error_img title:></td>
+  </tr>
+  <tr>
+    <th>Body:</th>
+    <td><textarea name="body" rows="5"><:old body:></textarea><:error_img body:></td>
+  </tr>
+  <tr>
+    <td class="buttons" colspan="2">
+      <input type="submit" name="a_addgroupfile" value="Add File" />
+    </td>
+  </tr>
+</table>
+</form>
+</div>
diff --git a/site/templates/admin/users/add_user_file.tmpl b/site/templates/admin/users/add_user_file.tmpl
new file mode 100644 (file)
index 0000000..99f7c95
--- /dev/null
@@ -0,0 +1,12 @@
+<:wrap admin/xbase.tmpl title=>"Site Member Files - Add File":>
+<h1>Add Member File</h1>
+
+<p>
+| <a href="/cgi-bin/admin/menu.pl">Admin menu</a> |
+<a href="/cgi-bin/admin/siteusers.pl">Site Members</a> |
+<a href="mailto:<:siteuser email:>">Email</a> |
+<:ifUserCan bse_members_user_edit:><a href="/cgi-bin/admin/siteusers.pl?a_edit=1&amp;id=<:siteuser id:>">Edit User</a> |<:or:><:eif:>
+<a href="/cgi-bin/admin/siteusers.pl?a_view=1&amp;id=<:siteuser id:>">User Details</a> |</p>
+
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<:include admin/users/inc_add_user_file.tmpl:>
diff --git a/site/templates/admin/users/delete_group_file.tmpl b/site/templates/admin/users/delete_group_file.tmpl
new file mode 100644 (file)
index 0000000..1732d6e
--- /dev/null
@@ -0,0 +1,47 @@
+<:wrap admin/xbase.tmpl title=>"Group Files - Delete File":>
+<h1>Delete Group File</h1>
+
+<:include admin/users/inc_group_menu.tmpl:>
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="delfile">
+<form enctype="multipart/form-data" method="post">
+<input type="hidden" name="id" value="<:group id:>" />
+<input type="hidden" name="file_id" value="<:groupfile id:>" />
+<:csrfp admin_group_del_file hidden:>
+<table class="editform editformsmall">
+  <tr>
+    <th>File:</th>
+    <td><:groupfile display_name:></td>
+  </tr>
+  <tr>
+   <th>Content-type:</th>
+   <td><:groupfile content_type:></td>
+  </tr>
+  <tr>
+   <th>Download:</th>
+   <td><:ifGroupfile download:>Yes<:or:>No<:eif:></td>
+  </tr>
+  <tr>
+   <th>Category:</th>
+   <td><:groupfile category:></td>
+  </tr>
+  <tr>
+    <th>Last modified:</th>
+    <td>Date: <:date "%d/%m/%Y" groupfile modwhen:><br />Time: <:date "%H:%M:%S" groupfile modwhen:></td>
+  </tr>
+  <tr>
+   <th>Title:</th>
+   <td><:groupfile title:></td>
+  </tr>
+  <tr>
+    <th>Body:</th>
+    <td><textarea name="body" rows="5" readonly="readonly"><: groupfile body:></textarea><:error_img body:></td>
+  </tr>
+  <tr>
+    <td class="buttons" colspan="2">
+      <input type="submit" name="a_delgroupfile" value="Delete File" />
+    </td>
+  </tr>
+</table>
+</form>
+</div>
diff --git a/site/templates/admin/users/delete_user_file.tmpl b/site/templates/admin/users/delete_user_file.tmpl
new file mode 100644 (file)
index 0000000..ecd5bb8
--- /dev/null
@@ -0,0 +1,47 @@
+<:wrap admin/xbase.tmpl title=>"Site Member Files - Delete File":>
+<h1>Delete Member File</h1>
+
+<:include admin/users/inc_user_menu.tmpl:>
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="delfile">
+<form enctype="multipart/form-data" method="post">
+<input type="hidden" name="id" value="<:siteuser id:>" />
+<input type="hidden" name="file_id" value="<:userfile id:>" />
+<:csrfp admin_user_del_file hidden:>
+<table class="editform editformsmall">
+  <tr>
+    <th>File:</th>
+    <td><:userfile display_name:></td>
+  </tr>
+  <tr>
+   <th>Content-type:</th>
+   <td><:userfile content_type:></td>
+  </tr>
+  <tr>
+   <th>Download:</th>
+   <td><:ifUserfile download:>Yes<:or:>No<:eif:></td>
+  </tr>
+  <tr>
+   <th>Category:</th>
+   <td><:userfile category:></td>
+  </tr>
+  <tr>
+    <th>Last modified:</th>
+    <td>Date: <:date "%d/%m/%Y" userfile modwhen:><br />Time: <:date "%H:%M:%S" userfile modwhen:></td>
+  </tr>
+  <tr>
+   <th>Title:</th>
+   <td><:userfile title:></td>
+  </tr>
+  <tr>
+    <th>Body:</th>
+    <td><textarea name="body" rows="5" readonly="readonly"><: userfile body:></textarea><:error_img body:></td>
+  </tr>
+  <tr>
+    <td class="buttons" colspan="2">
+      <input type="submit" name="a_deluserfile" value="Delete File" />
+    </td>
+  </tr>
+</table>
+</form>
+</div>
index 59b9d7e..4bf6c64 100644 (file)
           </tr>
 <:eif Cfg:>
 <:or UserCan:><:eif UserCan:>
+<:if Filecats:>
+      <tr>
+       <th>File subscriptions:</th>
+       <td>
+         <input type="hidden" name="save_file_subs" value="1" />
+         <:iterator begin filecats:>
+           <input type="checkbox" name="file_subscriptions" value="<:filecat id:>" <:ifFilecat subscribed:>checked="checked" <:or:><:eif:> /> <:filecat name:>
+         <:iterator separator filecats:>
+           <br />
+         <:iterator end filecats:>
+       </td>
+            <td class="help"><:help editsiteuser filesubs:></td></tr>
+<:or Filecats:><:eif Filecats:>
+
 <:include admin/users/custom_edit.tmpl optional:>
           <tr> 
             <td class="buttons"colspan="3"> 
diff --git a/site/templates/admin/users/edit_files.tmpl b/site/templates/admin/users/edit_files.tmpl
new file mode 100644 (file)
index 0000000..c5d38ad
--- /dev/null
@@ -0,0 +1,36 @@
+<:wrap admin/xbase.tmpl title=>"Site Member Files":>
+<h1>Site Member Files</h1>
+<:include admin/users/inc_user_menu.tmpl:>
+<:ifUserCan bse_members_user_add_file:><p><a href="/cgi-bin/admin/siteusers.pl?a_adduserfileform=1&amp;id=<:siteuser id:>">Add a file</a></p><:or:><:eif:>
+
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="filelist">
+<table class="editform">
+<tr>
+  <th>Filename</th>
+  <th>Content Type</th>
+  <th>Size</th>
+  <th>Category</th>
+  <th>Last Modified</th>
+  <th>Title</th>
+  <th>Body (partial)</th>
+  <th></th>
+</tr>
+<:if Userfiles:>
+  <:iterator begin userfiles:>
+  <tr>
+    <td><:userfile display_name:></td>
+    <td><:userfile content_type:></td>
+    <td><:kb userfile size_in_bytes:>b</td>
+    <td><:userfile category:></td>
+    <td><:date userfile modwhen:></td>
+    <td><:userfile title:></td>
+    <td><:replace [userfile body] ^([\w\W]{25})[\w\W]+ "$1 ...":></td>
+    <td><a href="<:script:>?a_deluserfileform=1&amp;id=<:siteuser id:>&amp;file_id=<:userfile id:>">Delete</a></div> <a href="<:script:>?a_edituserfile=1&amp;id=<:siteuser id:>&amp;file_id=<:userfile id:>">Edit</a></div></td>
+  </tr>
+  <:iterator end userfiles:>
+<:or Userfiles:>
+<tr><td colspan="8">There are no files attached to this user</td></tr>
+<:eif Userfiles:>
+</table>
+</div>
diff --git a/site/templates/admin/users/edit_group_file.tmpl b/site/templates/admin/users/edit_group_file.tmpl
new file mode 100644 (file)
index 0000000..339d464
--- /dev/null
@@ -0,0 +1,49 @@
+<:wrap admin/xbase.tmpl title=>"Group Files - Edit File":>
+<h1>Edit Group File</h1>
+
+<:include admin/users/inc_group_menu.tmpl:>
+
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="editfile">
+<form enctype="multipart/form-data" method="post">
+<input type="hidden" name="id" value="<:group id:>" />
+<input type="hidden" name="file_id" value="<:groupfile id:>" />
+<:csrfp admin_group_edit_file hidden:>
+<table class="editform editformsmall">
+  <tr>
+    <th>File:</th>
+    <td><input type="file" name="file" /><:error_img file:>
+<br />Currently: <:groupfile display_name:></td>
+  </tr>
+  <tr>
+   <th>Content-type:</th>
+   <td><input type="text" name="content_type" value="<:old content_type groupfile content_type:>" /><:error_img content_type:></td>
+  </tr>
+  <tr>
+   <th>Download:</th>
+   <td><input type="checkbox" name="download" <:if Cgi a_savegroupfile:><:ifOld download:>checked="checked" <:or:><:eif:><:or Cgi:><:ifGroupfile download:>checked="checked" <:or:><:eif:><:eif Cgi:>/><:error_img download:></td>
+  </tr>
+  <tr>
+   <th>Category:</th>
+   <td><:userfile_category:><:error_img category:></td>
+  </tr>
+  <tr>
+    <th>Last modified:</th>
+    <td>Date: <input type="text" name="modwhen_date" value="<:old modwhen_date date "%d/%m/%Y" groupfile modwhen:>" size="10" /><:error_img modwhen_date:> dd/mm/yyyy<br />Time: <input type="text" name="modwhen_time" value="<:old modwhen_time date "%H:%M:%S" groupfile modwhen:>" size="10" /><:error_img modwhen_time:> HH:MM:SS</td>
+  </tr>
+  <tr>
+   <th>Title:</th>
+   <td><input type="text" name="title" value="<:old title groupfile title:>" class="wide" /><:error_img title:></td>
+  </tr>
+  <tr>
+    <th>Body:</th>
+    <td><textarea name="body" rows="5"><:old body groupfile body:></textarea><:error_img body:></td>
+  </tr>
+  <tr>
+    <td class="buttons" colspan="2">
+      <input type="submit" name="a_savegroupfile" value="Save File" />
+    </td>
+  </tr>
+</table>
+</form>
+</div>
diff --git a/site/templates/admin/users/edit_user_file.tmpl b/site/templates/admin/users/edit_user_file.tmpl
new file mode 100644 (file)
index 0000000..8367143
--- /dev/null
@@ -0,0 +1,49 @@
+<:wrap admin/xbase.tmpl title=>"Site Member Files - Edit File":>
+<h1>Edit Member File</h1>
+
+<:include admin/users/inc_user_menu.tmpl:>
+
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="editfile">
+<form enctype="multipart/form-data" method="post">
+<input type="hidden" name="id" value="<:siteuser id:>" />
+<input type="hidden" name="file_id" value="<:userfile id:>" />
+<:csrfp admin_user_edit_file hidden:>
+<table class="editform editformsmall">
+  <tr>
+    <th>File:</th>
+    <td><input type="file" name="file" /><:error_img file:>
+<br />Currently: <:userfile display_name:></td>
+  </tr>
+  <tr>
+   <th>Content-type:</th>
+   <td><input type="text" name="content_type" value="<:old content_type userfile content_type:>" /><:error_img content_type:></td>
+  </tr>
+  <tr>
+   <th>Download:</th>
+   <td><input type="checkbox" name="download" <:if Cgi a_saveuserfile:><:ifOld download:>checked="checked" <:or:><:eif:><:or Cgi:><:ifUserfile download:>checked="checked" <:or:><:eif:><:eif Cgi:>/><:error_img download:></td>
+  </tr>
+  <tr>
+   <th>Category:</th>
+   <td><:userfile_category:><:error_img category:></td>
+  </tr>
+  <tr>
+    <th>Last modified:</th>
+    <td>Date: <input type="text" name="modwhen_date" value="<:old modwhen_date date "%d/%m/%Y" userfile modwhen:>" size="10" /><:error_img modwhen_date:> dd/mm/yyyy<br />Time: <input type="text" name="modwhen_time" value="<:old modwhen_time date "%H:%M:%S" userfile modwhen:>" size="10" /><:error_img modwhen_time:> HH:MM:SS</td>
+  </tr>
+  <tr>
+   <th>Title:</th>
+   <td><input type="text" name="title" value="<:old title userfile title:>" class="wide" /><:error_img title:></td>
+  </tr>
+  <tr>
+    <th>Body:</th>
+    <td><textarea name="body" rows="5"><:old body userfile body:></textarea><:error_img body:></td>
+  </tr>
+  <tr>
+    <td class="buttons" colspan="2">
+      <input type="submit" name="a_saveuserfile" value="Save File" />
+    </td>
+  </tr>
+</table>
+</form>
+</div>
diff --git a/site/templates/admin/users/fileaccess.tmpl b/site/templates/admin/users/fileaccess.tmpl
new file mode 100644 (file)
index 0000000..eb8e7ba
--- /dev/null
@@ -0,0 +1,48 @@
+<:wrap admin/xbase.tmpl title=>"File Access Log":>
+<h1>File Access Log</h1>
+<p>| <a href="/cgi-bin/admin/menu.pl">Admin menu</a> |</p>
+<:if Owner:><p>Only files owned by <a href="<:script:>?id=<:owner_id:>&amp;_t=files&amp;<:ifEq [owner_type] G:>a_editgroup=1<:or:>a_edit=1<:eif:>"><:owner_desc:></a> <a href="<:script:>?a_fileaccesslog=1&amp;<:page_args owner_id owner_type:>">(Remove this filter)</a></p><:or Owner:><:eif Owner:>
+<:ifSiteuser:><p>Files downloaded by <a href="<:script:>?a_edit=1&amp;id=<:siteuser id:>"><:siteuser userId:></a> <a href="<:script:>?a_fileaccesslog=1&amp;<:page_args siteuser_id:>">(Remove this filter)</a></p><:or:><:eif:>
+<:if File:><p>File <a href="<:script:>?<:ifEq [file owner_type] U:>a_edituserfile<:or:>a_editgroupfile<:eif:>=1&amp;id=<:file owner_id:>&amp;file_id=<:file id:>"><:file display_name:></a> <a href="<:script:>?a_fileaccesslog=1&amp;<:page_args file_id:>">(Remove this filter)</a></p><:or File:><:eif File:>
+<:ifCategory:>Files in category: <:category name:> <a href="<:script:>?a_fileaccesslog=1&amp;<:page_args category:>">(Remove this filter)</a><:or:><:eif:>
+
+<p>Page <:fileaccesses_pagenum:> of <:fileaccesses_pagecount:>
+<:ifFirstFileaccessesPage:>First Previous<:or:><a href="<:script:>?a_fileaccesslog=1&amp;p=1&amp;pp=<:fileaccesses_perpage:>">First</a> <a href="<:script:>?a_fileaccesslog=1&amp;p=<:prevFileaccessesPage:>&amp;pp=<:fileaccesses_perpage:>&amp;<:page_args:>">Previous</a><:eif:>
+<:iterator begin repeats [fileaccesses_pagecount]:>
+<:if Eq [repeat value] [fileaccesses_pagenum]:><:repeat value:><:or Eq:><a href="<:script:>?a_fileaccesslog=1&amp;p=<:repeat value:>&amp;pp=<:fileaccesses_perpage:>&amp;<:page_args:>"><:repeat value:></a><:eif Eq:>
+<:iterator end repeats:>
+<:ifLastFileaccessesPage:>Next Last<:or:><a href="<:script:>?a_fileaccesslog=1&amp;p=<:nextFileaccessesPage:>&amp;pp=<:fileaccesses_perpage:>&amp;<:page_args:>">Next</a> <a href="<:script:>?a_fileaccesslog=1&amp;p=<:fileaccesses_pagecount:>&amp;pp=<:fileaccesses_perpage:>&amp;<:page_args:>">Last</a><:eif:>
+</p>
+<form action="<:script:>"><:page_argsh from to:><p>Records from <input type="text" name="from" size="12" value="<:cgi from:>" /> to <input type="text" name="to" size="12" value="<:cgi to:>" /> <input type="submit" name="a_fileaccesslog" value="Go" /></form>
+
+<table class="editform" id="fileaccesslog">
+  <tr>
+    <th>When</th>
+    <th>User</th>
+    <th>File</th>
+    <th>Owner</th>
+    <th>Title</th>
+    <th>Category</th>
+    <th>Size</th>
+    <th>Filters</th>
+  </tr>
+<:iterator begin fileaccesses:>
+  <tr>
+    <td class="col_when_at"><:date "%d/%m/%Y %H:%M" fileaccess when_at:></td>
+    <td class="col_user"><a href="<:script:>?a_edit=1&amp;id=<:fileaccess siteuser_id:>"><:user:></a>
+    </td>
+    <td class="col_filename"><a href="<:script:>?<:ifEq [fileaccess owner_type] "U":>a_edituserfile<:or:>a_editgroupfile<:eif:>=1&amp;id=<:fileaccess owner_id:>&amp;file_id=<:fileaccess file_id:>"><:fileaccess display_name:> (<:fileaccess file_id:>)</a></td>
+    <td class="col_owner"><a href="<:script:>?<:ifEq [fileaccess owner_type] "U":>a_edit<:or:>a_editgroup<:eif:>=1&amp;<:ifMatch [fileaccess owner_id] ^-:>_t=files&amp;<:or:><:eif:>=1&amp;id=<:fileaccess owner_id:>"><:fileowner desc:></a></td>
+    <td class="col_title"><:fileaccess title:></td>
+    <td class="col_category"><:filecat:></td>
+    <td class="col_size"><:fileaccess size_in_bytes:></td>
+    <td class="col_links">
+<a href="<:script:>?a_fileaccesslog=1&amp;<:page_args siteuser_id:>&amp;siteuser_id=<:fileaccess siteuser_id:>">User</a>
+<a href="<:script:>?a_fileaccesslog=1&amp;<:page_args file_id:>&amp;file_id=<:fileaccess file_id:>">File</a>
+
+<a href="<:script:>?a_fileaccesslog=1&amp;<:page_args owner_type owner_id:>&amp;owner_id=<:fileaccess owner_id:>&amp;owner_type=<:fileaccess owner_type:>">Owner</a>
+<a href="<:script:>?a_fileaccesslog=1&amp;<:page_args category:>&amp;category=<:fileaccess category:>">Category</a>
+</td>
+  </tr>
+<:iterator end fileaccesses:>
+</table>
index 16f44a1..0157c23 100644 (file)
@@ -1,14 +1,6 @@
 <:wrap admin/xbase.tmpl title=>"Edit Member Group":>
 <h2>Add Member Group</h2>
-<p>
-| 
-<a href="/cgi-bin/admin/menu.pl">Admin menu</a>
-|
-<a href="<:script:>">Member List</a>
-|
-<a href="<:script:>?a_grouplist=1">Group List</a>
-|
-</p>
+<:include admin/users/inc_group_menu.tmpl:>
 <:ifMsg:><p><:msg:></p><:or:><:eif:>
 
 <form method="post" action="<:script:>" name="editgroup">
diff --git a/site/templates/admin/users/groupedit_files.tmpl b/site/templates/admin/users/groupedit_files.tmpl
new file mode 100644 (file)
index 0000000..cb064c6
--- /dev/null
@@ -0,0 +1,36 @@
+<:wrap admin/xbase.tmpl title=>"Group Files":>
+<h1>Group Files: <:group name:></h1>
+<:include admin/users/inc_group_menu.tmpl:>
+<:ifUserCan bse_members_group_add_file:><p><a href="/cgi-bin/admin/siteusers.pl?a_addgroupfileform=1&amp;id=<:group id:>">Add a file</a></p><:or:><:eif:>
+
+<:ifMessage:><div id="message"><:message:></div><:or:><:eif:>
+<div id="filelist">
+<table class="editform">
+<tr>
+  <th>Filename</th>
+  <th>Content Type</th>
+  <th>Size</th>
+  <th>Category</th>
+  <th>Last Modified</th>
+  <th>Title</th>
+  <th>Body (partial)</th>
+  <th></th>
+</tr>
+<:if Groupfiles:>
+  <:iterator begin groupfiles:>
+  <tr>
+    <td><:groupfile display_name:></td>
+    <td><:groupfile content_type:></td>
+    <td><:kb groupfile size_in_bytes:>b</td>
+    <td><:groupfile category:></td>
+    <td><:date groupfile modwhen:></td>
+    <td><:groupfile title:></td>
+    <td><:replace [groupfile body] ^([\w\W]{25})[\w\W]+ "$1 ...":></td>
+    <td><a href="<:script:>?a_delgroupfileform=1&amp;id=<:group id:>&amp;file_id=<:groupfile id:>">Delete</a></div> <a href="<:script:>?a_editgroupfile=1&amp;id=<:group id:>&amp;file_id=<:groupfile id:>">Edit</a></div></td>
+  </tr>
+  <:iterator end groupfiles:>
+<:or Groupfiles:>
+<tr><td colspan="8">There are no files attached to this group</td></tr>
+<:eif Groupfiles:>
+</table>
+</div>
index c13a2e5..de1dda4 100644 (file)
 <tr>
   <td><:group name:></td>
   <td>
+<:if Match [group id] ^\d:>
     <a href="<:script:>?a_editgroup=1&amp;id=<:group id:>">Edit</a>
     <a href="<:script:>?a_deletegroupform=1&amp;id=<:group id:>">Delete</a>
     <a href="<:script:>?a_groupmemberform=1&amp;id=<:group id:>">Members</a>
+<:or Match:><:eif Match:>
+    <a href="<:script:>?a_editgroup=1&amp;_t=files&amp;id=<:group id:>">Files</a>
   </td>
 </tr>
 <:iterator end groups:>
diff --git a/site/templates/admin/users/inc_add_user_file.tmpl b/site/templates/admin/users/inc_add_user_file.tmpl
new file mode 100644 (file)
index 0000000..c41335a
--- /dev/null
@@ -0,0 +1,41 @@
+<div id="addfile">
+<form enctype="multipart/form-data" method="post">
+<input type="hidden" name="id" value="<:siteuser id:>" />
+<:csrfp admin_user_add_file hidden:>
+<table class="editform editformsmall">
+  <tr>
+    <th>File:</th>
+    <td><input type="file" name="file" /><:error_img file:></td>
+  </tr>
+  <tr>
+   <th>Content-type:</th>
+   <td><input type="text" name="content_type" value="<:old content_type:>" /><:error_img content_type:></td>
+  </tr>
+  <tr>
+   <th>Download:</th>
+   <td><input type="checkbox" name="download" <:ifOld download:>checked="checked" <:or:><:eif:>/><:error_img download:></td>
+  </tr>
+  <tr>
+   <th>Category:</th>
+   <td><:userfile_category:><:error_img category:></td>
+  </tr>
+  <tr>
+    <th>Last modified:</th>
+    <td>Date: <input type="text" name="modwhen_date" value="<:old modwhen_date:>" size="10" /><:error_img modwhen_date:> dd/mm/yyyy<br />Time: <input type="text" name="modwhen_time" value="<:old modwhen_time:>" size="10" /><:error_img modwhen_time:> HH:MM:SS</td>
+  </tr>
+  <tr>
+   <th>Title:</th>
+   <td><input type="text" name="title" value="<:old title:>" class="wide" /><:error_img title:></td>
+  </tr>
+  <tr>
+    <th>Body:</th>
+    <td><textarea name="body" rows="5"><:old body:></textarea><:error_img body:></td>
+  </tr>
+  <tr>
+    <td class="buttons" colspan="2">
+      <input type="submit" name="a_adduserfile" value="Add File" />
+    </td>
+  </tr>
+</table>
+</form>
+</div>
diff --git a/site/templates/admin/users/inc_group_menu.tmpl b/site/templates/admin/users/inc_group_menu.tmpl
new file mode 100644 (file)
index 0000000..671624c
--- /dev/null
@@ -0,0 +1,11 @@
+<p>
+| 
+<a href="/cgi-bin/admin/menu.pl">Admin menu</a>
+|
+<a href="<:script:>">Member List</a>
+|
+<a href="<:script:>?a_editgroup=1&amp;id=<:group id:>&amp;_t=files">Group files</a>
+|
+<a href="<:script:>?a_grouplist=1">Group List</a>
+|
+</p>
diff --git a/site/templates/admin/users/inc_user_menu.tmpl b/site/templates/admin/users/inc_user_menu.tmpl
new file mode 100644 (file)
index 0000000..9902395
--- /dev/null
@@ -0,0 +1,11 @@
+<p>
+| <a href="/cgi-bin/admin/menu.pl">Admin menu</a> |
+<a href="/cgi-bin/admin/siteusers.pl">Site Members</a> |
+<a href="mailto:<:siteuser email:>">Email</a>
+<:ifUserorders:>| <a href="/cgi-bin/admin/siteusers.pl?a_view=1&amp;id=<:siteuser id:>&amp;_t=orders">Orders</a><:or:><:eif:> |
+<a href="/cgi-bin/admin/admin_seminar.pl?a_addattendseminar=1&amp;siteuser_id=<:siteuser id:>">Add to seminar</a> |
+<a href="/cgi-bin/admin/siteusers.pl?a_view=1&amp;id=<:siteuser id:>&amp;_t=bookings">Seminar Bookings</a> |
+<a href="<:script:>?a_edit=1&amp;id=<:siteuser id:>&amp;_t=groups">Groups</a> |
+<a href="/cgi-bin/admin/siteusers.pl?a_edit=1&amp;id=<:siteuser id:>&amp;_t=files">Files</a> |
+</p>
+
index dcf8260..a8dfb00 100644 (file)
@@ -1 +1,9 @@
-<div class="usermenu"><a href="<:script:>">Your Page</a> <a href="<:script:>?show_opts=1">Your Profile</a> <a href="<:script:>?_t=wishlist">Your Wishlist</a></div>
+<div class="usermenu">
+<a href="<:script:>">Your Page</a>
+<a href="<:script:>?show_opts=1">Your Profile</a>
+<a href="<:script:>?_t=wishlist">Your Wishlist</a>
+<a href="<:script:>?_t=orders">Your Orders</a>
+<:ifUserfiles:>
+<a href="<:script:>?_t=files">Your Files</a>
+<:or:><:eif:>
+</div>
diff --git a/site/templates/user/base_userpage_files.tmpl b/site/templates/user/base_userpage_files.tmpl
new file mode 100644 (file)
index 0000000..9b41e97
--- /dev/null
@@ -0,0 +1,31 @@
+<:wrap base.tmpl:> 
+<:include include/usermenu.tmpl:>
+<p><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Hello <:ifUser 
+  name1:><:user name1:><:or:><:user userId:><:eif:>, this section contains your 
+  personal account details. From here you can <:ifCfg subscriptions enabled:> manage 
+  your newsletter subscriptions,<:or:><:eif:><:ifCfg shop enabled:> monitor the current status 
+  or purchase history of your orders from our on-line store<:or:><:eif:><:ifOr [cfg shop enabled] [cfg subscriptions enabled]:> and<:or:><:eif:> modify 
+  your personal details, for example, your password, email and mailing addresses 
+  etc.</font></p>
+<p><font face="Verdana, Arial, Helvetica, sans-serif" size="2">To modify your 
+  account options, <:ifCfg subscriptions enabled:>like subscribing to one of our available newsletters,<:or:><:eif:> please 
+  proceed to edit your &#8220;<a href="<:script:>?show_opts=1">User Profile</a>&#8221; 
+  and make your changes <:ifCfg subscriptions enabled:>eg: select a newsletter from the available list<:or:><:eif:> then 
+  select &#8220;Save Options&#8221;.</font></p>
+
+<table>
+<tr>
+  <th>Name</th>
+  <th>Date</th>
+  <th>Title</th>
+  <th>Size</th>
+</tr>
+<:iterator begin userfiles sort: -modwhen:>
+<tr>
+  <td><a href="<:userfile url:>"><:userfile display_name:></a></td>
+  <td><:date userfile modwhen:></td>
+  <td><:userfile title:></td>
+  <td><:kb userfile size_in_bytes:></td>
+</tr>
+<:iterator end userfiles:>
+</table>
diff --git a/site/templates/user/base_userpage_orders.tmpl b/site/templates/user/base_userpage_orders.tmpl
new file mode 100644 (file)
index 0000000..428a160
--- /dev/null
@@ -0,0 +1,133 @@
+<:wrap base.tmpl:> 
+<:include include/usermenu.tmpl:>
+<p><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Hello <:ifUser 
+  name1:><:user name1:><:or:><:user userId:><:eif:>, this section contains your 
+  personal account details. From here you can <:ifCfg subscriptions enabled:> manage 
+  your newsletter subscriptions,<:or:><:eif:><:ifCfg shop enabled:> monitor the current status 
+  or purchase history of your orders from our on-line store<:or:><:eif:><:ifOr [cfg shop enabled] [cfg subscriptions enabled]:> and<:or:><:eif:> modify 
+  your personal details, for example, your password, email and mailing addresses 
+  etc.</font></p>
+<p><font face="Verdana, Arial, Helvetica, sans-serif" size="2">To modify your 
+  account options, <:ifCfg subscriptions enabled:>like subscribing to one of our available newsletters,<:or:><:eif:> please 
+  proceed to edit your &#8220;<a href="<:script:>?show_opts=1">User Profile</a>&#8221; 
+  and make your changes <:ifCfg subscriptions enabled:>eg: select a newsletter from the available list<:or:><:eif:> then 
+  select &#8220;Save Options&#8221;.</font></p>
+<div align="center"> 
+  <table border="0" cellspacing="0" cellpadding="0">
+    <tr>
+      <td>
+        <form name="userprofile" action="<:script:>">
+          <input type="submit" name="Submit" value="Edit user profile" class="user-buttons">
+          <input type="hidden" name="show_opts" value="1">
+        </form>
+      </td>
+      <:ifCfg shop enabled:><td>
+        <form name="ff" action="/cgi-bin/shop.pl">
+          <input type="submit" name="cart" value="View shopping cart" class="user-buttons">
+        </form>
+      </td><:or:><:eif:>
+    </tr>
+  </table>
+  <br>
+   
+<:if Message:><p class="message"><:message:></p> <:or Message:><:eif Message:> 
+  <table width="100%" cellpadding="0" cellspacing="1">
+    <tr> 
+      <th align="center" height="20"><font face="Verdana, Arial, Helvetica, sans-serif" size="3">Your 
+        Account</font></th>
+    </tr>
+    <tr> 
+      <td align="center"> <p><b><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Hello 
+          <:ifUser name1:><:user name1:> <:user name2:><:or:><:user userId:><:eif:></font></b></p>
+        <p><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#999999">Last 
+          logged in: <:date user previousLogon:><br>
+          Registered since: <:date user whenRegistered:></font><br>
+          <br>
+        </p></td>
+    </tr>
+       <:if Cfg shop enabled:>
+    <:if Orders:> <:iterator begin orders:> 
+    <tr> 
+      <td bgcolor="#CCCCCC"> <table width="100%" cellpadding="3" cellspacing="1">
+          <tr> 
+            <th align="center" nowrap width="25%" bgcolor="#666666"><a href="<:script:>?a_orderdetail=1&amp;id=<:order id:>"><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#CCCCCC">Order 
+              No:</font><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#FFFFFF"> 
+              <:order id:></font></a></th>
+            <th align="center" width="25%" nowrap bgcolor="#666666"><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#CCCCCC">Date:</font><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#FFFFFF"> 
+              <:date order orderDate:></font></th>
+            <th align="center" width="25%" nowrap bgcolor="<:ifOrder filled:>#CC0033<:or:>#66CC00<:eif:>"><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="<:ifOrder filled:>#CCCCCC<:or:>#000000<:eif:>">Status:</font><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#FFFFFF"> 
+              <:ifOrder filled:>Complete<:or:>Processing<:eif:></font></th>
+            <th align="center" width="25%" nowrap bgcolor="#FF7F00"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Total:</font><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#FFFFFF"> 
+              $<:money order total:></font></th>
+          </tr>
+        </table>
+        <:if Items:> <table width="100%" cellpadding="3" cellspacing="1">
+          <tr bgcolor="#EEEEEE"> 
+            <th width="100%" align="left"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Product</font></th>
+            <th><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Quantity</font></th>
+            <th><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Unit</font></th>
+          </tr>
+          <:iterator begin items:> 
+          <tr bgcolor="#FFFFFF"> 
+            <td width="100%"><a href="<:product link:>"><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:product 
+              description:></font></a></td>
+            <td align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:item 
+              units:></font></td>
+            <td align="right"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">$<:money 
+              item price:></font></td>
+          </tr>
+          <:iterator end items:> 
+        </table>
+        <:if Orderfiles:> <table width="100%" cellpadding="3" cellspacing="1">
+          <tr bgcolor="#CCCCCC"> 
+            <th colspan="4"><font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#666666"><:if 
+              Order filled:>Files available<:or Order:><:ifCfg downloads must_be_filled:>Files 
+              available when order status is &#145;Complete&#146;<:or:>Files<:eif:><:eif 
+              Order:></font></th>
+          </tr>
+          <tr bgcolor="#EEEEEE"> 
+            <th width="50%" align="left"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Description</font></th>
+            <th nowrap width="50%" align="left" colspan="2"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">File</font></th>
+            <th><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Size</font></th>
+          </tr>
+          <:iterator begin orderfiles:> 
+          <tr bgcolor="#FFFFFF"> 
+            <td width="50%"><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:orderfile 
+              description:></font></td>
+            <td nowrap width="50%"><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:if 
+              FileAvail:><a href="/cgi-bin/user.pl?download=1&file=<:orderfile id:>&order=<:order id:>&item=<:orderfile item_id:>"><:orderfile 
+              displayName:></a><:or FileAvail:><:orderfile displayName:><:eif 
+              FileAvail:></font></td>
+            <td><:if FileAvail:><a href="/cgi-bin/user.pl?download=1&file=<:orderfile id:>&order=<:order id:>&item=<:orderfile item_id:>"><img src="/images/filestatus/download.gif" width="15" height="15" alt="Download now" title="Download now" border="0"></a><:or 
+              FileAvail:><img src="/images/filestatus/locked.gif" width="15" height="15" alt="Locked" title="Locked"><:eif 
+              FileAvail:></td>
+            <td align="right"><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:kb 
+              orderfile sizeInBytes:></font></td>
+          </tr>
+          <:iterator end orderfiles:> 
+        </table>
+        <:or Orderfiles:><:eif Orderfiles:> <:or Items:><:eif Items:> </td>
+    </tr>
+    <:iterator separator orders:> 
+    <tr> 
+      <td >&nbsp; </td>
+    </tr>
+    <:iterator end orders:> <:or Orders:> 
+    <tr> 
+      <td  align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">You 
+        haven't made any orders yet.</font></td>
+    </tr>
+    <:eif Orders:>
+       <:or Cfg:>
+       <:eif Cfg:>
+  </table>
+  
+<:if Subscriptions:>  
+<table>
+<tr><th><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Name</font></th><th><font face="Verdana, Arial, Helvetica, sans-serif" size="2">Expires</font></th></tr>
+<:iterator begin subscriptions:>
+<tr><td><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:subscription title:></font></td><td><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:date subscription ends_at:></font></td></tr>
+<:iterator end subscriptions:>
+</table>
+<:or Subscriptions:><:eif Subscriptions:>
+</div>
index 1d2ef6c..37ed9d6 100644 (file)
         </td>
       </tr>
 <:eif Cfg:>
+<:if Filecats:>
+      <tr>
+       <th nowrap="nowrap" align="left">File subscriptions:</th>
+       <td>
+         <input type="hidden" name="save_file_subs" value="1" />
+         <:iterator begin filecats:>
+           <input type="checkbox" name="file_subscriptions" value="<:filecat id:>" <:ifFilecat subscribed:>checked="checked" <:or:><:eif:> /> <:filecat name:>
+         <:iterator separator filecats:>
+           <br />
+         <:iterator end filecats:>
+       </td>
+      </tr>
+<:or Filecats:><:eif Filecats:>
       <tr> 
         <td colspan="2" align="right"> 
           <input type="submit" name="saveopts" value="Save Options" class="user-buttons" />
index 2084884..c265b49 100644 (file)
@@ -102,6 +102,38 @@ Table bse_article_groups
 Column article_id;int(11);NO;NULL;
 Column group_id;int(11);NO;NULL;
 Index PRIMARY;1;[article_id;group_id]
+Table bse_file_access_log
+Column id;int(11);NO;NULL;auto_increment
+Column when_at;datetime;NO;NULL;
+Column siteuser_id;int(11);NO;NULL;
+Column siteuser_logon;varchar(40);NO;NULL;
+Column file_id;int(11);NO;NULL;
+Column owner_type;char(1);NO;NULL;
+Column owner_id;int(11);NO;NULL;
+Column category;varchar(20);NO;NULL;
+Column filename;varchar(255);NO;NULL;
+Column display_name;varchar(255);NO;NULL;
+Column content_type;varchar(80);NO;NULL;
+Column download;int(11);NO;NULL;
+Column title;varchar(255);NO;NULL;
+Column modwhen;datetime;NO;NULL;
+Column size_in_bytes;int(11);NO;NULL;
+Index PRIMARY;1;[id]
+Index by_file;0;[file_id]
+Index by_user;0;[siteuser_id;when_at]
+Index by_when_at;0;[when_at]
+Table bse_file_notifies
+Column id;int(11);NO;NULL;auto_increment
+Column siteuser_id;int(11);NO;NULL;
+Column file_id;int(11);NO;NULL;
+Index PRIMARY;1;[id]
+Index by_siteuser;0;[siteuser_id]
+Table bse_file_subscriptions
+Column id;int(11);NO;NULL;
+Column siteuser_id;int(11);NO;NULL;
+Column category;varchar(20);NO;NULL;
+Index by_category;0;[category]
+Index by_siteuser;0;[siteuser_id]
 Table bse_locations
 Column id;int(11);NO;NULL;auto_increment
 Column description;varchar(255);NO;NULL;
@@ -132,6 +164,21 @@ Column display;varchar(80);NO;NULL;
 Column display_order;int(11);NO;NULL;
 Index PRIMARY;1;[id]
 Index item_order;0;[order_item_id;display_order]
+Table bse_owned_files
+Column id;int(11);NO;NULL;auto_increment
+Column owner_type;char(1);NO;NULL;
+Column owner_id;int(11);NO;NULL;
+Column category;varchar(20);NO;NULL;
+Column filename;varchar(255);NO;NULL;
+Column display_name;varchar(255);NO;NULL;
+Column content_type;varchar(80);NO;NULL;
+Column download;int(11);NO;NULL;
+Column title;varchar(255);NO;NULL;
+Column body;text;NO;NULL;
+Column modwhen;datetime;NO;NULL;
+Column size_in_bytes;int(11);NO;NULL;
+Index PRIMARY;1;[id]
+Index by_owner_category;0;[owner_type;owner_id;category]
 Table bse_product_option_values
 Column id;int(11);NO;NULL;auto_increment
 Column product_option_id;int(11);NO;NULL;
index 8790e5d..f00c472 100644 (file)
@@ -1,6 +1,6 @@
 #!perl -w
 use strict;
-use Test::More tests=>42;
+use Test::More tests=>44;
 
 my $gotmodule;
 BEGIN { $gotmodule = use_ok('DevHelp::Date', ':all'); }
@@ -43,6 +43,11 @@ SKIP:
   is_deeply([ dh_parse_time("1101", \$msg) ], [ 11, 1, 0 ], "parse 1101");
   is($msg, undef, "no error");
 
+  is_deeply([ dh_parse_time("11:01:02", \$msg) ], [ 11, 1, 2 ],
+           "parse 11:01:02") or diag $msg;
+  is_deeply([ dh_parse_time("11:01:02pm", \$msg) ], [23, 1, 2 ],
+           "parse 11:01:02pm") or diag $msg;
+
   # fail a bit
   undef $msg;
   is_deeply([ dh_parse_time("xxx", \$msg) ], [], "parse xxx");