kivitendo/SL/DB/RequirementSpec.pm @ 7b1da9c3
d17e1b9d | Moritz Bunkus | package SL::DB::RequirementSpec;
|
||
use strict;
|
||||
34948207 | Moritz Bunkus | use Carp;
|
||
013804fd | Moritz Bunkus | use List::Util qw(max reduce);
|
||
405a41ef | Moritz Bunkus | use Rose::DB::Object::Helpers;
|
||
34948207 | Moritz Bunkus | |||
d17e1b9d | Moritz Bunkus | use SL::DB::MetaSetup::RequirementSpec;
|
||
b2e1809f | Moritz Bunkus | use SL::DB::Manager::RequirementSpec;
|
||
075499b0 | Moritz Bunkus | use SL::DB::Helper::AttrDuration;
|
||
0a2fb69e | Moritz Bunkus | use SL::DB::Helper::CustomVariables (
|
||
module => 'RequirementSpecs',
|
||||
cvars_alias => 1,
|
||||
);
|
||||
85ab58eb | Moritz Bunkus | use SL::DB::Helper::LinkedRecords;
|
||
d17e1b9d | Moritz Bunkus | use SL::Locale::String;
|
||
84fc52bd | Moritz Bunkus | use SL::Util qw(_hashify);
|
||
d17e1b9d | Moritz Bunkus | |||
__PACKAGE__->meta->add_relationship(
|
||||
51fec310 | Moritz Bunkus | items => {
|
||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpecItem',
|
||||
column_map => { id => 'requirement_spec_id' },
|
||||
d17e1b9d | Moritz Bunkus | },
|
||
51fec310 | Moritz Bunkus | text_blocks => {
|
||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpecTextBlock',
|
||||
column_map => { id => 'requirement_spec_id' },
|
||||
},
|
||||
versioned_copies => {
|
||||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpec',
|
||||
column_map => { id => 'working_copy_id' },
|
||||
d17e1b9d | Moritz Bunkus | },
|
||
13fbd336 | Moritz Bunkus | versions => {
|
||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpecVersion',
|
||||
column_map => { id => 'requirement_spec_id' },
|
||||
},
|
||||
working_copy_versions => {
|
||||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpecVersion',
|
||||
column_map => { id => 'working_copy_id' },
|
||||
},
|
||||
fb692c5f | Moritz Bunkus | orders => {
|
||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpecOrder',
|
||||
column_map => { id => 'requirement_spec_id' },
|
||||
},
|
||||
0c319351 | Moritz Bunkus | parts => {
|
||
type => 'one to many',
|
||||
class => 'SL::DB::RequirementSpecPart',
|
||||
column_map => { id => 'requirement_spec_id' },
|
||||
},
|
||||
d17e1b9d | Moritz Bunkus | );
|
||
__PACKAGE__->meta->initialize;
|
||||
075499b0 | Moritz Bunkus | __PACKAGE__->attr_duration(qw(time_estimation));
|
||
b2e1809f | Moritz Bunkus | __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
|
||
d17e1b9d | Moritz Bunkus | sub validate {
|
||
my ($self) = @_;
|
||||
my @errors;
|
||||
push @errors, t8('The title is missing.') if !$self->title;
|
||||
return @errors;
|
||||
}
|
||||
b2e1809f | Moritz Bunkus | sub _before_save_initialize_not_null_columns {
|
||
my ($self) = @_;
|
||||
9cddaf37 | Moritz Bunkus | for (qw(previous_section_number previous_fb_number previous_picture_number)) {
|
||
$self->$_(0) if !defined $self->$_;
|
||||
}
|
||||
b2e1809f | Moritz Bunkus | |||
return 1;
|
||||
}
|
||||
13fbd336 | Moritz Bunkus | sub version {
|
||
my ($self) = @_;
|
||||
croak "Not a writer" if scalar(@_) > 1;
|
||||
return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
|
||||
}
|
||||
84fc52bd | Moritz Bunkus | sub text_blocks_sorted {
|
||
my ($self, %params) = _hashify(1, @_);
|
||||
34948207 | Moritz Bunkus | |||
84fc52bd | Moritz Bunkus | my @text_blocks = @{ $self->text_blocks };
|
||
@text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
|
||||
@text_blocks = sort { $a->position <=> $b->position } @text_blocks;
|
||||
921db961 | Moritz Bunkus | return \@text_blocks;
|
||
34948207 | Moritz Bunkus | }
|
||
84fc52bd | Moritz Bunkus | sub sections_sorted {
|
||
34948207 | Moritz Bunkus | my ($self, @rest) = @_;
|
||
croak "This sub is not a writer" if @rest;
|
||||
921db961 | Moritz Bunkus | return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
|
||
34948207 | Moritz Bunkus | }
|
||
84fc52bd | Moritz Bunkus | sub sections { §ions_sorted; }
|
||
fb692c5f | Moritz Bunkus | sub orders_sorted {
|
||
my ($self, %params) = _hashify(1, @_);
|
||||
my $by = $params{by} || 'itime';
|
||||
return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
|
||||
}
|
||||
c1569bc1 | Moritz Bunkus | sub displayable_name {
|
||
my ($self) = @_;
|
||||
return sprintf('%s: "%s"', $self->type->description, $self->title);
|
||||
}
|
||||
84fc52bd | Moritz Bunkus | sub versioned_copies_sorted {
|
||
my ($self, %params) = _hashify(1, @_);
|
||||
my @copies = @{ $self->versioned_copies };
|
||||
@copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
|
||||
@copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
|
||||
921db961 | Moritz Bunkus | return \@copies;
|
||
84fc52bd | Moritz Bunkus | }
|
||
0c319351 | Moritz Bunkus | sub parts_sorted {
|
||
my ($self, @rest) = @_;
|
||||
croak "This sub is not a writer" if @rest;
|
||||
return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
|
||||
}
|
||||
405a41ef | Moritz Bunkus | sub create_copy {
|
||
my ($self, %params) = @_;
|
||||
return $self->_create_copy(%params) if $self->db->in_transaction;
|
||||
my $copy;
|
||||
96670fe8 | Moritz Bunkus | if (!$self->db->with_transaction(sub { $copy = $self->_create_copy(%params) })) {
|
||
405a41ef | Moritz Bunkus | $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
|
||
return undef;
|
||||
}
|
||||
return $copy;
|
||||
}
|
||||
sub _create_copy {
|
||||
my ($self, %params) = @_;
|
||||
da6a187a | Moritz Bunkus | my $copy = $self->clone_and_reset;
|
||
1af50c02 | Moritz Bunkus | $copy->copy_from($self, %params);
|
||
return $copy;
|
||||
}
|
||||
013804fd | Moritz Bunkus | sub _copy_from {
|
||
55e399ab | Moritz Bunkus | my ($self, $params, %attributes) = @_;
|
||
my $source = $params->{source};
|
||||
1af50c02 | Moritz Bunkus | |||
croak "Missing parameter 'source'" unless $source;
|
||||
# Copy attributes.
|
||||
55e399ab | Moritz Bunkus | if (!$params->{paste_template}) {
|
||
9cddaf37 | Moritz Bunkus | $self->assign_attributes(map({ ($_ => $source->$_) } qw(type_id status_id customer_id project_id title hourly_rate time_estimation previous_section_number previous_fb_number previous_picture_number is_template)),
|
||
55e399ab | Moritz Bunkus | %attributes);
|
||
}
|
||||
8b9174e8 | Moritz Bunkus | # Copy custom variables.
|
||
foreach my $var (@{ $source->cvars_by_config }) {
|
||||
$self->cvar_by_name($var->config->name)->value($var->value);
|
||||
}
|
||||
55e399ab | Moritz Bunkus | my %paste_template_result;
|
||
405a41ef | Moritz Bunkus | |||
9cddaf37 | Moritz Bunkus | # Clone text blocks and pictures.
|
||
31ead75c | Moritz Bunkus | my $clone_and_reset_position = sub {
|
||
my ($src_obj) = @_;
|
||||
da6a187a | Moritz Bunkus | my $cloned = $src_obj->clone_and_reset;
|
||
9cddaf37 | Moritz Bunkus | $cloned->position(undef);
|
||
return $cloned;
|
||||
};
|
||||
55e399ab | Moritz Bunkus | my $clone_text_block = sub {
|
||
my ($text_block) = @_;
|
||||
da6a187a | Moritz Bunkus | my $cloned = $text_block->clone_and_reset;
|
||
55e399ab | Moritz Bunkus | $cloned->position(undef);
|
||
31ead75c | Moritz Bunkus | $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
|
||
55e399ab | Moritz Bunkus | return $cloned;
|
||
};
|
||||
$paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
|
||||
if (!$params->{paste_template}) {
|
||||
$self->text_blocks($paste_template_result{text_blocks});
|
||||
} else {
|
||||
$self->add_text_blocks($paste_template_result{text_blocks});
|
||||
}
|
||||
405a41ef | Moritz Bunkus | |||
31ead75c | Moritz Bunkus | # Clone additional parts.
|
||
$paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
|
||||
my $accessor = $params->{paste_template} ? "add_parts" : "parts";
|
||||
$self->$accessor($paste_template_result{parts});
|
||||
405a41ef | Moritz Bunkus | # Save new object -- we need its ID for the items.
|
||
8b9174e8 | Moritz Bunkus | $self->save(cascade => 1);
|
||
405a41ef | Moritz Bunkus | |||
my %id_to_clone;
|
||||
# Clone items.
|
||||
my $clone_item;
|
||||
$clone_item = sub {
|
||||
my ($item) = @_;
|
||||
da6a187a | Moritz Bunkus | my $cloned = $item->clone_and_reset;
|
||
1af50c02 | Moritz Bunkus | $cloned->requirement_spec_id($self->id);
|
||
55e399ab | Moritz Bunkus | $cloned->position(undef);
|
||
8042b063 | Moritz Bunkus | $cloned->fb_number(undef) if $params->{paste_template};
|
||
405a41ef | Moritz Bunkus | $cloned->children(map { $clone_item->($_) } @{ $item->children });
|
||
$id_to_clone{ $item->id } = $cloned;
|
||||
return $cloned;
|
||||
};
|
||||
55e399ab | Moritz Bunkus | $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
|
||
if (!$params->{paste_template}) {
|
||||
$self->items($paste_template_result{sections});
|
||||
} else {
|
||||
$self->add_items($paste_template_result{sections});
|
||||
}
|
||||
405a41ef | Moritz Bunkus | |||
# Save the items -- need to do that before setting dependencies.
|
||||
1af50c02 | Moritz Bunkus | $self->save;
|
||
405a41ef | Moritz Bunkus | |||
# Set dependencies.
|
||||
1af50c02 | Moritz Bunkus | foreach my $item (@{ $source->items }) {
|
||
405a41ef | Moritz Bunkus | next unless @{ $item->dependencies };
|
||
$id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
|
||||
}
|
||||
55e399ab | Moritz Bunkus | $self->update_attributes(%attributes) unless $params->{paste_template};
|
||
c96c4bb2 | Moritz Bunkus | |||
55e399ab | Moritz Bunkus | return %paste_template_result;
|
||
405a41ef | Moritz Bunkus | }
|
||
013804fd | Moritz Bunkus | sub copy_from {
|
||
my ($self, $source, %attributes) = @_;
|
||||
51fec310 | Moritz Bunkus | |||
55e399ab | Moritz Bunkus | $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
|
||
}
|
||||
sub paste_template {
|
||||
my ($self, $template) = @_;
|
||||
$self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
|
||||
013804fd | Moritz Bunkus | }
|
||
51fec310 | Moritz Bunkus | |||
013804fd | Moritz Bunkus | sub highest_version {
|
||
my ($self) = @_;
|
||||
return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
|
||||
51fec310 | Moritz Bunkus | }
|
||
sub is_working_copy {
|
||||
my ($self) = @_;
|
||||
return !$self->working_copy_id;
|
||||
}
|
||||
sub next_version_number {
|
||||
my ($self) = @_;
|
||||
013804fd | Moritz Bunkus | |||
13fbd336 | Moritz Bunkus | return 1 if !$self->id;
|
||
my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
|
||||
SELECT MAX(v.version_number)
|
||||
FROM requirement_spec_versions v
|
||||
WHERE v.requirement_spec_id IN (
|
||||
SELECT rs.id
|
||||
FROM requirement_specs rs
|
||||
WHERE (rs.id = ?)
|
||||
OR (rs.working_copy_id = ?)
|
||||
)
|
||||
SQL
|
||||
return ($max_number // 0) + 1;
|
||||
51fec310 | Moritz Bunkus | }
|
||
sub create_version {
|
||||
my ($self, %attributes) = @_;
|
||||
84fc52bd | Moritz Bunkus | croak "Cannot work on a versioned copy" if $self->working_copy_id;
|
||
51fec310 | Moritz Bunkus | my ($copy, $version);
|
||
013804fd | Moritz Bunkus | my $ok = $self->db->with_transaction(sub {
|
||
51fec310 | Moritz Bunkus | delete $attributes{version_number};
|
||
13fbd336 | Moritz Bunkus | SL::DB::Manager::RequirementSpecVersion->update_all(
|
||
set => [ working_copy_id => undef ],
|
||||
where => [ requirement_spec_id => $self->id ],
|
||||
);
|
||||
$copy = $self->create_copy(working_copy_id => $self->id);
|
||||
$version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
|
||||
51fec310 | Moritz Bunkus | |||
1;
|
||||
});
|
||||
return $ok ? ($copy, $version) : ();
|
||||
}
|
||||
d90408d8 | Moritz Bunkus | sub invalidate_version {
|
||
my ($self, %params) = @_;
|
||||
84fc52bd | Moritz Bunkus | croak "Cannot work on a versioned copy" if $self->working_copy_id;
|
||
d90408d8 | Moritz Bunkus | |||
13fbd336 | Moritz Bunkus | return if !$self->id;
|
||
SL::DB::Manager::RequirementSpecVersion->update_all(
|
||||
set => [ working_copy_id => undef ],
|
||||
where => [ working_copy_id => $self->id ],
|
||||
);
|
||||
d90408d8 | Moritz Bunkus | }
|
||
89ade8da | Moritz Bunkus | sub compare_to {
|
||
my ($self, $other) = @_;
|
||||
return $self->id <=> $other->id;
|
||||
}
|
||||
d17e1b9d | Moritz Bunkus | 1;
|
||
013804fd | Moritz Bunkus | __END__
|
||
=pod
|
||||
=encoding utf8
|
||||
=head1 NAME
|
||||
SL::DB::RequirementSpec - RDBO model for requirement specs
|
||||
=head1 OVERVIEW
|
||||
The database structure behind requirement specs is a bit involved. The
|
||||
important thing is how working copy/versions are handled.
|
||||
The table contains three important columns: C<id> (which is also the
|
||||
13fbd336 | Moritz Bunkus | primary key) and C<working_copy_id>. C<working_copy_id> is a
|
||
self-referencing column: it can be C<NULL>, but if it isn't then it
|
||||
contains another requirement spec C<id>.
|
||||
Versions are represented similarly. The C<requirement_spec_versions>
|
||||
table has three important columns: C<id> (the primary key),
|
||||
C<requirement_spec_id> (references C<requirement_specs.id> and must
|
||||
not be C<NULL>) and C<working_copy_id> (references
|
||||
C<requirement_specs.id> as well but can be
|
||||
C<NULL>). C<working_copy_id> points to the working copy if and only if
|
||||
the working copy is currently equal to a versioned copy.
|
||||
013804fd | Moritz Bunkus | |||
The design is as follows:
|
||||
=over 2
|
||||
=item * The user is always working on a working copy. The working copy
|
||||
is identified in the database by having C<working_copy_id> set to
|
||||
C<NULL>.
|
||||
=item * All other entries in this table are referred to as I<versioned
|
||||
copies>. A versioned copy is a copy of a working frozen at the moment
|
||||
in time it was created. Each versioned copy refers back to the working
|
||||
copy it belongs to: each has its C<working_copy_id> set.
|
||||
13fbd336 | Moritz Bunkus | =item * Each versioned copy must be referenced from an entry in the
|
||
table C<requirement_spec_versions> via
|
||||
C<requirement_spec_id>.
|
||||
013804fd | Moritz Bunkus | |||
=item * Directly after creating a versioned copy even the working copy
|
||||
13fbd336 | Moritz Bunkus | itself is referenced from a version via that table's
|
||
C<working_copy_id> column. However, any modification that will be
|
||||
visible to the customer (text, positioning etc but not internal things
|
||||
like time/cost estimation changes) will cause the version to be
|
||||
disassociated from the working copy. This is achieved via before save
|
||||
hooks in Perl.
|
||||
013804fd | Moritz Bunkus | |||
=back
|
||||
=head1 DATABASE TRIGGERS AND CHECKS
|
||||
Several database triggers and consistency checks exist that manage
|
||||
requirement specs, their items and their dependencies. These are
|
||||
described here instead of in the individual files for the other RDBO
|
||||
models.
|
||||
=head2 DELETION
|
||||
When you delete a requirement spec all of its dependencies (items,
|
||||
text blocks, versions etc.) are deleted by triggers.
|
||||
When you delete an item (either a section or a (sub-)function block)
|
||||
all of its children will be deleted as well. This will trigger the
|
||||
same trigger resulting in a recursive deletion with the bottom-most
|
||||
items being deleted first. Their item dependencies are deleted as
|
||||
well.
|
||||
=head2 UPDATING
|
||||
Whenever you update a requirement spec item a trigger will fire that
|
||||
will update the parent's C<time_estimation> column. This also happens
|
||||
when an item is deleted or updated.
|
||||
=head2 CONSISTENCY CHECKS
|
||||
Several consistency checks are applied to requirement spec items:
|
||||
=over 2
|
||||
=item * Column C<requirement_spec_item.item_type> can only contain one of
|
||||
the values C<section>, C<function-block> or C<sub-function-block>.
|
||||
=item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
|
||||
C<requirement_spec_item.item_type> is set to C<section> and C<NOT
|
||||
NULL> otherwise.
|
||||
=back
|
||||
=head1 FUNCTIONS
|
||||
=over 4
|
||||
=item C<copy_from $source, %attributes>
|
||||
Copies everything (basic attributes like type/title/customer, items,
|
||||
text blocks, time/cost estimation) save for the versions from the
|
||||
other requirement spec object C<$source> into C<$self> and saves
|
||||
it. This is done within a transaction.
|
||||
C<%attributes> are attributes that are assigned to C<$self> after all
|
||||
the basic attributes from C<$source> have been assigned.
|
||||
This function can be used for resetting a working copy to a specific
|
||||
version. Example:
|
||||
13fbd336 | Moritz Bunkus | my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
|
||
my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
|
||||
013804fd | Moritz Bunkus | |||
13fbd336 | Moritz Bunkus | $requirement_spec->copy_from($versioned_copy);
|
||
$versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
|
||||
013804fd | Moritz Bunkus | |||
=item C<create_copy>
|
||||
Creates and returns a copy of C<$self>. The copy is already
|
||||
saved. Creating the copy happens within a transaction.
|
||||
=item C<create_version %attributes>
|
||||
3dbac78c | Moritz Bunkus | Prerequisites: C<$self> must be a working copy (see the overview),
|
||
013804fd | Moritz Bunkus | not a versioned copy.
|
||
This function creates a new version for C<$self>. This involves
|
||||
several steps:
|
||||
=over 2
|
||||
=item 1. The next version number is calculated using
|
||||
L</next_version_number>.
|
||||
13fbd336 | Moritz Bunkus | =item 2. A copy of C<$self> is created with L</create_copy>.
|
||
=item 3. An instance of L<SL::DB::RequirementSpecVersion> is
|
||||
013804fd | Moritz Bunkus | created. Its attributes are copied from C<%attributes> save for the
|
||
version number which is taken from step 1.
|
||||
13fbd336 | Moritz Bunkus | =item 4. The version instance created in step 3 is referenced to the
|
||
the copy from step 2 via C<requirement_spec_id> and to the working
|
||||
copy for which the version was created via C<working_copy_id>.
|
||||
013804fd | Moritz Bunkus | |||
=back
|
||||
All this is done within a transaction.
|
||||
In case of success a two-element list is returned consisting of the
|
||||
copy & version objects created in steps 3 and 2 respectively. In case
|
||||
of a failure an empty list will be returned.
|
||||
=item C<displayable_name>
|
||||
Returns a human-readable name for this instance consisting of the type
|
||||
and the title.
|
||||
=item C<highest_version>
|
||||
Given a working copy C<$self> this function returns the versioned copy
|
||||
of C<$self> with the highest version number. If such a version exist
|
||||
its instance is returned. Otherwise C<undef> is returned.
|
||||
This can be used for calculating the difference between the working
|
||||
copy and the last version created for it.
|
||||
=item C<invalidate_version>
|
||||
3dbac78c | Moritz Bunkus | Prerequisites: C<$self> must be a working copy (see the overview),
|
||
013804fd | Moritz Bunkus | not a versioned copy.
|
||
13fbd336 | Moritz Bunkus | Sets any C<working_copy_id> field in the C<requirement_spec_versions>
|
||
table containing C<$self-E<gt>id> to C<undef>.
|
||||
013804fd | Moritz Bunkus | |||
=item C<is_working_copy>
|
||||
Returns trueish if C<$self> is a working copy and not a versioned
|
||||
copy. The condition for this is that C<working_copy_id> is C<undef>.
|
||||
=item C<next_version_number>
|
||||
Calculates and returns the next version number for this requirement
|
||||
spec. Version numbers start at 1 and are incremented by one for each
|
||||
version created for it, no matter whether or not it has been reverted
|
||||
to a previous version since. It boils down to this pseudo-code:
|
||||
if (has_never_had_a_version)
|
||||
return 1
|
||||
else
|
||||
return max(version_number for all versions for this requirement spec) + 1
|
||||
=item C<sections>
|
||||
An alias for L</sections_sorted>.
|
||||
=item C<sections_sorted>
|
||||
921db961 | Moritz Bunkus | Returns an array reference of requirement spec items that do not have
|
||
a parent -- meaning that are sections.
|
||||
013804fd | Moritz Bunkus | |||
This is not a writer. Use the C<items> relationship for that.
|
||||
=item C<text_blocks_sorted %params>
|
||||
921db961 | Moritz Bunkus | Returns an array reference of text blocks sorted by their positional
|
||
column in ascending order. If the C<output_position> parameter is
|
||||
given then only the text blocks belonging to that C<output_position>
|
||||
are returned.
|
||||
013804fd | Moritz Bunkus | |||
0c319351 | Moritz Bunkus | =item C<parts_sorted>
|
||
Returns an array reference of additional parts sorted by their
|
||||
positional column in ascending order.
|
||||
013804fd | Moritz Bunkus | =item C<validate>
|
||
Validate values before saving. Returns list or human-readable error
|
||||
messages (if any).
|
||||
=item C<versioned_copies_sorted %params>
|
||||
921db961 | Moritz Bunkus | Returns an array reference of versioned copies sorted by their version
|
||
number in ascending order. If the C<max_version_number> parameter is
|
||||
given then only the versioned copies whose version number is less than
|
||||
or equal to C<max_version_number> are returned.
|
||||
013804fd | Moritz Bunkus | |||
=back
|
||||
=head1 BUGS
|
||||
Nothing here yet.
|
||||
=head1 AUTHOR
|
||||
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
|
||||
=cut
|