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' },
 
  48 __PACKAGE__->meta->initialize;
 
  50 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
 
  56   push @errors, t8('The title is missing.') if !$self->title;
 
  61 sub _before_save_initialize_not_null_columns {
 
  64   for (qw(previous_section_number previous_fb_number previous_picture_number)) {
 
  65     $self->$_(0) if !defined $self->$_;
 
  74   croak "Not a writer" if scalar(@_) > 1;
 
  76   return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
 
  79 sub text_blocks_sorted {
 
  80   my ($self, %params) = _hashify(1, @_);
 
  82   my @text_blocks = @{ $self->text_blocks };
 
  83   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
 
  84   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
 
  90   my ($self, @rest) = @_;
 
  92   croak "This sub is not a writer" if @rest;
 
  94   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
 
  97 sub sections { §ions_sorted; }
 
 100   my ($self, %params) = _hashify(1, @_);
 
 101   my $by              = $params{by} || 'itime';
 
 103   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
 
 106 sub displayable_name {
 
 109   return sprintf('%s: "%s"', $self->type->description, $self->title);
 
 112 sub versioned_copies_sorted {
 
 113   my ($self, %params) = _hashify(1, @_);
 
 115   my @copies = @{ $self->versioned_copies };
 
 116   @copies    = grep { $_->version->version_number <=  $params{max_version_number} } @copies if $params{max_version_number};
 
 117   @copies    = sort { $a->version->version_number <=> $b->version->version_number } @copies;
 
 123   my ($self, %params) = @_;
 
 125   return $self->_create_copy(%params) if $self->db->in_transaction;
 
 128   if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
 
 129     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
 
 137   my ($self, %params) = @_;
 
 139   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
 
 140   $copy->copy_from($self, %params);
 
 146   my ($self, $params, %attributes) = @_;
 
 148   my $source = $params->{source};
 
 150   croak "Missing parameter 'source'" unless $source;
 
 153   if (!$params->{paste_template}) {
 
 154     $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)),
 
 158   my %paste_template_result;
 
 160   # Clone text blocks and pictures.
 
 161   my $clone_picture = sub {
 
 163     my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($picture);
 
 164     $cloned->position(undef);
 
 168   my $clone_text_block = sub {
 
 169     my ($text_block) = @_;
 
 170     my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
 
 171     $cloned->position(undef);
 
 172     $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
 
 176   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
 
 178   if (!$params->{paste_template}) {
 
 179     $self->text_blocks($paste_template_result{text_blocks});
 
 181     $self->add_text_blocks($paste_template_result{text_blocks});
 
 184   # Save new object -- we need its ID for the items.
 
 193     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
 
 194     $cloned->requirement_spec_id($self->id);
 
 195     $cloned->position(undef);
 
 196     $cloned->fb_number(undef) if $params->{paste_template};
 
 197     $cloned->children(map { $clone_item->($_) } @{ $item->children });
 
 199     $id_to_clone{ $item->id } = $cloned;
 
 204   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
 
 206   if (!$params->{paste_template}) {
 
 207     $self->items($paste_template_result{sections});
 
 209     $self->add_items($paste_template_result{sections});
 
 212   # Save the items -- need to do that before setting dependencies.
 
 216   foreach my $item (@{ $source->items }) {
 
 217     next unless @{ $item->dependencies };
 
 218     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
 
 221   $self->update_attributes(%attributes) unless $params->{paste_template};
 
 223   return %paste_template_result;
 
 227   my ($self, $source, %attributes) = @_;
 
 229   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
 
 233   my ($self, $template) = @_;
 
 235   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
 
 238 sub highest_version {
 
 241   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
 
 244 sub is_working_copy {
 
 247   return !$self->working_copy_id;
 
 250 sub next_version_number {
 
 253   return 1 if !$self->id;
 
 255   my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
 
 256     SELECT MAX(v.version_number)
 
 257     FROM requirement_spec_versions v
 
 258     WHERE v.requirement_spec_id IN (
 
 260       FROM requirement_specs rs
 
 262          OR (rs.working_copy_id = ?)
 
 266   return ($max_number // 0) + 1;
 
 270   my ($self, %attributes) = @_;
 
 272   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 274   my ($copy, $version);
 
 275   my $ok = $self->db->with_transaction(sub {
 
 276     delete $attributes{version_number};
 
 278     SL::DB::Manager::RequirementSpecVersion->update_all(
 
 279       set   => [ working_copy_id     => undef     ],
 
 280       where => [ requirement_spec_id => $self->id ],
 
 283     $copy    = $self->create_copy(working_copy_id => $self->id);
 
 284     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
 
 289   return $ok ? ($copy, $version) : ();
 
 292 sub invalidate_version {
 
 293   my ($self, %params) = @_;
 
 295   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 297   return if !$self->id;
 
 299   SL::DB::Manager::RequirementSpecVersion->update_all(
 
 300     set   => [ working_copy_id => undef     ],
 
 301     where => [ working_copy_id => $self->id ],
 
 306   my ($self, $other) = @_;
 
 308   return $self->id <=> $other->id;
 
 320 SL::DB::RequirementSpec - RDBO model for requirement specs
 
 324 The database structure behind requirement specs is a bit involved. The
 
 325 important thing is how working copy/versions are handled.
 
 327 The table contains three important columns: C<id> (which is also the
 
 328 primary key) and C<working_copy_id>. C<working_copy_id> is a
 
 329 self-referencing column: it can be C<NULL>, but if it isn't then it
 
 330 contains another requirement spec C<id>.
 
 332 Versions are represented similarly. The C<requirement_spec_versions>
 
 333 table has three important columns: C<id> (the primary key),
 
 334 C<requirement_spec_id> (references C<requirement_specs.id> and must
 
 335 not be C<NULL>) and C<working_copy_id> (references
 
 336 C<requirement_specs.id> as well but can be
 
 337 C<NULL>). C<working_copy_id> points to the working copy if and only if
 
 338 the working copy is currently equal to a versioned copy.
 
 340 The design is as follows:
 
 344 =item * The user is always working on a working copy. The working copy
 
 345 is identified in the database by having C<working_copy_id> set to
 
 348 =item * All other entries in this table are referred to as I<versioned
 
 349 copies>. A versioned copy is a copy of a working frozen at the moment
 
 350 in time it was created. Each versioned copy refers back to the working
 
 351 copy it belongs to: each has its C<working_copy_id> set.
 
 353 =item * Each versioned copy must be referenced from an entry in the
 
 354 table C<requirement_spec_versions> via
 
 355 C<requirement_spec_id>.
 
 357 =item * Directly after creating a versioned copy even the working copy
 
 358 itself is referenced from a version via that table's
 
 359 C<working_copy_id> column. However, any modification that will be
 
 360 visible to the customer (text, positioning etc but not internal things
 
 361 like time/cost estimation changes) will cause the version to be
 
 362 disassociated from the working copy. This is achieved via before save
 
 367 =head1 DATABASE TRIGGERS AND CHECKS
 
 369 Several database triggers and consistency checks exist that manage
 
 370 requirement specs, their items and their dependencies. These are
 
 371 described here instead of in the individual files for the other RDBO
 
 376 When you delete a requirement spec all of its dependencies (items,
 
 377 text blocks, versions etc.) are deleted by triggers.
 
 379 When you delete an item (either a section or a (sub-)function block)
 
 380 all of its children will be deleted as well. This will trigger the
 
 381 same trigger resulting in a recursive deletion with the bottom-most
 
 382 items being deleted first. Their item dependencies are deleted as
 
 387 Whenever you update a requirement spec item a trigger will fire that
 
 388 will update the parent's C<time_estimation> column. This also happens
 
 389 when an item is deleted or updated.
 
 391 =head2 CONSISTENCY CHECKS
 
 393 Several consistency checks are applied to requirement spec items:
 
 397 =item * Column C<requirement_spec_item.item_type> can only contain one of
 
 398 the values C<section>, C<function-block> or C<sub-function-block>.
 
 400 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
 
 401 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
 
 410 =item C<copy_from $source, %attributes>
 
 412 Copies everything (basic attributes like type/title/customer, items,
 
 413 text blocks, time/cost estimation) save for the versions from the
 
 414 other requirement spec object C<$source> into C<$self> and saves
 
 415 it. This is done within a transaction.
 
 417 C<%attributes> are attributes that are assigned to C<$self> after all
 
 418 the basic attributes from C<$source> have been assigned.
 
 420 This function can be used for resetting a working copy to a specific
 
 423   my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
 
 424   my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
 
 426   $requirement_spec->copy_from($versioned_copy);
 
 427   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
 
 431 Creates and returns a copy of C<$self>. The copy is already
 
 432 saved. Creating the copy happens within a transaction.
 
 434 =item C<create_version %attributes>
 
 436 Prerequisites: C<$self> must be a working copy (see the overview),
 
 437 not a versioned copy.
 
 439 This function creates a new version for C<$self>. This involves
 
 444 =item 1. The next version number is calculated using
 
 445 L</next_version_number>.
 
 447 =item 2. A copy of C<$self> is created with L</create_copy>.
 
 449 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
 
 450 created. Its attributes are copied from C<%attributes> save for the
 
 451 version number which is taken from step 1.
 
 453 =item 4. The version instance created in step 3 is referenced to the
 
 454 the copy from step 2 via C<requirement_spec_id> and to the working
 
 455 copy for which the version was created via C<working_copy_id>.
 
 459 All this is done within a transaction.
 
 461 In case of success a two-element list is returned consisting of the
 
 462 copy & version objects created in steps 3 and 2 respectively. In case
 
 463 of a failure an empty list will be returned.
 
 465 =item C<displayable_name>
 
 467 Returns a human-readable name for this instance consisting of the type
 
 470 =item C<highest_version>
 
 472 Given a working copy C<$self> this function returns the versioned copy
 
 473 of C<$self> with the highest version number. If such a version exist
 
 474 its instance is returned. Otherwise C<undef> is returned.
 
 476 This can be used for calculating the difference between the working
 
 477 copy and the last version created for it.
 
 479 =item C<invalidate_version>
 
 481 Prerequisites: C<$self> must be a working copy (see the overview),
 
 482 not a versioned copy.
 
 484 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
 
 485 table containing C<$self-E<gt>id> to C<undef>.
 
 487 =item C<is_working_copy>
 
 489 Returns trueish if C<$self> is a working copy and not a versioned
 
 490 copy. The condition for this is that C<working_copy_id> is C<undef>.
 
 492 =item C<next_version_number>
 
 494 Calculates and returns the next version number for this requirement
 
 495 spec. Version numbers start at 1 and are incremented by one for each
 
 496 version created for it, no matter whether or not it has been reverted
 
 497 to a previous version since. It boils down to this pseudo-code:
 
 499   if (has_never_had_a_version)
 
 502     return max(version_number for all versions for this requirement spec) + 1
 
 506 An alias for L</sections_sorted>.
 
 508 =item C<sections_sorted>
 
 510 Returns an array reference of requirement spec items that do not have
 
 511 a parent -- meaning that are sections.
 
 513 This is not a writer. Use the C<items> relationship for that.
 
 515 =item C<text_blocks_sorted %params>
 
 517 Returns an array reference of text blocks sorted by their positional
 
 518 column in ascending order. If the C<output_position> parameter is
 
 519 given then only the text blocks belonging to that C<output_position>
 
 524 Validate values before saving. Returns list or human-readable error
 
 527 =item C<versioned_copies_sorted %params>
 
 529 Returns an array reference of versioned copies sorted by their version
 
 530 number in ascending order. If the C<max_version_number> parameter is
 
 531 given then only the versioned copies whose version number is less than
 
 532 or equal to C<max_version_number> are returned.
 
 542 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>