1 package SL::DB::RequirementSpec;
 
   6 use List::Util qw(max reduce);
 
   7 use Rose::DB::Object::Helpers;
 
   9 use SL::DB::MetaSetup::RequirementSpec;
 
  10 use SL::DB::Manager::RequirementSpec;
 
  11 use SL::DB::Helper::LinkedRecords;
 
  12 use SL::Locale::String;
 
  13 use SL::Util qw(_hashify);
 
  15 __PACKAGE__->meta->add_relationship(
 
  17     type           => 'one to many',
 
  18     class          => 'SL::DB::RequirementSpecItem',
 
  19     column_map     => { id => 'requirement_spec_id' },
 
  22     type           => 'one to many',
 
  23     class          => 'SL::DB::RequirementSpecTextBlock',
 
  24     column_map     => { id => 'requirement_spec_id' },
 
  27     type           => 'one to many',
 
  28     class          => 'SL::DB::RequirementSpec',
 
  29     column_map     => { id => 'working_copy_id' },
 
  32     type           => 'one to many',
 
  33     class          => 'SL::DB::RequirementSpecVersion',
 
  34     column_map     => { id => 'requirement_spec_id' },
 
  36   working_copy_versions => {
 
  37     type           => 'one to many',
 
  38     class          => 'SL::DB::RequirementSpecVersion',
 
  39     column_map     => { id => 'working_copy_id' },
 
  42     type           => 'one to many',
 
  43     class          => 'SL::DB::RequirementSpecOrder',
 
  44     column_map     => { id => 'requirement_spec_id' },
 
  47     type           => 'one to many',
 
  48     class          => 'SL::DB::RequirementSpecPart',
 
  49     column_map     => { id => 'requirement_spec_id' },
 
  53 __PACKAGE__->meta->initialize;
 
  55 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
 
  61   push @errors, t8('The title is missing.') if !$self->title;
 
  66 sub _before_save_initialize_not_null_columns {
 
  69   for (qw(previous_section_number previous_fb_number previous_picture_number)) {
 
  70     $self->$_(0) if !defined $self->$_;
 
  79   croak "Not a writer" if scalar(@_) > 1;
 
  81   return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
 
  84 sub text_blocks_sorted {
 
  85   my ($self, %params) = _hashify(1, @_);
 
  87   my @text_blocks = @{ $self->text_blocks };
 
  88   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
 
  89   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
 
  95   my ($self, @rest) = @_;
 
  97   croak "This sub is not a writer" if @rest;
 
  99   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
 
 102 sub sections { §ions_sorted; }
 
 105   my ($self, %params) = _hashify(1, @_);
 
 106   my $by              = $params{by} || 'itime';
 
 108   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
 
 111 sub displayable_name {
 
 114   return sprintf('%s: "%s"', $self->type->description, $self->title);
 
 117 sub versioned_copies_sorted {
 
 118   my ($self, %params) = _hashify(1, @_);
 
 120   my @copies = @{ $self->versioned_copies };
 
 121   @copies    = grep { $_->version->version_number <=  $params{max_version_number} } @copies if $params{max_version_number};
 
 122   @copies    = sort { $a->version->version_number <=> $b->version->version_number } @copies;
 
 128   my ($self, @rest) = @_;
 
 130   croak "This sub is not a writer" if @rest;
 
 132   return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
 
 136   my ($self, %params) = @_;
 
 138   return $self->_create_copy(%params) if $self->db->in_transaction;
 
 141   if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
 
 142     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
 
 150   my ($self, %params) = @_;
 
 152   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
 
 153   $copy->copy_from($self, %params);
 
 159   my ($self, $params, %attributes) = @_;
 
 161   my $source = $params->{source};
 
 163   croak "Missing parameter 'source'" unless $source;
 
 166   if (!$params->{paste_template}) {
 
 167     $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)),
 
 171   my %paste_template_result;
 
 173   # Clone text blocks and pictures.
 
 174   my $clone_and_reset_position = sub {
 
 176     my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($src_obj);
 
 177     $cloned->position(undef);
 
 181   my $clone_text_block = sub {
 
 182     my ($text_block) = @_;
 
 183     my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
 
 184     $cloned->position(undef);
 
 185     $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
 
 189   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
 
 191   if (!$params->{paste_template}) {
 
 192     $self->text_blocks($paste_template_result{text_blocks});
 
 194     $self->add_text_blocks($paste_template_result{text_blocks});
 
 197   # Clone additional parts.
 
 198   $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
 
 199   my $accessor                  = $params->{paste_template} ? "add_parts" : "parts";
 
 200   $self->$accessor($paste_template_result{parts});
 
 202   # Save new object -- we need its ID for the items.
 
 211     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
 
 212     $cloned->requirement_spec_id($self->id);
 
 213     $cloned->position(undef);
 
 214     $cloned->fb_number(undef) if $params->{paste_template};
 
 215     $cloned->children(map { $clone_item->($_) } @{ $item->children });
 
 217     $id_to_clone{ $item->id } = $cloned;
 
 222   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
 
 224   if (!$params->{paste_template}) {
 
 225     $self->items($paste_template_result{sections});
 
 227     $self->add_items($paste_template_result{sections});
 
 230   # Save the items -- need to do that before setting dependencies.
 
 234   foreach my $item (@{ $source->items }) {
 
 235     next unless @{ $item->dependencies };
 
 236     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
 
 239   $self->update_attributes(%attributes) unless $params->{paste_template};
 
 241   return %paste_template_result;
 
 245   my ($self, $source, %attributes) = @_;
 
 247   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
 
 251   my ($self, $template) = @_;
 
 253   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
 
 256 sub highest_version {
 
 259   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
 
 262 sub is_working_copy {
 
 265   return !$self->working_copy_id;
 
 268 sub next_version_number {
 
 271   return 1 if !$self->id;
 
 273   my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
 
 274     SELECT MAX(v.version_number)
 
 275     FROM requirement_spec_versions v
 
 276     WHERE v.requirement_spec_id IN (
 
 278       FROM requirement_specs rs
 
 280          OR (rs.working_copy_id = ?)
 
 284   return ($max_number // 0) + 1;
 
 288   my ($self, %attributes) = @_;
 
 290   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 292   my ($copy, $version);
 
 293   my $ok = $self->db->with_transaction(sub {
 
 294     delete $attributes{version_number};
 
 296     SL::DB::Manager::RequirementSpecVersion->update_all(
 
 297       set   => [ working_copy_id     => undef     ],
 
 298       where => [ requirement_spec_id => $self->id ],
 
 301     $copy    = $self->create_copy(working_copy_id => $self->id);
 
 302     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
 
 307   return $ok ? ($copy, $version) : ();
 
 310 sub invalidate_version {
 
 311   my ($self, %params) = @_;
 
 313   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 315   return if !$self->id;
 
 317   SL::DB::Manager::RequirementSpecVersion->update_all(
 
 318     set   => [ working_copy_id => undef     ],
 
 319     where => [ working_copy_id => $self->id ],
 
 324   my ($self, $other) = @_;
 
 326   return $self->id <=> $other->id;
 
 338 SL::DB::RequirementSpec - RDBO model for requirement specs
 
 342 The database structure behind requirement specs is a bit involved. The
 
 343 important thing is how working copy/versions are handled.
 
 345 The table contains three important columns: C<id> (which is also the
 
 346 primary key) and C<working_copy_id>. C<working_copy_id> is a
 
 347 self-referencing column: it can be C<NULL>, but if it isn't then it
 
 348 contains another requirement spec C<id>.
 
 350 Versions are represented similarly. The C<requirement_spec_versions>
 
 351 table has three important columns: C<id> (the primary key),
 
 352 C<requirement_spec_id> (references C<requirement_specs.id> and must
 
 353 not be C<NULL>) and C<working_copy_id> (references
 
 354 C<requirement_specs.id> as well but can be
 
 355 C<NULL>). C<working_copy_id> points to the working copy if and only if
 
 356 the working copy is currently equal to a versioned copy.
 
 358 The design is as follows:
 
 362 =item * The user is always working on a working copy. The working copy
 
 363 is identified in the database by having C<working_copy_id> set to
 
 366 =item * All other entries in this table are referred to as I<versioned
 
 367 copies>. A versioned copy is a copy of a working frozen at the moment
 
 368 in time it was created. Each versioned copy refers back to the working
 
 369 copy it belongs to: each has its C<working_copy_id> set.
 
 371 =item * Each versioned copy must be referenced from an entry in the
 
 372 table C<requirement_spec_versions> via
 
 373 C<requirement_spec_id>.
 
 375 =item * Directly after creating a versioned copy even the working copy
 
 376 itself is referenced from a version via that table's
 
 377 C<working_copy_id> column. However, any modification that will be
 
 378 visible to the customer (text, positioning etc but not internal things
 
 379 like time/cost estimation changes) will cause the version to be
 
 380 disassociated from the working copy. This is achieved via before save
 
 385 =head1 DATABASE TRIGGERS AND CHECKS
 
 387 Several database triggers and consistency checks exist that manage
 
 388 requirement specs, their items and their dependencies. These are
 
 389 described here instead of in the individual files for the other RDBO
 
 394 When you delete a requirement spec all of its dependencies (items,
 
 395 text blocks, versions etc.) are deleted by triggers.
 
 397 When you delete an item (either a section or a (sub-)function block)
 
 398 all of its children will be deleted as well. This will trigger the
 
 399 same trigger resulting in a recursive deletion with the bottom-most
 
 400 items being deleted first. Their item dependencies are deleted as
 
 405 Whenever you update a requirement spec item a trigger will fire that
 
 406 will update the parent's C<time_estimation> column. This also happens
 
 407 when an item is deleted or updated.
 
 409 =head2 CONSISTENCY CHECKS
 
 411 Several consistency checks are applied to requirement spec items:
 
 415 =item * Column C<requirement_spec_item.item_type> can only contain one of
 
 416 the values C<section>, C<function-block> or C<sub-function-block>.
 
 418 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
 
 419 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
 
 428 =item C<copy_from $source, %attributes>
 
 430 Copies everything (basic attributes like type/title/customer, items,
 
 431 text blocks, time/cost estimation) save for the versions from the
 
 432 other requirement spec object C<$source> into C<$self> and saves
 
 433 it. This is done within a transaction.
 
 435 C<%attributes> are attributes that are assigned to C<$self> after all
 
 436 the basic attributes from C<$source> have been assigned.
 
 438 This function can be used for resetting a working copy to a specific
 
 441   my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
 
 442   my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
 
 444   $requirement_spec->copy_from($versioned_copy);
 
 445   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
 
 449 Creates and returns a copy of C<$self>. The copy is already
 
 450 saved. Creating the copy happens within a transaction.
 
 452 =item C<create_version %attributes>
 
 454 Prerequisites: C<$self> must be a working copy (see the overview),
 
 455 not a versioned copy.
 
 457 This function creates a new version for C<$self>. This involves
 
 462 =item 1. The next version number is calculated using
 
 463 L</next_version_number>.
 
 465 =item 2. A copy of C<$self> is created with L</create_copy>.
 
 467 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
 
 468 created. Its attributes are copied from C<%attributes> save for the
 
 469 version number which is taken from step 1.
 
 471 =item 4. The version instance created in step 3 is referenced to the
 
 472 the copy from step 2 via C<requirement_spec_id> and to the working
 
 473 copy for which the version was created via C<working_copy_id>.
 
 477 All this is done within a transaction.
 
 479 In case of success a two-element list is returned consisting of the
 
 480 copy & version objects created in steps 3 and 2 respectively. In case
 
 481 of a failure an empty list will be returned.
 
 483 =item C<displayable_name>
 
 485 Returns a human-readable name for this instance consisting of the type
 
 488 =item C<highest_version>
 
 490 Given a working copy C<$self> this function returns the versioned copy
 
 491 of C<$self> with the highest version number. If such a version exist
 
 492 its instance is returned. Otherwise C<undef> is returned.
 
 494 This can be used for calculating the difference between the working
 
 495 copy and the last version created for it.
 
 497 =item C<invalidate_version>
 
 499 Prerequisites: C<$self> must be a working copy (see the overview),
 
 500 not a versioned copy.
 
 502 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
 
 503 table containing C<$self-E<gt>id> to C<undef>.
 
 505 =item C<is_working_copy>
 
 507 Returns trueish if C<$self> is a working copy and not a versioned
 
 508 copy. The condition for this is that C<working_copy_id> is C<undef>.
 
 510 =item C<next_version_number>
 
 512 Calculates and returns the next version number for this requirement
 
 513 spec. Version numbers start at 1 and are incremented by one for each
 
 514 version created for it, no matter whether or not it has been reverted
 
 515 to a previous version since. It boils down to this pseudo-code:
 
 517   if (has_never_had_a_version)
 
 520     return max(version_number for all versions for this requirement spec) + 1
 
 524 An alias for L</sections_sorted>.
 
 526 =item C<sections_sorted>
 
 528 Returns an array reference of requirement spec items that do not have
 
 529 a parent -- meaning that are sections.
 
 531 This is not a writer. Use the C<items> relationship for that.
 
 533 =item C<text_blocks_sorted %params>
 
 535 Returns an array reference of text blocks sorted by their positional
 
 536 column in ascending order. If the C<output_position> parameter is
 
 537 given then only the text blocks belonging to that C<output_position>
 
 540 =item C<parts_sorted>
 
 542 Returns an array reference of additional parts sorted by their
 
 543 positional column in ascending order.
 
 547 Validate values before saving. Returns list or human-readable error
 
 550 =item C<versioned_copies_sorted %params>
 
 552 Returns an array reference of versioned copies sorted by their version
 
 553 number in ascending order. If the C<max_version_number> parameter is
 
 554 given then only the versioned copies whose version number is less than
 
 555 or equal to C<max_version_number> are returned.
 
 565 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>