Projekt

Allgemein

Profil

Herunterladen (16,4 KB) Statistiken
| Zweig: | Markierung: | Revision:
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 { &sections_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