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::Locale::String;
 
  12 use SL::Util qw(_hashify);
 
  14 __PACKAGE__->meta->add_relationship(
 
  16     type           => 'one to many',
 
  17     class          => 'SL::DB::RequirementSpecItem',
 
  18     column_map     => { id => 'requirement_spec_id' },
 
  21     type           => 'one to many',
 
  22     class          => 'SL::DB::RequirementSpecTextBlock',
 
  23     column_map     => { id => 'requirement_spec_id' },
 
  26     type           => 'one to many',
 
  27     class          => 'SL::DB::RequirementSpec',
 
  28     column_map     => { id => 'working_copy_id' },
 
  31     type           => 'one to many',
 
  32     class          => 'SL::DB::RequirementSpecVersion',
 
  33     column_map     => { id => 'requirement_spec_id' },
 
  35   working_copy_versions => {
 
  36     type           => 'one to many',
 
  37     class          => 'SL::DB::RequirementSpecVersion',
 
  38     column_map     => { id => 'working_copy_id' },
 
  41     type           => 'one to many',
 
  42     class          => 'SL::DB::RequirementSpecOrder',
 
  43     column_map     => { id => 'requirement_spec_id' },
 
  47 __PACKAGE__->meta->initialize;
 
  49 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
 
  55   push @errors, t8('The title is missing.') if !$self->title;
 
  60 sub _before_save_initialize_not_null_columns {
 
  63   for (qw(previous_section_number previous_fb_number previous_picture_number)) {
 
  64     $self->$_(0) if !defined $self->$_;
 
  73   croak "Not a writer" if scalar(@_) > 1;
 
  75   return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
 
  78 sub text_blocks_sorted {
 
  79   my ($self, %params) = _hashify(1, @_);
 
  81   my @text_blocks = @{ $self->text_blocks };
 
  82   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
 
  83   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
 
  89   my ($self, @rest) = @_;
 
  91   croak "This sub is not a writer" if @rest;
 
  93   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
 
  96 sub sections { §ions_sorted; }
 
  99   my ($self, %params) = _hashify(1, @_);
 
 100   my $by              = $params{by} || 'itime';
 
 102   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
 
 105 sub displayable_name {
 
 108   return sprintf('%s: "%s"', $self->type->description, $self->title);
 
 111 sub versioned_copies_sorted {
 
 112   my ($self, %params) = _hashify(1, @_);
 
 114   my @copies = @{ $self->versioned_copies };
 
 115   @copies    = grep { $_->version->version_number <=  $params{max_version_number} } @copies if $params{max_version_number};
 
 116   @copies    = sort { $a->version->version_number <=> $b->version->version_number } @copies;
 
 122   my ($self, %params) = @_;
 
 124   return $self->_create_copy(%params) if $self->db->in_transaction;
 
 127   if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
 
 128     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
 
 136   my ($self, %params) = @_;
 
 138   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
 
 139   $copy->copy_from($self, %params);
 
 145   my ($self, $params, %attributes) = @_;
 
 147   my $source = $params->{source};
 
 149   croak "Missing parameter 'source'" unless $source;
 
 152   if (!$params->{paste_template}) {
 
 153     $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)),
 
 157   my %paste_template_result;
 
 159   # Clone text blocks and pictures.
 
 160   my $clone_picture = sub {
 
 162     my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($picture);
 
 163     $cloned->position(undef);
 
 167   my $clone_text_block = sub {
 
 168     my ($text_block) = @_;
 
 169     my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
 
 170     $cloned->position(undef);
 
 171     $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
 
 175   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
 
 177   if (!$params->{paste_template}) {
 
 178     $self->text_blocks($paste_template_result{text_blocks});
 
 180     $self->add_text_blocks($paste_template_result{text_blocks});
 
 183   # Save new object -- we need its ID for the items.
 
 192     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
 
 193     $cloned->requirement_spec_id($self->id);
 
 194     $cloned->position(undef);
 
 195     $cloned->fb_number(undef) if $params->{paste_template};
 
 196     $cloned->children(map { $clone_item->($_) } @{ $item->children });
 
 198     $id_to_clone{ $item->id } = $cloned;
 
 203   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
 
 205   if (!$params->{paste_template}) {
 
 206     $self->items($paste_template_result{sections});
 
 208     $self->add_items($paste_template_result{sections});
 
 211   # Save the items -- need to do that before setting dependencies.
 
 215   foreach my $item (@{ $source->items }) {
 
 216     next unless @{ $item->dependencies };
 
 217     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
 
 220   $self->update_attributes(%attributes) unless $params->{paste_template};
 
 222   return %paste_template_result;
 
 226   my ($self, $source, %attributes) = @_;
 
 228   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
 
 232   my ($self, $template) = @_;
 
 234   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
 
 237 sub highest_version {
 
 240   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
 
 243 sub is_working_copy {
 
 246   return !$self->working_copy_id;
 
 249 sub next_version_number {
 
 252   return 1 if !$self->id;
 
 254   my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
 
 255     SELECT MAX(v.version_number)
 
 256     FROM requirement_spec_versions v
 
 257     WHERE v.requirement_spec_id IN (
 
 259       FROM requirement_specs rs
 
 261          OR (rs.working_copy_id = ?)
 
 265   return ($max_number // 0) + 1;
 
 269   my ($self, %attributes) = @_;
 
 271   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 273   my ($copy, $version);
 
 274   my $ok = $self->db->with_transaction(sub {
 
 275     delete $attributes{version_number};
 
 277     SL::DB::Manager::RequirementSpecVersion->update_all(
 
 278       set   => [ working_copy_id     => undef     ],
 
 279       where => [ requirement_spec_id => $self->id ],
 
 282     $copy    = $self->create_copy(working_copy_id => $self->id);
 
 283     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
 
 288   return $ok ? ($copy, $version) : ();
 
 291 sub invalidate_version {
 
 292   my ($self, %params) = @_;
 
 294   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 296   return if !$self->id;
 
 298   SL::DB::Manager::RequirementSpecVersion->update_all(
 
 299     set   => [ working_copy_id => undef     ],
 
 300     where => [ working_copy_id => $self->id ],
 
 313 SL::DB::RequirementSpec - RDBO model for requirement specs
 
 317 The database structure behind requirement specs is a bit involved. The
 
 318 important thing is how working copy/versions are handled.
 
 320 The table contains three important columns: C<id> (which is also the
 
 321 primary key) and C<working_copy_id>. C<working_copy_id> is a
 
 322 self-referencing column: it can be C<NULL>, but if it isn't then it
 
 323 contains another requirement spec C<id>.
 
 325 Versions are represented similarly. The C<requirement_spec_versions>
 
 326 table has three important columns: C<id> (the primary key),
 
 327 C<requirement_spec_id> (references C<requirement_specs.id> and must
 
 328 not be C<NULL>) and C<working_copy_id> (references
 
 329 C<requirement_specs.id> as well but can be
 
 330 C<NULL>). C<working_copy_id> points to the working copy if and only if
 
 331 the working copy is currently equal to a versioned copy.
 
 333 The design is as follows:
 
 337 =item * The user is always working on a working copy. The working copy
 
 338 is identified in the database by having C<working_copy_id> set to
 
 341 =item * All other entries in this table are referred to as I<versioned
 
 342 copies>. A versioned copy is a copy of a working frozen at the moment
 
 343 in time it was created. Each versioned copy refers back to the working
 
 344 copy it belongs to: each has its C<working_copy_id> set.
 
 346 =item * Each versioned copy must be referenced from an entry in the
 
 347 table C<requirement_spec_versions> via
 
 348 C<requirement_spec_id>.
 
 350 =item * Directly after creating a versioned copy even the working copy
 
 351 itself is referenced from a version via that table's
 
 352 C<working_copy_id> column. However, any modification that will be
 
 353 visible to the customer (text, positioning etc but not internal things
 
 354 like time/cost estimation changes) will cause the version to be
 
 355 disassociated from the working copy. This is achieved via before save
 
 360 =head1 DATABASE TRIGGERS AND CHECKS
 
 362 Several database triggers and consistency checks exist that manage
 
 363 requirement specs, their items and their dependencies. These are
 
 364 described here instead of in the individual files for the other RDBO
 
 369 When you delete a requirement spec all of its dependencies (items,
 
 370 text blocks, versions etc.) are deleted by triggers.
 
 372 When you delete an item (either a section or a (sub-)function block)
 
 373 all of its children will be deleted as well. This will trigger the
 
 374 same trigger resulting in a recursive deletion with the bottom-most
 
 375 items being deleted first. Their item dependencies are deleted as
 
 380 Whenever you update a requirement spec item a trigger will fire that
 
 381 will update the parent's C<time_estimation> column. This also happens
 
 382 when an item is deleted or updated.
 
 384 =head2 CONSISTENCY CHECKS
 
 386 Several consistency checks are applied to requirement spec items:
 
 390 =item * Column C<requirement_spec_item.item_type> can only contain one of
 
 391 the values C<section>, C<function-block> or C<sub-function-block>.
 
 393 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
 
 394 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
 
 403 =item C<copy_from $source, %attributes>
 
 405 Copies everything (basic attributes like type/title/customer, items,
 
 406 text blocks, time/cost estimation) save for the versions from the
 
 407 other requirement spec object C<$source> into C<$self> and saves
 
 408 it. This is done within a transaction.
 
 410 C<%attributes> are attributes that are assigned to C<$self> after all
 
 411 the basic attributes from C<$source> have been assigned.
 
 413 This function can be used for resetting a working copy to a specific
 
 416   my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
 
 417   my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
 
 419   $requirement_spec->copy_from($versioned_copy);
 
 420   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
 
 424 Creates and returns a copy of C<$self>. The copy is already
 
 425 saved. Creating the copy happens within a transaction.
 
 427 =item C<create_version %attributes>
 
 429 Prerequisites: C<$self> must be a working copy (see the overview),
 
 430 not a versioned copy.
 
 432 This function creates a new version for C<$self>. This involves
 
 437 =item 1. The next version number is calculated using
 
 438 L</next_version_number>.
 
 440 =item 2. A copy of C<$self> is created with L</create_copy>.
 
 442 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
 
 443 created. Its attributes are copied from C<%attributes> save for the
 
 444 version number which is taken from step 1.
 
 446 =item 4. The version instance created in step 3 is referenced to the
 
 447 the copy from step 2 via C<requirement_spec_id> and to the working
 
 448 copy for which the version was created via C<working_copy_id>.
 
 452 All this is done within a transaction.
 
 454 In case of success a two-element list is returned consisting of the
 
 455 copy & version objects created in steps 3 and 2 respectively. In case
 
 456 of a failure an empty list will be returned.
 
 458 =item C<displayable_name>
 
 460 Returns a human-readable name for this instance consisting of the type
 
 463 =item C<highest_version>
 
 465 Given a working copy C<$self> this function returns the versioned copy
 
 466 of C<$self> with the highest version number. If such a version exist
 
 467 its instance is returned. Otherwise C<undef> is returned.
 
 469 This can be used for calculating the difference between the working
 
 470 copy and the last version created for it.
 
 472 =item C<invalidate_version>
 
 474 Prerequisites: C<$self> must be a working copy (see the overview),
 
 475 not a versioned copy.
 
 477 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
 
 478 table containing C<$self-E<gt>id> to C<undef>.
 
 480 =item C<is_working_copy>
 
 482 Returns trueish if C<$self> is a working copy and not a versioned
 
 483 copy. The condition for this is that C<working_copy_id> is C<undef>.
 
 485 =item C<next_version_number>
 
 487 Calculates and returns the next version number for this requirement
 
 488 spec. Version numbers start at 1 and are incremented by one for each
 
 489 version created for it, no matter whether or not it has been reverted
 
 490 to a previous version since. It boils down to this pseudo-code:
 
 492   if (has_never_had_a_version)
 
 495     return max(version_number for all versions for this requirement spec) + 1
 
 499 An alias for L</sections_sorted>.
 
 501 =item C<sections_sorted>
 
 503 Returns an array reference of requirement spec items that do not have
 
 504 a parent -- meaning that are sections.
 
 506 This is not a writer. Use the C<items> relationship for that.
 
 508 =item C<text_blocks_sorted %params>
 
 510 Returns an array reference of text blocks sorted by their positional
 
 511 column in ascending order. If the C<output_position> parameter is
 
 512 given then only the text blocks belonging to that C<output_position>
 
 517 Validate values before saving. Returns list or human-readable error
 
 520 =item C<versioned_copies_sorted %params>
 
 522 Returns an array reference of versioned copies sorted by their version
 
 523 number in ascending order. If the C<max_version_number> parameter is
 
 524 given then only the versioned copies whose version number is less than
 
 525 or equal to C<max_version_number> are returned.
 
 535 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>