# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Content.pm 3243 2008-08-23 13:17:21Z josh $

package BigMed::Content;
use strict;
use warnings;
use utf8;
use Carp;

use base qw(BigMed::Data);
use BigMed::Pointer;
use BigMed::Plugin;

my %content_class;
my %child_types;

###########################################################
# SET CONTENT DATA SCHEMA
###########################################################

my @data_schema = (
    {   name  => 'site',
        type  => 'system_id',
        index => 1,
    },
    {   name     => 'sections',
        type     => 'system_id',
        multiple => 1,
        index    => 1,
    },
    {   name  => 'subtype',
        type  => 'simple_text',
        index => 1,
    },
    {   name  => 'owner',
        type  => 'system_id',
        index => 1,
    },
    {   name    => 'pub_status',
        type    => 'value_list',
        options => ['draft', 'edit', 'ready', 'published'],
        labels  => {
            'draft'     => 'CONTENT_pub_status_draft',
            'edit'      => 'CONTENT_pub_status_edit',
            'ready'     => 'CONTENT_pub_status_ready',
            'published' => 'CONTENT_pub_status_published',
        },
        default => 'draft',
        index   => 1,
    },
    {   name  => 'priority',
        type  => 'number_zeroplus_integer',
        index => 1,
    },
    {   name  => 'pub_time',
        type  => 'system_time',
        index => 1,
    },
    {   name  => 'auto_pub_time',
        type  => 'system_time',
        index => 1,
    },
    {   name  => 'auto_unpub_time',
        type  => 'system_time',
        index => 1,
    },
    {   name    => 'version',
        type    => 'number_integer_positive',
        index   => 1,
        default => 1,
    },
    {   name    => 'title',
        type    => 'rich_text_inline',
        default => q{},
        index   => 1,
    },
    {   name => 'last_editor',
        type => 'system_id',
    },
    {   name    => 'content',
        type    => 'rich_text',
        default => q{},
    },
);

BigMed::Content->set_schema(
    source   => 'content',
    label    => 'content',
    elements => \@data_schema,
);

###########################################################
# CONTENT REGISTRATION
###########################################################

sub register_content_class {
    my $class = shift;
    $class eq 'BigMed::Content'
      && croak 'register_content_class must be invoked via a BigMed::Content '
      . 'subclass';

    @_ % 2
      && croak 'register_content_class arguments must be hash: odd number of '
      . 'parameters. Check key/value pairs';
    my %arg = @_;

    #get parent class for inheritance;
    my $parent_class;
    {
        no strict 'refs';
        $parent_class = ${"${class}::ISA"}[0];
    }

    #register as a data object
    $class->set_schema(
        source   => $arg{source},
        label    => $arg{label},
        elements => $arg{elements},
    );

    #format the content metadata, inheriting from immediate parent, and store
    #as the default values in the content_class hash (the defaults are used
    #only as the basis for defining subtypes; subtype definitions live in
    #their entirety in the content class' subtype hash).
    $content_class{$class} = $parent_class->_format_content_metadata(%arg);
    $content_class{$class}->{subtypes} = {}
      unless $content_class{$class}->{subtypes};

    return 1;
}

sub register_content_subtype {
    my $parent_class = shift;    #registering as a subset of this parent class
    exists $content_class{$parent_class}
      || croak "No such content class '$parent_class'; did you forget to "
      . 'register it?';
    @_ % 2
      && croak 'register_content_type arguments must be hash: odd number of '
      . 'parameters. Check key/value pairs';
    my %arg  = @_;
    my $name = $arg{name}
      or croak
      "No name provided in $parent_class content subtype registration";

    $content_class{$parent_class}->{subtypes}->{$name} =
      $parent_class->_format_content_metadata(%arg);
    $content_class{$parent_class}->{default_subtype} = $name if $arg{default};
    return 1;
}

###########################################################
# OBJECT RETRIEVAL
###########################################################

sub fetch_or_create_content_obj {
    my $class = shift;
    my %attr  = @_;
    my ( $cid, $site, $subtype ) = @attr{qw(id site subtype)};
    my $site_id = ( ref $site ? $site->id : $site )
      or croak 'site required in fetch_or_create_content_obj';

    my $obj;
    if ($cid) {
        $obj =
          $class->fetch( { site => $site_id, id => $cid }, { limit => 1 } );
        return if !defined $obj;
    }
    if ( !$obj ) {    #no such object, or new
        $obj = $class->new();
        $obj->set_site($site_id);
        if ( !$subtype || !$class->subtype_exists($subtype) ) {
            $subtype = $class->default_subtype;
        }
        $obj->set_subtype($subtype);
        if ($cid) {
            $obj->set_id($cid);

            #prevent arbitrary advancement of the counter into the future
            #has to be an id that we've seen before
            $obj->has_valid_id()
              or return $class->set_error(
                head => 'CONTENT_Invalid content ID',
                text =>
                  ['CONTENT_TEXT_Invalid content ID', $obj->data_label, $cid],
              );
        }
        else {
            $obj->update_id() or return;
        }
    }
    return $obj;
}

###########################################################
# EXTRA COPY ROUTINES
###########################################################

BigMed::Content->add_callback( 'after_copy', \&_after_copy );

sub _after_copy {
    my ($orig, $clone, $rparam) = @_;
    $rparam ||= {};

    $clone->set_version(1);
    if ( $clone->isa('BigMed::Content::Page') ) {
        $clone->generate_slug() or return;
    }

    my ($osite, $nsite); #site objects for new site if applicable
    if ( $clone->id != $orig->id && $rparam->{target_site} ) {    #load sites

        #new site
        $nsite =
          ref $rparam->{target_site}
          ? $rparam->{target_site}
          : BigMed::Site->fetch( $rparam->{target_site} );
        if ( !$nsite ) {
            return if !defined $nsite;
            return $orig->set_error(
                head => 'BM_No such site',
                text => ['BM_TEXT_No such site', $rparam->{target_site}],
            );
        }
        
        #original site
        if ( ref $rparam->{source_site}
            && $rparam->{source_site}->id == $orig->site )
        { #already have the site object
            $osite = $rparam->{source_site};
        }
        else {
            defined( $osite = BigMed::Site->fetch( $orig->site ) )
              or return;
            return $orig->set_error(
                head => 'BM_No such site',
                text => ['BM_TEXT_No such site', $orig->site],
              )
              if !$osite;
        }

    }
    if ($nsite) { #got site objects
    
        #revise sections to match new site
        my @new_sec;
        foreach my $secid ( $orig->sections ) {
            my $sec = $osite->section_obj_by_id($secid) or next;
            if ($sec->is_homepage) { #e.g. when cloning a site and get home
                push @new_sec, $nsite->homepage_id;
                next;
            }
            my $slug = $sec->slug or next;
            my $nsec = $nsite->section_obj_by_slug($slug) or next;
            push @new_sec, $nsec->id;
        }
        if (!@new_sec) { #no matching slugs
            my $home = $nsite->homepage_obj or return;
            my $first_sec = ($home->kids)[0];
            @new_sec = ($first_sec) if $first_sec;
        }
        $clone->set_sections(\@new_sec);      
    }
    
    #copy related objects
    my %omit =
      $rparam->{omit_rel} ? map { $_ => 1 } @{ $rparam->{omit_rel} } : ();
    foreach my $rel ( $orig->data_relationships ) {
        next if $omit{$rel};
        my @obj = $orig->load_related_objects($rel) or next;
        my %relationship = $orig->relationship_info( $rel, $orig->subtype );
        if ( $relationship{points} ) {    #pointed minicontent/library item
            #copy minicontent object only if moving to a new site and object
            #does not already exist at that site.
            foreach my $pair (@obj) {
                my $lib = $pair->[1] or next;
                if ($nsite) {
                    $lib =
                      $lib->exists_at_site( $nsite,
                        { source_site => $osite } )
                      || $lib->copy(
                        {   source_site => $osite,
                            target_site => $nsite,
                        }
                      );
                    return if !$lib;    #i/o error
                }
                $clone->save_object_link(
                    type       => $rel,
                    linked_obj => $lib,
                    metadata   => { $pair->[0]->metadata },
                  )
                  or return;                         #i/o error
            }

        }
        else {    #"has" relationship
            foreach my $obj (@obj) {
                my $clone_obj = $obj->copy(
                    {   source_site => $osite,
                        target_site => $nsite,
                    }
                  )
                  or return; #i/o error
                $clone->save_object_link(
                    type       => $rel,
                    linked_obj => $clone_obj,
                  )
                  or return; #i/o error
            }
        }
    }
    return 1;
}

###########################################################
# TRASH AND TRASH_ALL CLEANUP
###########################################################

BigMed::Content->add_callback( 'before_trash', \&_before_trash );
BigMed::Content->add_callback( 'after_trash',  \&_after_trash_or_trash_all );
BigMed::Content->add_callback( 'before_trash_all', \&_before_trash_all );
BigMed::Content->add_callback( 'after_trash_all',
    \&_after_trash_or_trash_all );

sub _before_trash {
    my $obj     = shift;
    my $site_id = $obj->site;

    #note sections to rebuild, if any
    my %build_sections;
    %build_sections = map { $_ => 1 } $obj->sections
      if $obj->pub_status && $obj->pub_status eq 'published';

    #note detail pages to delete, if any
    my @remove_pages;
    my @rebuild_tags;
    if ( $obj->isa('BigMed::Content::Page') ) {
        my $slug = $obj->slug;
        $slug = 'index'
          if !defined $slug && $obj->subtype && $obj->subtype eq 'section';
        @remove_pages = ( [$slug, $obj->id, [$obj->sections]] )
          if $slug && $obj->pub_status eq 'published';
        @rebuild_tags = map { $_->[1] } $obj->load_related_objects('tag');

        require BigMed::Search::Scheduler;
        import BigMed::Search::Scheduler;
        schedule_deindex($site_id, $obj) or return;
    }

    require BigMed::Site;
    my $site = BigMed::Site->fetch($site_id) or return;

    #clean up related objects and links to the page, if applicable
    my $class = ref $obj;
    $class->_trash_related_objects( $site, [$obj->id], [$obj->subtype] )
      or return;

    my ( $rlinking_pages, $rlinking_sections ) =
      $class->_trash_links_to_pages( $site, [$obj->id] );
    return if !defined $rlinking_pages;    #i/o error
    $build_sections{$_} = 1 foreach @{$rlinking_sections};

    #stow the collected info in the object
    $obj->set_stash(
        'CONTENT_build_pages'    => $rlinking_pages,
        'CONTENT_build_sections' => [keys %build_sections],
        'CONTENT_remove_pages'   => \@remove_pages,
        'CONTENT_site'           => $site,
        'CONTENT_rebuild_tags'   => \@rebuild_tags,
    );
    return 1;                              #okay to trash the content objects
}

sub _before_trash_all {
    my $select = shift;   #driver object with BigMed::Content/subclass objects
    return 1 if !$select->count;    #none to trash
    my $class   = $select->selection_class();
    my $is_page = $class->isa('BigMed::Content::Page'); #er, bad encapsulation

    #mark items, sections, pages for builder
    my %build_sections;
    my @remove_pages;
    my @rebuild_tags;
    my @delete_ids;
    my %subtype;
    my $item;
    my $site_id;
    $select->set_index(0);    #in case it's been used before
    while ( $item = $select->next ) {
        $site_id ||= $item->site;
        push @delete_ids, $item->id;
        my $subtype = $item->subtype || q{};
        $subtype{$subtype} = 1 if $subtype;
        if ( $item->pub_status && $item->pub_status eq 'published' ) {
            $build_sections{$_} = 1 for ( $item->sections );
        }
        if ($is_page) {       #mark detail page for deletion if valid slug
            my $slug = $item->slug;
            $slug = 'index' if !defined $slug && $subtype eq 'section';
            push @remove_pages, [$slug, $item->id, [$item->sections]]
              if $slug && $item->pub_status eq 'published';
            push @rebuild_tags,
              map { $_->[1] } $item->load_related_objects('tag');
       }
    }
    return if !defined $item;    #i/o error
    return 1 if !@delete_ids;

    require BigMed::Site;
    my $site = BigMed::Site->fetch($site_id) or return;

    #clean up related objects and links to the page, if applicable
    $class->_trash_related_objects( $site, \@delete_ids, [keys %subtype] )
      or return;

    my ( $rlinking_pages, $rlinking_sections ) =
      $class->_trash_links_to_pages( $site, \@delete_ids );
    return if !defined $rlinking_pages;    #i/o error
    $build_sections{$_} = 1 foreach @{$rlinking_sections};
    
    if ($is_page) {
        require BigMed::Search::Scheduler;
        import BigMed::Search::Scheduler;
        schedule_deindex($site, $select) or return;
    }

    #collected all the info and deleted all related objects;
    #stow the collected info in the select object
    $select->set_stash(
        'CONTENT_build_pages'    => $rlinking_pages,
        'CONTENT_build_sections' => [keys %build_sections],
        'CONTENT_remove_pages'   => \@remove_pages,
        'CONTENT_site'           => $site,
        'CONTENT_rebuild_tags'   => \@rebuild_tags,
    );

    return 1;                              #okay to trash the content objects
}

sub _trash_links_to_pages {
    my ( $class, $site, $rids ) = @_;
    $rids = [$rids] if ref $rids ne 'ARRAY';
    return ( [], [] ) if !$class->isa('BigMed::Content::Page');
    my @build_pages;

    #find and remove any links *to* the pages and mark the source
    #pages for building
    my $sid = $site->id;
    my @url = map { "bm://$sid/$_" } @{$rids};
    my $url;
    my %linking_page;
    my %linking_section;

    my $links = BigMed::URL->select( { site => $sid, url => \@url } )
      or return;
    while ( $url = $links->next ) {
        my $source = $url->link_source_obj;
        next if !$source || $source->pub_status ne 'published';
        $linking_page{ $source->id } = 1
          if $source->isa('BigMed::Content::Page');
        $linking_section{$_} = 1 for ( $source->sections );
    }
    return if !defined $url;

    $links->trash_all or return;
    return ( [keys %linking_page], [keys %linking_section] );
}

sub _trash_related_objects {
    my ( $class, $site, $rids, $rsubtypes ) = @_;
    $rids = [$rids] if ref $rids ne 'ARRAY';
    $rsubtypes ||= [];

    #gather points_to objects for all objects to delete
    my $pointers = BigMed::Pointer->select(
        {   site         => $site->id,
            source_table => $class->data_source,
            source_id    => $rids,
        }
      )
      or return;

    #gather any tags to do orphan cleanup later
    require BigMed::Tag;
    my $tag_pointers = $pointers->select(
        { site         => $site->id },
        { target_table => BigMed::Tag->data_source },
      )
      or return;
    my %tag;
    while ( my $rindex = $tag_pointers->next_index ) {
        next if $tag{ $rindex->{target_id} };
        my $tag =
          BigMed::Tag->fetch(
            { site => $rindex->{site}, id => $rindex->{target_id} } );
        return if !defined $tag;
        $tag{ $rindex->{target_id} } = $tag if $tag;
    }

    #all clear to delete (have to delete before checking orphans)
    $pointers->trash_all or return;
    
    #check the tag orphans
    if (%tag) {
        BigMed::Tag->cleanup_orphans( [ values %tag ] ) or return;
    }

    #delete all has relationships
    foreach my $subtype ( undef, @{$rsubtypes} ) {
        foreach my $rel ( $class->data_relationships($subtype) ) {
            my %info = $class->relationship_info( $rel, $subtype );
            next if $info{points};
            my %criteria = ( site => $site->id, $info{id_column} => $rids );
            $criteria{ $info{source_column} } = $class->data_source
              if $info{source_column};
            my @rel_class = $class->load_related_classes( $rel, $subtype );
            foreach my $rel_class (@rel_class) {
                my $rel_objects = $info{class}->select( \%criteria )
                  or return;
                $rel_objects->trash_all or return;
            }
        }
    }
    
    #for pages, take care of pageversions
    if ($class->isa('BigMed::Content::Page')) {
        my $pv = BigMed::PageVersion->select({site=>$site->id, page=>$rids});
        $pv->trash_all or return;
    }
    return 1;
}

sub _after_trash_or_trash_all {    #update the live site with changes
    my $select_or_obj = shift;     #can be either selection or object
    return 1
      if $select_or_obj->isa('BigMed::Driver') && !$select_or_obj->count;
    my @build_pages    = @{ $select_or_obj->stash('CONTENT_build_pages') };
    my @build_sections = @{ $select_or_obj->stash('CONTENT_build_sections') };
    my @remove_pages   = @{ $select_or_obj->stash('CONTENT_remove_pages') };
    my $site           = $select_or_obj->stash('CONTENT_site');
    my @rebuild_tags   = @{ $select_or_obj->stash('CONTENT_rebuild_tags') };

    $select_or_obj->set_stash(     #reset
        'CONTENT_build_pages'    => undef,
        'CONTENT_build_sections' => undef,
        'CONTENT_remove_pages'   => undef,
        'CONTENT_site'           => undef,
        'CONTENT_rebuild_tags'   => undef,
    );
    
    return 1 if !@build_sections && !@remove_pages;

    require BigMed::Builder;
    my $builder = BigMed::Builder->new( site => $site ) or return;

    #kill orphaned detail pages
    foreach my $rargs (@remove_pages) {
        $builder->remove_old_files( $rargs->[0], $rargs->[2] ) or return;
        BigMed::Comment->trash_comment_file( $site, $rargs->[1] ) or return;
    }

    my ( $no_detail, $pages );
    if (@build_pages) {
        $pages = \@build_pages;
    }
    else {
        $no_detail = 1;
    }

    #rebuild other affected pages
    return $builder->build(
        pages     => $pages,
        sections  => \@build_sections,
        no_detail => $no_detail,
        defer_overflow => 1,
        force_tags => \@rebuild_tags,
    );
}

###########################################################
# METADATA ACCESSORS
###########################################################

sub content_classes {
    my $class = shift;
    return sort { lc $a cmp lc $b } keys %content_class;
}

sub content_types {
    my $class = shift;
    return sort map { $_->data_label } keys %content_class;
}

sub content_class_exists {
    return exists $content_class{ $_[1] };
}

sub content_subtypes {
    my $class = shift;
    exists $content_class{$class} || croak "No such content class '$class'";
    return
      sort { lc $a cmp lc $b } keys %{ $content_class{$class}->{subtypes} };
}

sub default_subtype {    #returns undef if no subtype exists
    my $class = shift;
    exists $content_class{$class} || croak "No such content class '$class'";
    return $content_class{$class}->{default_subtype};
}

sub subtype_exists {
    my $self    = shift;
    my $class   = ref $self || $self;
    my $subtype = shift or return;
    exists $content_class{$class} || croak "No such content class '$class'";
    return ( $content_class{$class}->{subtypes}
          && $content_class{$class}->{subtypes}->{$subtype} ) ? 1 : 0;
}

sub data_relationships {    #arguments are class and content subtype
    my $self           = shift;
    my $subtype        = shift || ( ref $self ? $self->subtype : undef );
    my $rrelationships = _relationship_hash_reference( $self, $subtype );
    return sort { lc $a cmp lc $b } keys %{$rrelationships};
}

sub content_hook {
    my $self    = shift;
    my $subtype = shift || ( ref $self ? $self->subtype : undef );
    my $class   = ref $self || $self;
    return $subtype
      ? $content_class{$class}->{subtypes}->{$subtype}->{content_hook}
      : $content_class{$class}->{content_hook};
}

sub relationship_info {
    my $self              = shift;
    my $relationship_name = shift
      or croak 'No relationship name specified to retrieve relationship info';
    my $content_subtype = shift || ( ref $self ? $self->subtype : undef );

    my $class = ref $self || $self;
    my $rrelationships =
      _relationship_hash_reference( $class, $content_subtype );
    return () if !$rrelationships->{$relationship_name};
    return %{ $rrelationships->{$relationship_name} };
}

###########################################################
# UTILITIES
###########################################################

sub generate_slug {
    my $content = shift;
    return q{} if !$content->can('slug');
    if ( !defined $content->slug || $content->slug eq q{} ) {
        my $slug = lc $content->title || q{};
        $slug =~ s/<[^>]+>//msg;
        $slug =~ s/&[^;]*;//msg;
        $slug =~ s/[^0-9a-zA-Z\- ]//msg;
        $slug =~ s/\s+/-/msg;
        $slug = substr( $slug, 0, 50 ) if length($slug) > 50;
        if ( $slug !~ /[0-9a-zA-Z]/ms || $slug eq q{} ) {
            $content->update_id or return;
            $slug = $content->id;
        }
        $content->set_slug($slug);
    }
    while ( ( my $not_unique = !$content->is_unique('slug') )
        || $content->slug =~ /\A\s*index\s*\z/msi )
    {
        return if !defined $not_unique;    #i/o error from driver
        my $slug = $content->slug;
        if ( $slug =~ /\-(\d+)$/ms ) {
            my $suffix = $1;
            $suffix++;
            $slug =~ s/\-\d+$/\-$suffix/ms;
        }
        else {
            $slug .= '-2';
        }
        $content->set_slug($slug);
    }
    return $content->slug;
}

#effective_section and effective_active_section are both wrappers to the
#internal _find_matching_section (active_only parameter the only diff).

sub effective_section {
    my ( $obj, $site, $section, $rkids ) = @_;
    croak 'effective_section requires site object' if !ref $site;
    return _find_matching_section( $obj, $site, $section, $rkids );
}

sub effective_active_section {
    my ( $obj, $site, $section, $rkids ) = @_;
    croak 'effective_active_section requires site object' if !ref $site;
    return _find_matching_section( $obj, $site, $section, $rkids, 'active_only' );
}

sub _find_matching_section {
    my ( $obj, $site, $section, $rkids, $active_only ) = @_;
    return q{}
      if $active_only && $section && !$site->is_section_active($section);
    
    my @sections = $obj->sections or return q{};
    my $sec_id = ref $section ? $section->id : $section;
    if ($sec_id) { #return matching section or subsection
        my %check = map { $_ => 1 } @sections;
        if ( !$check{$sec_id} ) {    #check for descendants
            if (!$rkids) {
                my @kids = $active_only
                         ? $site->all_active_descendants_ids($sec_id)
                         : $site->all_descendants_ids($sec_id);
                $rkids = { map { $_ => 1 } @kids };
            }
            my $child;
            foreach (@sections) {
                $child = $_, last if $rkids->{$_};
            }
            $sec_id = $child || q{};
        }
        return $sec_id;
    }
    
    #no section specified, find first available section
    if ($active_only) {
        foreach my $sid (@sections) {
            $sec_id = $sid, last if $site->is_section_active($sid);
        }
        return q{} if !$sec_id; #no active sections
    }
    else {
        $sec_id = $sections[0];
    }
    return $sec_id;
}

sub editable_by_user {
    my $content = shift;
    my $user = shift or return 1;    #allowed if no user specified
    my $level = $content->_base_privileges_for_user($user);
    return 1 if $level > 4;    #admins and webmasters can edit anything

    my $is_owner = !defined $content->owner || $content->owner == $user->id;
    my $is_published =
      $content->pub_status && $content->pub_status eq 'published';
    if ( $level < 3 && ( !$is_owner || $is_published ) ) { #writers and guests
        return undef; #explicitly return undef
    }
    elsif ( $level == 3 && $is_published ) {               #editor
        return undef; #explicitly return undef
    }
    return 1;
}

sub publishable_by_user {
    my $content = shift;
    my $user = shift or return 1;    #allowed if no user specified
    return ( $content->_base_privileges_for_user($user) > 3 );
}

sub _base_privileges_for_user {
    my $content = shift;
    my $user    = shift or return 0;
    my $site    = $content->site;
    my $level   = $user->privilege_level($site);
    return $level if $level > 4;    #webmasters and admins have sitewide privs

    #find lowest permission level of all of the content's sections
    my @sections = $content->sections;
    @sections = (undef) if @sections == 0;
    return $user->privilege_level( $site, \@sections );
}

###########################################################
# EDITOR FIELDS
# define order and info to present for user editing
###########################################################

my $default_editor_fields = [
    {   title  => 'CONTENT_FS_Content',
        fields => [
            { column     => 'title', required => 1, },
            { custom_obj => 1, },
            { column     => 'content', },
        ],
    },
    {   title  => 'CONTENT_FS_Media',
        id     => 'bmfsMedia',
        fields => [{ relation => 'media' }],
    },
    {   title  => 'CONTENT_FS_Category',
        fields => [
            {   column    => 'sections',
                prompt_as => 'select_section',
                description => 'CONTENT_DESC_Section',
                required  => 1,
                multiple  => 10,
                homepage  => 1,
            },
        ],
    },
    {   title  => 'CONTENT_FS_Publish Info',
        fields => [
            { column => 'pub_status', },
            {   column      => 'priority',
                description => 'CONTENT_DESC_Priority',
                prompt_as   => 'priority_slider',
            },
            {   column          => 'owner',
                required        => 1,
                prompt_as       => 'value_list',
                prompt_callback => \&BigMed::Content::owner_prompt_params,
            },
            {   column          => 'auto_pub_time',
#use default start year: 20 years back from current year
#                start_year      => ( localtime(time) )[5] + 1900,
                option_label    => 'CONTENT_Schedule publication time',
                prompt_callback => sub {
                    auto_time_prompt_params( @_, 'auto_pub_time' );
                },
                parse_callback => \&auto_time_parse_params,
            },

            {   column          => 'auto_unpub_time',
                start_year      => ( localtime(time) )[5] + 1900,
                option_label    => 'CONTENT_Schedule unpublication time',
                prompt_callback => sub {
                    auto_time_prompt_params( @_, 'auto_unpub_time' );
                },
                parse_callback => \&auto_time_parse_params,
            },
        ],
    },
];

sub editor_fields {
    my $obj_or_class = shift;
    my $class = ref $obj_or_class || $obj_or_class;
    return @{$default_editor_fields} if $class eq 'BigMed::Content';
    my $rclass_info = $content_class{$class}
      || croak "No such content class '$class'";

    my $subtype = shift
      || ( ref $obj_or_class ? $obj_or_class->subtype : undef );
    my $rsubtype_hash =
      $subtype ? $rclass_info->{subtypes}->{$subtype} : undef;

    #use subtype's edit fields, if any, or use fields for class
    my $redit_fields =
      ( $rsubtype_hash && $rsubtype_hash->{editor_fields} )
      ? $rsubtype_hash->{editor_fields}
      : (    $rclass_info->{editor_fields}
          || $class->_inherited_editor_fields() );

    #determine custom relations if it's a subtype
    my @custom_relations;
    my @relations = $class->data_relationships();
    if ($subtype) {
        my @subtype_relations = $obj_or_class->data_relationships($subtype);
        my %extra;
        @extra{@subtype_relations} = ();
        delete @extra{@relations};
        @custom_relations = sort { lc $a cmp lc $b } keys %extra;

        @relations = @subtype_relations;
    }

    #gather suppressed fields if any
    my %suppress;
    my @hide_fields = $obj_or_class->_suppressed_fields($subtype);
    @suppress{@hide_fields} = (1) x @hide_fields;

    #run through the default values, adding custom objects
    #and removing suppressed fields or relations
    my @fieldsets;
    my %has_relation;
    @has_relation{@relations} = (1) x @relations;
    foreach my $rfieldset ( @{$redit_fields} ) {
        my %fieldset = %{$rfieldset};    #deref so we don't meddle w/original
        my @def_fields = $fieldset{fields} ? @{ $fieldset{fields} } : ();
        my @fields;
        foreach my $rfield (@def_fields) {
            my %field = %{$rfield};
            if ( $field{custom_obj} && @custom_relations )
            {                            #insert custom objects
                push @fields, map { { relation => $_ } } @custom_relations;
            }
            elsif ( $field{media} ) {    #media fieldset
                push @fields, \%field if $has_relation{media};
            }
            elsif (( $field{relation} && $has_relation{ $field{relation} } )
                || ( $field{column} && !$suppress{ $field{column} } ) )
            {
                push @fields, \%field;
            }
        }
        if (@fields) {
            $fieldset{fields} = \@fields;
            push @fieldsets, \%fieldset;
        }
    }
    return @fieldsets;
}

sub post_parse {
    my ( $content, $rfields ) = @_;
    my $class   = ref $content;
    my $coderef = $content_class{$class}->{post_parse};
    return $coderef ? $coderef->( $content, $rfields ) : 1;
}

sub _suppressed_fields {    #arguments are class and content subtype
    my $obj_or_class = shift;
    my $class        = ref $obj_or_class || $obj_or_class;
    my $rclass_info  = $content_class{$class}
      or croak "No such content class '$class'";
    my $subtype = shift
      || ( ref $obj_or_class ? $obj_or_class->subtype : undef );
    $subtype = $rclass_info->{subtypes}->{$subtype} if $subtype;

    my $rsuppress =
      ( $subtype && $subtype->{suppress} )
      ? $subtype->{suppress}
      : $rclass_info->{suppress};

    return ( $rsuppress && ref $rsuppress ) ? @{$rsuppress}
      : $rsuppress ? ($rsuppress)
      : ();
}

sub _inherited_editor_fields {
    my $class = shift;
    $content_class{$class} or croak "No such content class '$class'";
    my $parent_class;
    {
        no strict 'refs';
        $parent_class = ${"${class}::ISA"}[0];
    }

    if ( $class eq 'BigMed::Content' || $parent_class eq 'BigMed::Content' ) {
        return $default_editor_fields;
    }
    elsif ( $content_class{$parent_class}->{editor_fields} ) {
        return $content_class{$parent_class}->{editor_fields};
    }
    else {
        return $parent_class->inherited_editor_fields();
    }

}

###########################################################
# RELATIONSHIP METHODS
###########################################################

sub _relationship_hash_reference {
    my $class = shift;
    $class = ref $class if ref $class;
    my $content_type = shift;
    exists $content_class{$class}
      || croak
      "No such content class '$class'; did you forget to register it?";

    if ( $content_type
        && !$content_class{$class}->{subtypes}->{$content_type} )
    {
        croak "No such subtype '$content_type' in content class $class";
    }

    return $content_type
      ? $content_class{$class}->{subtypes}->{$content_type}->{relationships}
      : $content_class{$class}->{relationships};
}

sub save_related_info {
    my $content = shift;
    my %param   = @_;
    my ( $relation, $data_type, $parser, $data_id, $user ) =
      @param{qw(relation_name data_type parser data_id user)};
    if ( ref $parser ne 'CODE' ) {
        croak 'save_related_info requires parser code ref to read user input';
    }
    if ( !$data_type ) {
        croak 'save_related_info requires a data type for the related obj';
    }
    if ( !$content->editable_by_user($user) ) {
        return $content->set_error(
            head => 'CONTENT_Not Allowed To Do That',
            text =>
              'CONTENT_TEXT_You do not have permission to edit that content',
        );
    }

    #validate the relationship and class info
    my $content_type = $content->subtype || $content->data_label;
    my %relationship = $content->relationship_info($relation)
      or return $content->set_error(
        head => 'CONTENT_Unknown Data Relationship',
        text => [
            'CONTENT_TEXT_Unknown Data Relationship', $relation,
            $content_type,
        ],
      );
    my $data_class = $content->related_obj_class( $relation, $data_type )
      or return;

    my $field_suffix = "_$relation-$data_type";
    $field_suffix .= "-$data_id" if $data_id;
    my $pointer_id;
    my %metadata;
    my $no_edit;
    my $has_parse_error;
    if ( $relationship{points} ) {    # get metadata and linked object id
        $pointer_id = $data_id;
        if ( $relationship{metadata} ) {
            %metadata =
              _parse_related_fields( $parser, $data_class,
                $relationship{metadata}, $field_suffix );
            $relationship{post_parse}->( \%metadata )
              if $relationship{post_parse};

            #mark any parsing errors for later when we collect all fields
            $has_parse_error =
              _has_related_parse_error( \%metadata, $relationship{metadata},
                $data_class );
        }
        my $tab              = 'BM_TAB' . $field_suffix;
        my $id               = 'BM_LINKED_ID' . $field_suffix;
        my $lib_id           = "BM_LIB_ID_$relation-$data_type";
        my %data_action_meta = $parser->(
            {   id       => $tab,
                parse_as => 'simple_text',
                required => 1,
            },
            {   id         => $id,
                data_class => $data_class,
                column     => 'id',
            },
            {   id         => $lib_id,
                data_class => $data_class,
                column     => 'id',
            },
        );
        $data_action_meta{$tab} ||= 'New';    #default to creating new object
        $data_id =
            $data_action_meta{$tab} eq 'Edit' ? $data_action_meta{$id}
          : $data_action_meta{$tab} eq 'New'  ? undef
          : $data_action_meta{$lib_id};
        if ( $data_action_meta{$tab} eq 'Library' ) {
            $no_edit = 1;
            return $content->set_error(
                head => 'CONTENT_No library item selected',
                text => 'CONTENT_TEXT_No library item selected',
              )
              if !$data_id;
        }
    }

    #load object (display error if doesn't exist)
    my $linked_obj;
    if ($data_id) {
        defined(
            $linked_obj = $data_class->fetch(
                { site => $content->site, id => $data_id }
            )
          )
          or return;    #i/o error
        if ( !$linked_obj ) {    # no such object, complain with error
            my $type = BigMed->bigmed->language($data_type);
            return $content->set_error(
                head => 'CONTENT_Could not find requested item',
                text => ['CONTENT_TEXT_Could not edit mini', $type, $data_id]
            );
        }
        $no_edit = 1 if !$linked_obj->editable_by_user($user);
    }
    else {
        $linked_obj = $data_class->new();
        $linked_obj->update_id or return;
        $linked_obj->set_site( $content->site );
    }

    if ( $no_edit ) {
        #tells editor.pm that there's no need to rebuild pages... no change
        $linked_obj->set_stash('CONTENT_no_edit', 1);
    }
    else {    #update the linked object with submitted values
        $linked_obj->save_submission(
            abbr_fields => 1,
            field_prefix => 'BM_MINI_',
            field_suffix => $field_suffix,
            parser => $parser,
            user => $user,
        ) or return;
    }
    
    return if $has_parse_error;    #metadata error, in case of no_edit
    return $content->save_object_link(
        linked_obj => $linked_obj,
        type       => $relation,
        metadata   => \%metadata,
        pointer_id => $pointer_id,
    );    #returns object (or array ref of pointer/object for pointed objects)
}

sub save_object_link {

    # expects a hash of parameter values:
    # $obj->save_object_link(
    #    linked_obj => $object,
    #    type       => 'relationship_type',
    #    metadata   => \%metadata,
    #    pointer_id => $pointer_obj_id,    #optional
    # );

    my $obj = shift;
    if ( !$obj->id ) {    #must have an object id
        $obj->update_id or return;
    }
    $obj->site
      or croak 'save_object_link requires source object to have a site id';

    my %param    = @_;
    my $relation = $param{type}
      or croak 'save_object_link requires type parameter (relationship name)';
    my $target = $param{linked_obj}
      or croak 'save_object_link requires linked_obj param (object for link)';

    #load the info about this relationship type
    my $subtype      = $obj->subtype;
    my $class        = ref $obj;
    my %relationship = $class->relationship_info( $relation, $subtype );
    my %class_check;
    my @classes = $obj->load_related_classes($relation);
    @class_check{@classes} = ();
    my $target_class = ref $target;
    if ( !exists $class_check{$target_class} ) {
        croak "save_object_link: target object ($target_class) not a "
          . "'$relation' relationship class";
    }

    $target->set_site( $obj->site ) if $target->can('set_site');

    #reset relationship stash
    $obj->set_stash("_CONTENT::RELATEDOBJ_$relation", undef);


    #if this is a simple 'has' relationship, put content object's info
    #into appropriate columns and stop
    if ( !$relationship{points} ) {
        my $id_method = "set_$relationship{id_column}";
        $target->$id_method( $obj->id );
        if ( $relationship{source_column} ) {
            my $class_method = "set_$relationship{source_column}";
            $target->$class_method( $obj->data_source );
        }
        $target->save or return;
        return $target;
    }

    #otherwise, create or fetch the pointer object
    my ( $pointer_obj, $wrong_id, $wrong_source );
    if ( $param{'pointer_id'} ) {    #confirm pointerid , or start over
        defined(
            $pointer_obj = BigMed::Pointer->fetch(
                { site => $obj->site, id => $param{'pointer_id'} }
            )
          )
          or return;
        if ($pointer_obj) {
            $wrong_id     = ( $pointer_obj->source_id != $obj->id );
            $wrong_source =
              ( $pointer_obj->source_table ne $obj->data_source );
        }
    }
    if ( !$pointer_obj || $wrong_id || $wrong_source ) {
        $pointer_obj = BigMed::Pointer->new();
    }
    $target->save or return;

    #set the info and save the pointer
    my %metadata =
      ref $param{metadata} eq 'HASH' ? %{ $param{metadata} } : ();
    $pointer_obj->set_source_table( $obj->data_source );
    $pointer_obj->set_source_id( $obj->id );
    $pointer_obj->set_target_table( $target->data_source );
    $pointer_obj->set_target_id( $target->id );
    $pointer_obj->set_site( $obj->site );
    $pointer_obj->set_type($relation);
    $pointer_obj->set_metadata( \%metadata );

    #reset the pointer cache for the object and save
    $pointer_obj->reset_obj_cache($obj);    #
    return defined( $pointer_obj->save ) ? [$pointer_obj, $target] : undef;
}

sub load_related_classes {
    my $content  = shift;                   #object or class
    my $relation = shift
      or croak 'usage: $obj->load_related_classes($relationship_name)';
    my $subtype = shift || ( ref $content ? $content->subtype : undef );
    my %relationship = $content->relationship_info( $relation, $subtype )
      or return ();                         #no such relationship

    my $class   = $relationship{class};
    my @classes =
        ref $class eq 'ARRAY' ? @{$class}
      : ref $class eq 'CODE'  ? ( $class->() )
      : ($class);
    return ( grep { BigMed->load_required_class($_) } @classes );
}

sub related_obj_class {
    my $self = shift;
    my ( $relation, $data_type, $subtype ) = @_;
    $subtype ||= ( ref $self ? $self->subtype : undef );
    my @valid_classes = $self->load_related_classes( $relation, $subtype );
    my $data_class;
    foreach my $related_class (@valid_classes) {
        if ( $related_class->data_label eq $data_type ) {
            $data_class = $related_class;
            last;
        }
    }
    return $data_class || $self->set_error(
        head => 'CONTENT_Unknown Data Relationship',
        text => [
            'CONTENT_TEXT_Unknown Data Relationship',
            $data_type,
            ( $self->subtype || $self->data_label ),
        ],
    );
}

my %CLASS_LOOKUP;
sub load_related_objects {
    my $content = shift;
    return () if !$content->id || !$content->site();
    my $relation = shift
      or croak 'usage: $obj->load_related_objects($relationship_name)';
    my $rstashed = $content->stash("_CONTENT::RELATEDOBJ_$relation");
    return @{$rstashed} if $rstashed;

    my $rcache = shift;
    $content->load_related_classes($relation) or return ();
    my @objects;
    my %relationship =
      $content->relationship_info( $relation, $content->subtype );
    return () if !%relationship;
    if ( $relationship{points} ) {
        my $pcache = $rcache ? $rcache->{'BigMed::Pointer'} : undef;
        my $all_pointers = BigMed::Pointer->obj_pointers($content, $pcache)
          or return ();    #fetch from cache

#       OPTION ONE: Slightly slower (modest difference) but more "correct":
#       easier to read, doesn't violate encapsulation:
        my $rel_pointers = $all_pointers->select( { type => $relation },
            { limit => $relationship{limit_num} } )
          or return ();
        my $point;
        while ( $point = $rel_pointers->next ) {
            defined( my $obj = $point->fetch_target() ) or return ();
            push @objects, [$point, $obj] if $obj;
        }
        return if !defined $point;    #error

#       OPTION TWO: Only slightly faster, but higher obscurity and doing
#       the job that the driver and pointer class should take care of.
#        $all_pointers->set_index(0);
#        my $limit = $relationship{limit_num};
#        while ( my $rindex = $all_pointers->next_index ) {
#            next if !$rindex->{type} || $rindex->{type} ne $relation;
#            last if $limit && $limit <= @objects;
#            my $class =
#              ( $CLASS_LOOKUP{ $rindex->{target_table} }
#                  ||= $content->source_class( $rindex->{target_table} ) );
#
#            defined(
#                my $obj = $class->fetch(
#                    { site => $rindex->{site}, id => $rindex->{target_id} }
#                )
#              )
#              or return;
#            next if !$obj;
#            
#            defined(
#                my $point = BigMed::Pointer->fetch(
#                    { site => $rindex->{site}, id => $rindex->{id} }
#                )
#              )
#              or return;
#            push @objects, [$point, $obj] if $point;
#        }
    }
    else {
        my %criteria = (
            site                     => $content->site,
            $relationship{id_column} => $content->id
        );
        $criteria{ $relationship{source_column} } = $content->data_source
          if $relationship{source_column};
        my $seek = $rcache ? $rcache->{ $relationship{class} } : $relationship{class};
        if (!$seek) { #no cache for relationship class; get it and cache
            $seek = $relationship{class}->select({site=>$content->site});
            $seek->tune( $relationship{id_column} );
            $rcache->{ $relationship{class} } = $seek;
        }
        @objects =
          $seek->fetch( \%criteria, { limit => $relationship{limit_num} } );
    }
    $content->set_stash("_CONTENT::RELATEDOBJ_$relation", \@objects);
    return @objects;    #if empty, could be error
}

sub sorted_related_objects {
    my $obj      = shift;
    my $relation = shift;
    my $rcache   = shift;
    my %info     = $obj->relationship_info($relation);
    my @obj = $obj->load_related_objects($relation, $rcache);
    @obj = $info{sort}->(@obj) if $info{sort};
    return @obj;
}

sub _parse_related_fields {
    my ( $parser, $data_class, $rfields, $suffix ) = @_;
    $suffix ||= q{};
    my %id_map = ( '_ERROR' => '_ERROR' );
    my @fields;    #to hold fields with the long-form BM_MINI ids
    foreach my $rf ( @{$rfields} ) {
        my %info = %{$rf};
        my $dep  = $info{depend};
        next if $dep && $data_class->can($dep) && !$data_class->$dep;
        my $id = $info{id} || $info{column} || q{};
        $id_map{ $info{id} = "BM_MINI_$id$suffix" } = $id;
        $info{data_class} ||= $data_class;
        push @fields, \%info;
    }
    my %results = $parser->(@fields);
    if ( $results{_ERROR} ) {    #revise for actual IDs
        my %rev_error =
          map { ( ( $id_map{$_} || $_ ) => $results{_ERROR}->{$_} ) }
          keys %{ $results{_ERROR} };
        $results{_ERROR} = \%rev_error;
    }
    return map { $id_map{$_} => $results{$_} } keys %results;
}

sub _has_related_parse_error {
    my ( $rparsed, $rfields, $data_class ) = @_;
    return 0 if !$rparsed->{_ERROR};
    my $app = BigMed->bigmed->app;
    my %err = %{ $rparsed->{_ERROR} };
    foreach my $field ( @{$rfields} ) {
        my $id = $field->{column} || $field->{id};
        next if !$err{$id};
        my $ref =
          $app->prompt_field_ref( %{$field}, data_class => $data_class );
        my $label = $ref->[2]->{label} || $id;
        my $msg = $app->language( $err{$id} );
        BigMed::Content->set_error(
            head => 'BM_Trouble_processing_form',
            text => ['CONTENT_parse_error', $label, $msg]
        );
    }
    return 1;
}

###########################################################
# REGISTRATION HELPER ROUTINES
###########################################################

sub _format_content_metadata {    #INTERNAL
    my $parent_class = shift;
    my %arg          = @_;

    #do not currently inherit subtypes when subclass an element. If we
    #decide to do that, probably need to change relationship storage
    #and retrieval. See note in _load_relationship_info.

    my %related_data = $parent_class->_load_relationship_info(%arg);

    return {
        relationships => \%related_data,
        content_hook => ref $arg{content_hook} eq 'CODE'
        ? $arg{content_hook}
        : undef,
        editor_fields =>
          $arg{editor_fields},    #optional array of editable fields
        suppress   => $arg{suppress},   #array of column names to suppress
        post_parse => $arg{post_parse}, #callback after receiving/parsing data
    };

}

sub _load_relationship_info {           #INTERNAL
    my $parent_class = shift;
    my %arg          = @_;

    #get the parent class relationship information and then
    #modify according to submitted arguments.

    #NOTE: A drawback to this approach is that subsequent
    #overrrides to parent values will not get reflected
    #in child values. Not sure if this will be an issue.
    #If it is, a solution would be to store only the
    #changes and then apply them when actually requesting for
    #relationship values via _relationship_hash_reference. This
    #will make sure that any parent changes right up to the
    #request will be taken into consideration. Will also help
    #if we decide to allow subclassing to inherit the parent's
    #subtypes.

    my %related_data = $parent_class->_default_relationships();
    foreach my $relationship qw(has points_to) {
        next unless $arg{$relationship};
        my %data_types =
          ref $arg{$relationship} eq 'HASH'
          ? %{ $arg{$relationship} }
          : croak "'$relationship' definition must be a hash ref";

        foreach my $type ( keys %data_types ) {
            my %type_info =
              ref $data_types{$type} eq 'HASH'
              ? %{ $data_types{$type} }
              : croak "'$type' $relationship definition must be a hash "
              . 'reference in content type definition';

            #require class
            $type_info{'class'}
              || croak "No class name specified in '$type' $relationship "
              . 'relationship definition';

            #format (and de-reference) limit_type if defined
            if ( defined $type_info{'limit_type'} ) {
                my @limit_type =
                  ref $type_info{'limit_type'} eq 'ARRAY'
                  ? @{ $type_info{'limit_type'} }
                  : ( $type_info{'limit_type'} );
                $type_info{'limit_type'} = \@limit_type;
            }

            my %this_relationship;
            @this_relationship{
                'class',    'limit_num', 'limit_type', 'sort',
                'sortable', 'embed',     'required', 'custom_prompt',
                'custom_save',
              }
              = @type_info{
                'class',    'limit_num', 'limit_type', 'sort',
                'sortable', 'embed',     'required', 'custom_prompt',
                'custom_save',
              };

            if ( $relationship eq 'points_to' ) {
                $this_relationship{points}       = 1;
                $this_relationship{metadata}     = $type_info{metadata} || [];
                $this_relationship{preview}      = $type_info{preview} || {};
                $this_relationship{post_preview} = $type_info{post_preview};
            }
            else {
                $type_info{'id_column'}
                  || croak 'No external-object id_column specified in '
                  . "'$type' $relationship relationship definition";
                @this_relationship{ 'id_column', 'source_column' } =
                  @type_info{ 'id_column', 'source_column' };
            }

            $related_data{$type} = \%this_relationship;
        }
    }

    #remove relationships flagged by has_no
    if ( $arg{'has_no'} ) {
        my @has_no =
          ref $arg{'has_no'} eq 'ARRAY'
          ? @{ $arg{'has_no'} }
          : ( $arg{'has_no'} );
        delete @related_data{@has_no};
    }
    return (%related_data);
}

sub _default_relationships {    #INTERNAL
    my $parent_class = shift;
    if ( $parent_class eq 'BigMed::Content' )
    {                           #registering a top-level subclass
        return (
            'media' => {
                class => sub {
                    require BigMed::Media;
                    BigMed::Media->media_classes();
                },
                limit_num  => undef,
                limit_type => undef,
                points     => 1,
                preview    => { html => \&_media_preview },
                sort       => \&_media_sort,
                embed      => 1,
                metadata   => [
                    {   id        => 'position',
                        prompt_as => 'body_position',
                        label     => 'CONTENT_MEDIA_Page Position',
                        default   => 'block:1',
                    },
                    {   id        => 'align',
                        prompt_as => 'alignment',
                        label     => 'CONTENT_MEDIA_Alignment',
                        options   => ['default', 'left', 'center', 'right'],
                        default   => 'default',
                    },
                    {   id          => 'priority',
                        prompt_as   => 'priority_slider',
                        default     => '500',
                        parse_as    => 'number_integer_positive',
                        label       => 'CONTENT_MEDIA_Priority',
                        description => 'CONTENT_MEDIA_DESC_Priority',
                    },
                    {   id        => 'caption',
                        prompt_as => 'rich_text_brief',
                        label     => 'CONTENT_MEDIA_Caption',
                    },
                    {   id        => 'hotlink_url',
                        prompt_as => 'url',
                        label     => 'CONTENT_MEDIA_Hotlink_URL',
                        default   => 'http://',
                        depend    => 'can_hotlink',
                    },
                ],
            },
        );
    }

    #otherwise, defining a type of a top-level subclass.
    #get the subclass's definition.

    exists $content_class{$parent_class}
      || croak "'$parent_class' content class is unknown. Did you forget to "
      . 'register it first?';

    #get the default relationships for parent class and de-reference them
    my %relationships = %{ $content_class{$parent_class}->{relationships} };
    foreach my $type ( keys %relationships ) {
        my %definition = %{ $relationships{$type} };
        my @limit_type =
          $definition{limit_type} ? @{ $definition{limit_type} } : ();
        $definition{limit_type} = scalar @limit_type ? \@limit_type : undef;
        $relationships{$type} = \%definition;
    }
    return %relationships;
}

sub _media_preview {
    my ( $app, $item, $robj_preview ) = @_;
    my %meta = $item->[0]->metadata();
    my $obj      = $item->[1];

    #add caption to existing html if indicated
    my $obj_html = $robj_preview->{PREVIEW_HTML} || q{};
    require BigMed::Filter;
    my $caption = BigMed::Filter->filter( $meta{caption} );
    $caption = $obj->sanitize_preview_html($app, $caption);
    $obj_html =~ s/(<div class="bmcpMediaCaption">)/$1$caption/ms;

    #create the status text
    my $links    = q{};
    my $position = q{};
    if ( $meta{link_position} && $meta{link_position} ne 'none' ) {
        my $linkpos =
          $app->language( 'CONTENT_MEDIA_LINK_' . $meta{link_position} );
        $links =
          $app->language( ['CONTENT_Link display:', $linkpos] ) . ' &nbsp; ';
    }
    if ( $meta{position} && $meta{position} ne 'hidden' ) {
        my $text_pos     = $obj->text_position_lang( $meta{position} );
        my $align        = $obj->text_align_lang( $meta{align} );
        my $pos_language =
            $meta{position} eq 'gallery'
          ? $app->language($text_pos)
          : $app->language($align) . q{ } . $app->language($text_pos);
        $position =
          $app->language( ['CONTENT_Page position:', $pos_language] )
          . ' &nbsp; ';
    }
    $links = $app->language('CONTENT_MEDIA_Not displayed') . ' &nbsp; '
      if !$links && !$position;
    my $priority_value = defined $meta{priority} ? $meta{priority} : '500';
    my $priority = $app->language( ['CONTENT_Priority:', $priority_value] );
    return (
        PREVIEW_HTML => $obj_html,
        STATUS_HTML  => "$links $position $priority",
    );
}

my %pos_order = (    #kinda hacky, but it'll do
    gallery => -1,
    above   => 0,
    below   => 777_777_777,
    other   => 888_888_888,
    hidden  => 999_999_999,
);

sub _media_sort {
    return map { $_->[0] }
      sort     {
        $a->[1] <=> $b->[1]         #ascending page order
          || $b->[2] <=> $a->[2]    #descending priority
      }
      map {
        my %meta = $_->[0]->metadata;
        my ( $type, $num ) = split( /:/, ( $meta{position} || 'block:1' ) );
        my $p_rank = $num || $pos_order{$type} || 0;
        [$_, $p_rank, ( $meta{priority} || '500' )];
      } @_;
}

my %time_shift = (
    auto_pub_time   => '+1d',
    auto_unpub_time => '+1m',
);

sub auto_time_prompt_params {
    my ( $app, $content, $col_name ) = @_;

    my $hidden;
    $hidden = 'hidden'
      if !$content->publishable_by_user( $app->current_user );
    
    my $site = $app->current_site;
    my %param = (
        date_format => $site->date_format,
        time_format => $site->time_format,
        offset    => $site->time_offset,
        prompt_as => $hidden,
    );

    if ( $content->$col_name ) {
        return (
            %param,
            optional_time => 1,
        );
    }
    else {
        my $time_obj = BigMed->time_obj;
        $time_obj->shift_time( $time_shift{$col_name} );
        return (
            %param,
            value         => $time_obj->bigmed_time,
            optional_time => 0,
        );
    }
}

sub auto_time_parse_params {
    return ( offset => $_[0]->current_site->time_offset );
}

sub pub_status_params {
    my ( $app, $obj ) = @_;
    my $user = $app->current_user;
    return $obj->publishable_by_user($user) ? ()
      : $obj->editable_by_user($user) ? ( options => [qw(draft edit ready)] )
      : ( prompt_as => 'hidden' );
}

sub owner_prompt_params {
    my ( $app, $content ) = @_;
    my $user = $app->current_user;
    my $site = $app->current_site;
    if ( $user->privilege_level($site) < 3 ) {    #writer or less
        return ( prompt_as => 'hidden' );
    }
    my %label;
    my @users;
    foreach my $u ( $site->users() ) {
        defined( $label{ $u->id } = $u->name ) or next;
        push @users, [$u->id, lc $u->name];
    }
    my @options = map { $_->[0] } sort { $a->[1] cmp $b->[1] } @users;
    return (
        labels  => \%label,
        options => \@options,
    );
}

1;

__END__

=head1 NAME

BigMed::Content - Base class for Big Medium content records

=head1 DESCRIPTION

BigMed::Content provides the interface for registering new, database-backed
content classes and subtypes and is the base class for all Big Medium content
classes. Each content element in Big Medium (pages, tips,
announcements) is an object instance of a BigMed::Content subclass.

=head1 ABOUT CONTENT CLASS DEFINITIONS

Registration of a content class and its subtypes (e.g. BigMed::Page and
subtypes article, event, product, etc) establishes the class as a
database-backed storage class and also creates an in-memory definition
of the content type, including:

=over 4

=item * Database source and schema for the class's content objects

=item * Links and relationships to external data objects

E.g., event objects, product objects, media objects, etc.

=item * Data field information

Whether fields are user-editable, which fields are required, etc.

=item * Editing organization

Categories and order of field presentation for the editing screen.

=item * Callback routines for custom field validation if necessary

=back

=head2 Data schema

BigMed::Content subclasses are also subclasses of BigMed::Data which allows
them to save and retrieve objects from disk.

Every subclass of the BigMed::Content class inherits the following data
columns, which may be further extended at registration. They can be
accessed and set using the standard data access methods described in the
BigMed::Data documentation. See the L<"Searching and Sorting"> section below
for details on the data columns available to search and sort BigMed::Content
objects.

=over 4

=item * id

The content object's unique ID

=item * site

ID of the site (BigMed::Site) to which the content belongs

=item * sections

Array of sections (BigMed::Section) to which the content belongs; the first
item in the list is the content object's primary section.

=item * owner

The id of the user (BigMed::User) who owns the content.

=item * pub_status

The publication status of the content. The status value should be one of five
values: draft, edit, ready, published, unpublished.

=item * priority

Integer whose value can be used to affect sort order for articles (the
higher the number, the more important it's considered to be).

=item * pub_time

Timestamp for the time when the content was published, if applicable.

=item * auto_pub_time

Timestamp indicating when the content should be published.

=item * auto_unpub_time

Timestamp indicating when the content should be unpublished.

=item * version

Integer indicating the revision number of the content.

=item * title

The content title.

=item * content

The article's full content.

=item * flags

=item * mod_time

Timestamp for when the last time the object was saved.

=item * create_time

Timestamp for when the object was first created.

=back

=head3 C<trash> and C<trash_all> methods

When the C<trash> and C<trash_all> data methods are called on BigMed::Content
objects, a few additional steps are taken in addition to deleting the object
from the data store:

=over 4

=item * Any links and content for the object are removed from the live site.

=item * Any related objects are deleted; in the case of "points_to" related
objects, the pointer relationship is removed but the target object is
left in the library to be used for other relationships.

=item * For page objects, all associated PageVersion objects are removed.

=back

=head3 Searching and Sorting

You can look up and sort records by any combination of the following fields.
See the C<fetch> and C<select> documentation in BigMed::Data for more info.

=over 4

=item * id

=item * mod_time

=item * site

=item * sections

=item * owner

=item * pub_status

=item * priority

=item * pub_time

=item * auto_pub_time

=item * auto_unpub_time

=item * version

=item * title

=back

=head3 Copying content objects

BigMed::Content extends BigMed::Data's C<copy> method by copying
all of the related data objects and their associated source files
where necessary. The method returns the cloned, unsaved content object
on success, or undef on failure.

    $clone = $content->copy(
        {   source_site => $site1,
            target_site => $site2,
            omit_rel    => ['comment'],
        }
    );

In addition to the value changes noted in the documentation for
BigMed::Data's C<copy> method, the returned object also has its
version column reset to 1 and, in the case of BigMed::Content::Page objects,
has its slug name set to a unique value.

The optional hash reference can contain three parameters. All are optional:

=over 4

=item * C<<target_site => $site_id_or_obj>>

For performance, it's recommended that you include this parameter and
provide a site object. This is the site to which you would like to copy
the document object. This can be a site id or object, and if no value is
provided, the document is copied to the same site as the original.

=item * C<<source_site => $site_id_or_obj>>

For performance, it's recommended that you include this parameter and
provide a site object. This is the site of the original document object.

=item * C<<omit_rel => ['relname1', 'relname2']>>

An array reference of relationships whose objects you do not wish to copy.
The individual values are the names/labels of the relationships to omit.
If not provided, all related objects will be copied.

=back

=head2 Related objects

BigMed::Content subclasses can establish relationships with other database-
backed objects. These relationships fall into four categories:

=over 4

=item * has

The subclass may be directly associated with one or more object instances of the
related class. The related objects have the object's ID stored in a specified
data column.

=item * points_to

The subclass may be associated with one or more object instances of the related
class, but via a third BigMed::Pointer object. This is relevant when the related
object can be associated with other objects in addition to the single content
object.

=back

Every subclass of the BigMed::Content class inherits the following data
relationships, which may be further extended (or undone) at registration:

=over 4

=item points_to: media (BigMed::Media)

=back

=head1 METHODS

=head2 Content Class and Subtype Registration Methods

=over 4

=item * Foo->register_content_class(%arguments);

Registers a content class definition for class Foo in the system and
establishes the data structure and database table to be used for
persistence. Note that Foo must be a subclass of BigMed::Content (or
one of its subclasses):

    package Foo;
    use base qw(BigMed::Content);
    Foo->register_content_class(%arguments);

The hash of arguments consists of the following key-value pairs:

=over 4

=item * source => $database_source

The name of the data source. This should be a name that is unique
to this subclass and will be used internally by the data driver,
which could use it as a SQL table name, a file directory, etc. It
is recommended to keep this name simple: a lowercase name that
consists only of the letters a-z.

If left undefined, the source name will correspond to the lower-case, plural
version of the last portion of the class name.

    BigMed::MyContent->set_schema();      #source is mycontents
    BigMed::Animal::Dog->set_schema();    #source is dogs

Note that this automatic pluralization is not particularly clever -- it just
adds a "s" to the end of class name, so some class names may result in
poor grammatical constructions:

    BigMed::Person->set_schema();           #source is persons
    BigMed::Person::Woman->set_schema();    #source is womans

    #in these cases, better to specify a source name explicitly
    BigMed::Person->set_schema( source => 'people' );
    BigMed::Person::Woman->set_schema( source => 'women' );

=item * label => "Public Name"

The name to use as an internal identifier (in localization text, for example)
to refer to this subclass within Big Medium.  If left undefined, the source
name will correspond to the lower-case version of the last portion
of the class name -- same treatment as source, above, but not pluralized.

    BigMed::Person->set_schema();          #label is person
    BigMed::Media::Image->set_schema();    #label is image

=item * elements => \@data_schema

A reference to the data schema array for the content class. This data schema
is added to the data schema inherited from the parent class. For details on
the composition of the data schema array, see the BigMed::Data documentation.

=item * has => { 'label1' => \%parameters, 'label2' => \%parameters, ... }

Creates a "has" relationship between the class's objects and one or more
other types of objects. A "has" relationship means that the object may be
related to exactly one object from each named class, and that object is likewise
related only to this object.

The value is a hash reference where the keys are the label names for the
relationships, and the values are hash references containing the following
parameters:

=over 4

=item * class => $class_name

B<Required.> The class(es) of accepted related objects for this relationship.
May be a scalar (the name of the single related class), or a reference to an
array (one or more class names) or a coderef to a routine that will
return an array of related objects.

    class => 'BigMed::Pullquote'

    class => ['BigMed::Image', 'BigMed::Document']

    class => sub {
        require BigMed::Media;
        BigMed::Media->media_classes();
      }

=item * id_column => $column_name

B<Required.> The name of the column in the external data object that contains
the id of the content object.

=item * source_column => $column_name

B<Optional.> Specifies the name of the column in the external data object that
contains the data source of the content object. If not defined, then the
source name will not be used to look up the related objects. In that case,
it's strongly recommended that the related class not have any 'has'
relationships with any content class other than the current class.

=item * limit_num => $integer

Optional. Limits the number of objects of this class that can be associated
with the content object. If note defined, no limit is applied.

=item * limit_type => \@type_names

Optional. If the related class has multiple types (like BigMed::Media, for example), the
limit_type parameter allows relationships with only the listed types. If
limit_type is not defined, any type may be related.

=item * sort => \&sort_routine

Optional. Creates a callback which receives an array of objects of the type(s)
specified in the class attribute and should return the array back in sorted
order. For now, this is used only for display within editing contexts.

If no sort routine is specified, the editor will display the related objects
in an arbitrary order.

=item * sortable => 1

If true, application editors should allow relationship objects to be sorted
into a custom order via drag-n-drop. By default, the value is applied to
the priority column of the object (so in that case, you should make sure that
the data schema has one), where the first item is assigned a value of 999, the
second 998, etc. You can specify a different sort order and a different
numbering system by making sortable a hash reference:

    sortable => {column=>'column_name', do_value => sub { $_[0] } }

The column value specifies which minicontent object column will receive the
value, and do_value is a callback that receives the field's 1-based order
in the list.

=item * embed => 1

If true, instructs BigMed::Format to embed this relationships objects
within the body text (e.g., pullquotes, images).

=item * required => 1

If true, signals that the content type requires at least one of these
relationship objects.

=back

=item * points_to => { 'label1' => \%parameters, 'label2' => \%parameters, ... }

Creates a "points_to" relationship between the class's objects and one or more
other types of objects. Unlike "has" relationships, the link is not tracked
within the related object itself but instead in a third, BigMed::Pointer object.
This allows relationships between objects that can both have relationships
with other objects.

The value is a hash reference where the keys are the label names for the
relationships, and the values are hash references containing the following
parameters:

=over 4

=item * class => $class_name

B<Required.> The class of the related object.

=item * limit_num => $integer

Optional. Limits the number of objects of this class that can be associated
with the content object. If note defined, no limit is applied.

=item * limit_type => \@type_names

Optional. If the related class has multiple types (like BigMed::Media, for example), the
limit_type parameter allows relationships with only the listed types. If
limit_type is not defined, any type may be related.

=item * embed => 1

If true, instructs BigMed::Format to embed this relationships objects
within the body text (e.g., pullquotes, images).

=item * required => 1

If true, signals that the content type requires at least one of these
relationship objects.

=item * metadata => \@fields

Optional. An array reference of fields that may be stored as metadata for
this relationship type. Each item in the array is represented by a hash
reference suitable to be passed to BigMed::App's
prompt_field_ref method for prompting and to parse_submission for parsing:

In addition to the usual fields, there's also a depend parameter which allows
you to specify a BigMed::MiniContent "can_" class attribute that must be true
for the class in order to display the field (can_link, can_embed,
can_hotlink):

    metadata => [
        {   id        => 'hotlink_url',
            prompt_as => 'url',
            label     => 'CONTENT_MEDIA_Hotlink_URL',
            depend    => 'can_hotlink',
        }
    ]

=item * custom_prompt => 'prompt_name'

By default, all relationship item are prompted for editing using the same
mini_module prompt. To use a different prompt style, enter the prompt
to use here. The usual mini_module parameters are submitted in that
prompt's parameter hash.

Content pages use the custom_prompt for the BigMed::Tag relationship,
for example. In that case, the custom_prompt for tags is set to 'tags'
which then prompts with BigMed::App::Web::Prompt's 'tags' prompt
(displaying the auto-complete interface).

=item * custom_save => \&callback

This goes hand-in-hand with custom_prompt.

By default, when editing relationship data, all items are prompted using
the mini_module prompt (see e.g., BigMed::App::Web::Editor's
C<rm_ajax_save_mini> runmode method). Supplying a custom_save callback
causes the editor to instead run this method when saving.

So, for example, for pages' BigMed::Tag relationship, the page's save_tag
method is called when the page is saved. This method parses and saves
the custom tag field.

=item * post_parse => \&post_parse_routine

An optional coderef to call after the metadata editor_fields fields
have been submitted and parsed but before they are saved. The routine
receives two arguments: the BigMed::App object and
a hash reference to the BigMed::App parse_submission results for the
metadata's editor_fields.

After the callback returns, the parse_submission hash is checked for
errors and, if clear, is saved.

=item * sort => \&sort_routine

Optional. Creates a callback which receives an array of "item" references,
where each item is a two-object array reference; the first object
is the BigMed::Pointer reference (which contains the metadata info) and
the second object is the pointed object. The callback routine should
return the item array in sorted order for the preferred display
within editing contexts.

If no sort routine is specified, the editor will display the related objects
in an arbitrary order.

=item * sortable => 1

If true, application editors should allow relationship objects to be sorted
into a custom order via drag-n-drop. By default, the value is applied to
the priority field in metadata, where the first item is assigned 999, the
second 998, etc. You can specify a different sort order and a different
numbering system by making sortable a hash reference:

    sortable => {column=>'metadata_field', do_value => sub { $_[0] } }

The column value specifies which metadata_field will receive the value,
and do_value is a callback that receives the field's 1-based order in the
list.

=item * preview => { html => \&html_preview }

Optional. Supplements the similar preview attribute for BigMed::MiniContent
attributes, determining the display of related objects in editor contexts.

The preview attribute is a hash reference where the keys are a format-
specific value (html for the BigMed::App::Web editor) and the values
are callbacks to generate preview data for a pointer/object pair.

If supplied, the callback receives three arguments: The application object;
an array reference where the first object is the BigMed::Pointer object
(which contains the metadata info) and the second object is the pointed
object; and the results of the pointed object's BigMed::MiniContent
preview callback, if any.

In the html context, both the relationship preview callback and the
BigMed::MiniContent preview callback are expected to return a hash of
HTML::Template parameters. The relationship callback is called after the
BigMed::MiniContent callback and has the opportunity to modify the
hash generated in the first callback. Specifically, the BigMed::MiniContent
object generates a PREVIEW_HTML parameter that displays the main body of the
object-specific content.

    #we've already gotten a document's preview html with something
    #like this:
    my %document_preview = $doc_preview->( $app, $document_obj );
    
    # %document_preview contains:
    # ( PREVIEW_HTML => $html )
    
    # this relationship's preview argument gets called like so:
    my %final_params = $relationship_preview->(
      $app, \@pointer_and_document_obj, \%document_preview
    );

    #and %final_params gets sent along as the final HTML::Template parameters
    #to generate this object's preview html.

For details of which parameters are used and how, see the
module_objects prompt documentation in BigMed::App::Web::Prompt.

=back

=item * has_no => ['label1', 'label2', ... ]

Removes a relationship established in the parent class. So, while the parent
content class will continue to have the removed relationship, this subclass
will not have the relationship as part of its definition.

Accepts an array reference with the label names for the relationships to remove.

=item * editor_fields => \@fieldset_info

Defines the recommended order and content of the fields and objects to present
to the user for editing in a Big Medium application's content editor context
(for example, by BigMed::App::Web::Editor). The value is a reference to an
array of hash references, where each hash reference represents a "fieldset"
containing a group of related fields. See the C<editor_fields> method for
details on retrieving this info later.

Each fieldset hash reference may be composed of the following field value pairs
(each should, at a minimum, have the fields, custom_obj, or media value
defined):

=over 4

=item * title => $fieldset_title

The (unlocalized, unescaped) label to use for this fieldset.

=item * fields => \@fields_to_display

An array reference of the fields or objects to appear in this fieldset.
Each item is represented by a hash reference, which can take one of two
forms:

=over 4

=item * A hash reference suitable to be passed to BigMed::App's
prompt_field_ref method for prompting and to parse_submission for parsing:

    fields => [
        {   data_class => 'BigMed::Content::Page',
            column     => 'description',
        },
        {   data_class => 'BigMed::Content::Page',
            column     => 'meta_description',
        },
        {   data_class => 'BigMed::Content::Page',
            column     => 'meta_keywords',
        },
    ]

No need to supply a value parameter to these hash references -- the column
value is taken automatically from the object's column value
when displaying the edit fields. The data_class can be optionally omitted,
and the data_class of the current object will be inserted.

In addition to the traditional parameters for prompt_field_ref and
parse_submission, you can also supply two callbacks to add or customize
parameters dynamically. The callbacks receive the BigMed::App object
and the BigMed::Content object as arguments:

    fields => [
        {   column     => 'flags',
            prompt_callback => sub {
                my ($app, $content) = shift;
                require BigMed::Plugin;
                BigMed::Plugin->load_formats;
                my @options = BigMed::Format->page_flags;
                my %labels =
                  map { $_ => $app->language("PAGEFLAG_$_") } @options;
                return (options => \@options, labels => \%labels );
            },
            parse_callback =>  \%parse_coderef,
        },
    ]

=item * A hash reference where the key is 'relation' and the value is the name
of an object relationship for this content type:

    fields => [
       { relation => 'pullquote' },
       { relation => 'tag' }
    ]

No matter what relationships are included in fields, only valid relationships
will be observed. For example, if a content subtype has registered
a has_no relationship for an object that's included here in the fields tag,
the relation will not be included when the editor fields are retrieved via
the C<editor_fields> method.

=back

=item * custom_obj => 1

If true, the fieldset will be used to display a subtypes custom relationships
(relationship objects that are not included in the basic content subclass).
Any fields attribute is ignored if this value is present, its value replaced
by an array of 'relation' hash references for the custom objects. If
there are no custom objects, then the fieldset will be omitted by the
C<editor_fields> method.

=item * optional => 1

If true, then the values of the fieldset are optional. This might signal
the current application's content editor to treat the display of the
fieldset differently (BigMed::App::Web::Editor, for example, offers
access to such fields via a toggle link, so that the fields start off
hidden).

=back

If no value is specified, the inheritance tree is climbed to find a parent
class with editor_fields defined. If none is found, then a default set of
fields will be used from the standard BigMed::Content data columns.

=item * post_parse => \&post_parse_routine

An optional coderef to call after a content object's editor_fields fields
have been submitted and parsed but before they are applied to the content
object. The routine receives two arguments: the original BigMed::Content
object before the fields are applied, and a hash reference to the BigMed::App
parse_submission results.

After the callback returns, the parse_submission hash is checked for
errors and, if clear, is used to fill the content object.

=item * content_hook => \&content_hook

An optional coderef for BigMed::Format subclasses to call after generating
a content element's html content but before inserting it into a page.
The routine receives three arguments:
the content object, the context object, and the content html.

=back

=item * Foo->register_content_subtype(%arguments);

Registers a subtype of the Foo content class.  Subtypes share the same
data structure as their parent content classes, but can otherwise depart from
their parent classes' definitions.

The method accepts nearly all of the parameters as the C<register_content_class>
method, except for those which establish the data structure and source.

The method accepts a hash of arguments with the following key-value pairs:

=over 4

=item * label => $label_name

The display name of the subtype (e.g. Article, Event, Product, etc).

=item * default => 1

If true, makes this subtype be the default subtype for the class. Unless
otherwise specified, content objects should be assumed to be of this
subtype.

=item * has => { 'label1' => \%parameters, 'label2' => \%parameters, ... }

Creates a "has" relationship between the class's objects and one or more
other types of objects. See the description above for the C<has> parameter
of the C<register_content_class> method.

=item * points_to => { 'label1' => \%parameters, 'label2' => \%parameters, ... }

Creates a "points_to" relationship between the class's objects and one or more
other types of objects. See the description above for the C<points_to> parameter
of the C<register_content_class> method.

=item * editor_fields => \@fieldset_info

A custom fieldset definition to use for this subtype instead of the
editor_fields value established by the parent class definition. See the
description above for the editor_fields parameter of the
C<register_content_class> method.

=item * has_no => ['label1', 'label2', ... ]

Removes a relationship established in the parent class. So, while the parent
content class will continue to have the removed relationship, this subtype
will not have the relationship as part of its definition.

=item * suppress => ['column_name1', 'column_name2', ... ]

Similar to has_no, but for data columns instead of object relationships.
Prevents fields for the named data columns from being included when the
C<editor_fields> method is called for this subtype.

=back

=item * content_hook => \&content_hook

An optional coderef for BigMed::Format subclasses to call after generating
the subtype object's html content but before inserting it into a page.
The routine receives three arguments:
the content object, the context object, and the content html.

=back

=head2 Object Retrieval Methods

In addition to the standard C<BigMed::Data> retrieval methods (fetch,
select, next, previous), BigMed::Content also has the following:

=over 4

=item * Foo->fetch_or_create_content_obj(%args)

Returns a content object of the Foo class based on the following key/value
pairs in the argument hash:

=over 4

=item * site => $site_obj_or_id

Required. The site object or site id of the site for which to retrieve the
content object.

=item * id => $content_id

The id of the content object to retrieve.

If no ID is supplied or if no such object exists with that id, a new object
is returned with a brand new ID (as well as the appropriate site ID and
subtype settings).

=item * subtype => $subtype_name

When fetching a new object, this optional subtype name is used to assign
a subtype to the content object. (This value is currently ignored if
retrieving an existing object, but that may change).

=back

=back

=head2 Permissions

=over 4

=item * $obj->editable_by_user($user)

Returns true if the content object is editable by the user argument
object.

The rules are that administrators and webmaster can edit anything.
Users with publisher permissions in all of the sections to which
the content is assigned can edit the content. Users with editor
permissions in all of the sections to which the content is assigned
can edit the content if it is unpublished. Users with lesser
permissions can edit the content if they are the owner and the
content is unpublished.

=item * $obj->publishable_by_user($user)

Returns true if the content object is publishable by the user argument
object.

The rules are that administrators and webmaster can publish anything.
Publishers can publish the article only if they have publishing privileges
to all of the content object's sections. Writers, editors and guests
cannot publish.

=back

=head2 Accessor Methods

=over 4

=item * $content_obj->effective_section($site, $section_obj_or_id, \%kids)

Returns the ID of the content object's section which most closely belongs
to the section object or ID specified in the second argument. So, if
the object does not actually belong to the section specified in the
argument but does belong to one of that section's child sections, the
ID of that child section will be returned.

The third argument is an optional hash reference whose keys are the ids
of the child sections. If provided, this cache will be used to find
the first child section instead of looking up the descendant ids, for
a performance boost.

If no section is specified, the ID of the object's first section will be
returned.

If no matching sections are found (or if the object does not belong to
any sections), an empty string is returned.

If an error is encountered along the way, the method returns an undef
value and sets an error in the BigMed::Error queue.

=item * $content_obj->effective_active_section($site, $section_obj_or_id, \%kids)

Similar to C<effective_section> but requires that the section be active.
Returns an empty string if no active sections are found within the
specified section scope.

The third argument is an optional hash reference whose keys are the ids
of the *active* child sections. If provided, thsi cache will be used
to find the first active child section instead of looking up the
active subsections, for a performance boost.

=item * BigMed::Content->content_classes

Returns an array of all Big Medium content classes in alphabetical order.
This static method can be called on BigMed::Content or any of its subclasses.

=item * BigMed::Content->content_class_exists($class_name)

Returns true if the content class in the argument is a registered content
class.

=item * Foo->content_subtypes

Returns an array of all subtype label names registered to the Foo class
in alphabetical order.

=item * Foo->default_subtype

Returns the name of the default subtype for the class, if any. Returns
undef if there are no subtypes or if none have been specified as default.

=item * Foo->subtype_exists($subtype_name)

Returns true if the subtype named in the first argument is a registered
subtype.

=item * Foo->data_relationships($subtype)

Returns the label names of all data relationships for the specified subtype
of the Foo class, in alphabetical order. If no subtype is specified, the default data relationships for Foo are returned instead.

If called as an object method, the object's subtype is used if none is
specified in the argument.

=item * Foo->editor_fields($subtype)

$foo->editor_fields()

Returns an array of fieldsets containing the fields and related objects
that are editable for this content class. If a subtype is specified as the
argument, then the array will be customized, as appropriate, for the specified
subtype of the Foo class.

The returned format is similar to the formatted submitted via the editor_fields
parameter of the C<register_content_class> method, with the following exceptions:

=over 4

=item Any field columns indicated by the 'suppress' parameter in
C<register_content_class> or C<register_content_subtype> are omitted.

=item Any object relationships indicated by the 'has_no' parameter in
C<register_content_class> or C<register_content_subtype> are omitted.

=item Any 'custom_obj' fieldsets have a 'fields' parameter added, containing
an array of 'relation' hash references of all of the custom objects for
this subtype. (If there are none, the fieldset is omitted entirely).

=back

=item * Foo->content_hook($subtype)

Returns the code reference for the content_hook callback for the class.
If a subtype is specified in the argument, that subtype's callback is
returned. If the method is called as an object method, the object's
subtype is used if none is specified in the argument.

=back

=head2 Utility Methods

=over 4

=item * $obj->post_parse(\%parsed_fields)

Submits the object and parsed editor_fields to the class's post_parse
callback, if any. Returns undef if an error is encountered.

=item * $obj->generate_slug

Ensures a valid and unique slug for the object within its content class.
If the object does not have a slug, a slug is created based on the title
name. Only alphanumerics, hyphens and underscores are used when creating
the slug from the title, and other characters are removed. If no such
characters exist (in the case of non-western languages, for example),
then the ID name is used instead.

If another object already has the generated slug name (or if the slug
name is "index"), the slug is changed until it is unique.

The method sets the new slug value and returns the new slug. Returns
undef if there's an error along the way.

=back

=head2 Relationship Link Methods

=over 4

=item * $obj->save_related_info( %arguments )

Parses a user submission and creates/updates the appropriate target object
and (if applicable) pointer object, linking the target object to the
primary content object.

If successful, returns the linked object or, for points_to relationships,
returns an array reference where the pointer object is the first item
and the target object is the second item. Returns undef if there's an
error.

    my $related_obj = $obj->save_related_info(
        relation  => 'media',
        data_type => 'image',
        parser    => sub { $app->parse_submission(@_) },
        data_id   => $pointer->id,
        user      => $user_obj,
      )
      or $obj->error_stop;

The method accepts an argument hash with the following key/value pairs:

=over 4

=item * relation => $relationship_name

Required. The name of the relationship. Must be one of the registered
relationship names for the content class.

=item * data_type => $data_type

Required. The data label for the object to save. Must be one of the data types
registered for the relationship name

=item * parser => sub { $app->parse_submission(@_) }

Required. A code reference to pass parsing parameters to the application's
parse_submission routine.

=item * data_id => $id

If editing an existing related object, this is the id of the target object
itself (for "has" relationships) or the pointer object (for "points_to"
relationships). If no id is provided, a new related object will be created.

=item * user => $user

If provided, the method checks the user's permissions to make sure that
they have permission to edit both the primary object as well as the
target object. If no user object is provided, no restrictions are applied.

=back

=item * $obj->save_object_link( %arguments )

Establishes a relationship link between the content object and another
external link. If this is a C<points_to> relationship, a BigMed::Pointer
object will be created to link the two objects if one does not already
exist. If this is a C<has> relationship, the linked (target) object
will be updated to point back at the source object. Both the pointer
and target object are saved to disk.

If successful, returns the linked object or, for points_to relationships,
returns an array reference where the pointer object is the first item
and the target object is the second item. Returns undef if there's an
error.

    my $object = $obj->save_object_link(
        linked_obj => $object,
        type       => 'relationship_type',
        pointer_id => $pointer_obj_id,
        metadata   => \%metadata,
    ) or $obj->error_stop;

The argument hash accepts the following key/value pairs:

=over 4

=item * linked_obj => $target_object

<B>Required.</B> The object to which the link should be established.

=item * type => 'relationship name'

<B>Required.</B> The type of relationship for this link. This must be one of the registered relationship types identified by the C<data_relationships>
method for the source object's content class and subtype.

=item * pointer_id => $id

The id of the pointer id to use if a pointer object already exists for
the link (for points_to relationship only).

=item * metadata = \%metadata

A reference to a hash of metadata values appropriate to the specific
relationship type (for points_to relationships only).

=back

=item * C<< $obj->load_related_classes( $relation_name )

Loads/requires the classes for the relationship named in the first argument
and returns an array of those class names.

=item * C<< $obj->load_related_objects( $relation_name, $rcache )

Returns an array of all related objects for the relationship named in the
first argument. The result differs based on whether the relationship is
a 'has' or 'points_to' type:

=over 4

=item * For 'has' relationships, it's an array of the objects.

=item * For 'points_to' relationships, it's an array of array references,
where the first item in the array is the BigMed::Pointer object, and the
second item is the target object.

=back

The optional second argument is a hash reference containing cached
selections of the site's relationship objects. The keys are the class
names of the related objects (or 'BigMed::Pointer' for all points_to
relationships). The selections should be for *all* objects of that
site for the class.

The array of result objects is cached in the object to speed future
requests for the same relationship.

=item * C<< $obj->sorted_related_objects( $relation_name, $rcache ) >>

Same as load_related_objects but the objects are sorted based on the
relationship's "sort" callback.

=item * C<< $self->related_obj_class( $relation, $data_type, $subtype ) >>

Loads and returns the class for related objects associated with
the relation name and data type in the arguments. A content subtype
may be specified as a third argument if the relationship is unique
to a content class subtype. May be called as either a class or object method:

    $class = $obj->related_obj_class( 'media', 'image' );
    $class = BigMed::Content::Page->related_obj_class( 'author', 'person');

If called as an object method, the object's subtype will be used if no
subtype is specified in the argument.

An empty array could indicate an error, it's good
practice to check the error queue after calling this method. For example:

    my @objects = $content->load_related_objects('poodles');
    $content->error_stop if !@objects && $content->error;

=back

=head1 SEE ALSO

=over 4

=item * BigMed::Data

=back

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

