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::AttrDuration;
 
  12 use SL::DB::Helper::CustomVariables (
 
  13   module      => 'RequirementSpecs',
 
  16 use SL::DB::Helper::LinkedRecords;
 
  17 use SL::Locale::String;
 
  18 use SL::Util qw(_hashify);
 
  20 __PACKAGE__->meta->add_relationship(
 
  22     type           => 'one to many',
 
  23     class          => 'SL::DB::RequirementSpecItem',
 
  24     column_map     => { id => 'requirement_spec_id' },
 
  27     type           => 'one to many',
 
  28     class          => 'SL::DB::RequirementSpecTextBlock',
 
  29     column_map     => { id => 'requirement_spec_id' },
 
  32     type           => 'one to many',
 
  33     class          => 'SL::DB::RequirementSpec',
 
  34     column_map     => { id => 'working_copy_id' },
 
  37     type           => 'one to many',
 
  38     class          => 'SL::DB::RequirementSpecVersion',
 
  39     column_map     => { id => 'requirement_spec_id' },
 
  41   working_copy_versions => {
 
  42     type           => 'one to many',
 
  43     class          => 'SL::DB::RequirementSpecVersion',
 
  44     column_map     => { id => 'working_copy_id' },
 
  47     type           => 'one to many',
 
  48     class          => 'SL::DB::RequirementSpecOrder',
 
  49     column_map     => { id => 'requirement_spec_id' },
 
  52     type           => 'one to many',
 
  53     class          => 'SL::DB::RequirementSpecPart',
 
  54     column_map     => { id => 'requirement_spec_id' },
 
  58 __PACKAGE__->meta->initialize;
 
  60 __PACKAGE__->attr_duration(qw(time_estimation));
 
  62 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
 
  68   push @errors, t8('The title is missing.') if !$self->title;
 
  73 sub _before_save_initialize_not_null_columns {
 
  76   for (qw(previous_section_number previous_fb_number previous_picture_number)) {
 
  77     $self->$_(0) if !defined $self->$_;
 
  86   croak "Not a writer" if scalar(@_) > 1;
 
  88   return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
 
  91 sub text_blocks_sorted {
 
  92   my ($self, %params) = _hashify(1, @_);
 
  94   my @text_blocks = @{ $self->text_blocks };
 
  95   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
 
  96   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
 
 101 sub sections_sorted {
 
 102   my ($self, @rest) = @_;
 
 104   croak "This sub is not a writer" if @rest;
 
 106   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
 
 109 sub sections { §ions_sorted; }
 
 112   my ($self, %params) = _hashify(1, @_);
 
 113   my $by              = $params{by} || 'itime';
 
 115   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
 
 118 sub displayable_name {
 
 121   return sprintf('%s: "%s"', $self->type->description, $self->title);
 
 124 sub versioned_copies_sorted {
 
 125   my ($self, %params) = _hashify(1, @_);
 
 127   my @copies = @{ $self->versioned_copies };
 
 128   @copies    = grep { $_->version->version_number <=  $params{max_version_number} } @copies if $params{max_version_number};
 
 129   @copies    = sort { $a->version->version_number <=> $b->version->version_number } @copies;
 
 135   my ($self, @rest) = @_;
 
 137   croak "This sub is not a writer" if @rest;
 
 139   return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
 
 143   my ($self, %params) = @_;
 
 145   return $self->_create_copy(%params) if $self->db->in_transaction;
 
 148   if (!$self->db->with_transaction(sub { $copy = $self->_create_copy(%params) })) {
 
 149     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
 
 157   my ($self, %params) = @_;
 
 159   my $copy = $self->clone_and_reset;
 
 160   $copy->copy_from($self, %params);
 
 166   my ($self, $params, %attributes) = @_;
 
 168   my $source = $params->{source};
 
 170   croak "Missing parameter 'source'" unless $source;
 
 173   if (!$params->{paste_template}) {
 
 174     $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)),
 
 178   # Copy custom variables.
 
 179   foreach my $var (@{ $source->cvars_by_config }) {
 
 180     $self->cvar_by_name($var->config->name)->value($var->value);
 
 183   my %paste_template_result;
 
 185   # Clone text blocks and pictures.
 
 186   my $clone_and_reset_position = sub {
 
 188     my $cloned    = $src_obj->clone_and_reset;
 
 189     $cloned->position(undef);
 
 193   my $clone_text_block = sub {
 
 194     my ($text_block) = @_;
 
 195     my $cloned       = $text_block->clone_and_reset;
 
 196     $cloned->position(undef);
 
 197     $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
 
 201   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
 
 203   if (!$params->{paste_template}) {
 
 204     $self->text_blocks($paste_template_result{text_blocks});
 
 206     $self->add_text_blocks($paste_template_result{text_blocks});
 
 209   # Clone additional parts.
 
 210   $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
 
 211   my $accessor                  = $params->{paste_template} ? "add_parts" : "parts";
 
 212   $self->$accessor($paste_template_result{parts});
 
 214   # Save new object -- we need its ID for the items.
 
 215   $self->save(cascade => 1);
 
 223     my $cloned = $item->clone_and_reset;
 
 224     $cloned->requirement_spec_id($self->id);
 
 225     $cloned->position(undef);
 
 226     $cloned->fb_number(undef) if $params->{paste_template};
 
 227     $cloned->children(map { $clone_item->($_) } @{ $item->children });
 
 229     $id_to_clone{ $item->id } = $cloned;
 
 234   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
 
 236   if (!$params->{paste_template}) {
 
 237     $self->items($paste_template_result{sections});
 
 239     $self->add_items($paste_template_result{sections});
 
 242   # Save the items -- need to do that before setting dependencies.
 
 246   foreach my $item (@{ $source->items }) {
 
 247     next unless @{ $item->dependencies };
 
 248     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
 
 251   $self->update_attributes(%attributes) unless $params->{paste_template};
 
 253   return %paste_template_result;
 
 257   my ($self, $source, %attributes) = @_;
 
 259   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
 
 263   my ($self, $template) = @_;
 
 265   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
 
 268 sub highest_version {
 
 271   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
 
 274 sub is_working_copy {
 
 277   return !$self->working_copy_id;
 
 280 sub next_version_number {
 
 283   return 1 if !$self->id;
 
 285   my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
 
 286     SELECT MAX(v.version_number)
 
 287     FROM requirement_spec_versions v
 
 288     WHERE v.requirement_spec_id IN (
 
 290       FROM requirement_specs rs
 
 292          OR (rs.working_copy_id = ?)
 
 296   return ($max_number // 0) + 1;
 
 300   my ($self, %attributes) = @_;
 
 302   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 304   my ($copy, $version);
 
 305   my $ok = $self->db->with_transaction(sub {
 
 306     delete $attributes{version_number};
 
 308     SL::DB::Manager::RequirementSpecVersion->update_all(
 
 309       set   => [ working_copy_id     => undef     ],
 
 310       where => [ requirement_spec_id => $self->id ],
 
 313     $copy    = $self->create_copy(working_copy_id => $self->id);
 
 314     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
 
 319   return $ok ? ($copy, $version) : ();
 
 322 sub invalidate_version {
 
 323   my ($self, %params) = @_;
 
 325   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 327   return if !$self->id;
 
 329   SL::DB::Manager::RequirementSpecVersion->update_all(
 
 330     set   => [ working_copy_id => undef     ],
 
 331     where => [ working_copy_id => $self->id ],
 
 336   my ($self, $other) = @_;
 
 338   return $self->id <=> $other->id;
 
 350 SL::DB::RequirementSpec - RDBO model for requirement specs
 
 354 The database structure behind requirement specs is a bit involved. The
 
 355 important thing is how working copy/versions are handled.
 
 357 The table contains three important columns: C<id> (which is also the
 
 358 primary key) and C<working_copy_id>. C<working_copy_id> is a
 
 359 self-referencing column: it can be C<NULL>, but if it isn't then it
 
 360 contains another requirement spec C<id>.
 
 362 Versions are represented similarly. The C<requirement_spec_versions>
 
 363 table has three important columns: C<id> (the primary key),
 
 364 C<requirement_spec_id> (references C<requirement_specs.id> and must
 
 365 not be C<NULL>) and C<working_copy_id> (references
 
 366 C<requirement_specs.id> as well but can be
 
 367 C<NULL>). C<working_copy_id> points to the working copy if and only if
 
 368 the working copy is currently equal to a versioned copy.
 
 370 The design is as follows:
 
 374 =item * The user is always working on a working copy. The working copy
 
 375 is identified in the database by having C<working_copy_id> set to
 
 378 =item * All other entries in this table are referred to as I<versioned
 
 379 copies>. A versioned copy is a copy of a working frozen at the moment
 
 380 in time it was created. Each versioned copy refers back to the working
 
 381 copy it belongs to: each has its C<working_copy_id> set.
 
 383 =item * Each versioned copy must be referenced from an entry in the
 
 384 table C<requirement_spec_versions> via
 
 385 C<requirement_spec_id>.
 
 387 =item * Directly after creating a versioned copy even the working copy
 
 388 itself is referenced from a version via that table's
 
 389 C<working_copy_id> column. However, any modification that will be
 
 390 visible to the customer (text, positioning etc but not internal things
 
 391 like time/cost estimation changes) will cause the version to be
 
 392 disassociated from the working copy. This is achieved via before save
 
 397 =head1 DATABASE TRIGGERS AND CHECKS
 
 399 Several database triggers and consistency checks exist that manage
 
 400 requirement specs, their items and their dependencies. These are
 
 401 described here instead of in the individual files for the other RDBO
 
 406 When you delete a requirement spec all of its dependencies (items,
 
 407 text blocks, versions etc.) are deleted by triggers.
 
 409 When you delete an item (either a section or a (sub-)function block)
 
 410 all of its children will be deleted as well. This will trigger the
 
 411 same trigger resulting in a recursive deletion with the bottom-most
 
 412 items being deleted first. Their item dependencies are deleted as
 
 417 Whenever you update a requirement spec item a trigger will fire that
 
 418 will update the parent's C<time_estimation> column. This also happens
 
 419 when an item is deleted or updated.
 
 421 =head2 CONSISTENCY CHECKS
 
 423 Several consistency checks are applied to requirement spec items:
 
 427 =item * Column C<requirement_spec_item.item_type> can only contain one of
 
 428 the values C<section>, C<function-block> or C<sub-function-block>.
 
 430 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
 
 431 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
 
 440 =item C<copy_from $source, %attributes>
 
 442 Copies everything (basic attributes like type/title/customer, items,
 
 443 text blocks, time/cost estimation) save for the versions from the
 
 444 other requirement spec object C<$source> into C<$self> and saves
 
 445 it. This is done within a transaction.
 
 447 C<%attributes> are attributes that are assigned to C<$self> after all
 
 448 the basic attributes from C<$source> have been assigned.
 
 450 This function can be used for resetting a working copy to a specific
 
 453   my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
 
 454   my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
 
 456   $requirement_spec->copy_from($versioned_copy);
 
 457   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
 
 461 Creates and returns a copy of C<$self>. The copy is already
 
 462 saved. Creating the copy happens within a transaction.
 
 464 =item C<create_version %attributes>
 
 466 Prerequisites: C<$self> must be a working copy (see the overview),
 
 467 not a versioned copy.
 
 469 This function creates a new version for C<$self>. This involves
 
 474 =item 1. The next version number is calculated using
 
 475 L</next_version_number>.
 
 477 =item 2. A copy of C<$self> is created with L</create_copy>.
 
 479 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
 
 480 created. Its attributes are copied from C<%attributes> save for the
 
 481 version number which is taken from step 1.
 
 483 =item 4. The version instance created in step 3 is referenced to the
 
 484 the copy from step 2 via C<requirement_spec_id> and to the working
 
 485 copy for which the version was created via C<working_copy_id>.
 
 489 All this is done within a transaction.
 
 491 In case of success a two-element list is returned consisting of the
 
 492 copy & version objects created in steps 3 and 2 respectively. In case
 
 493 of a failure an empty list will be returned.
 
 495 =item C<displayable_name>
 
 497 Returns a human-readable name for this instance consisting of the type
 
 500 =item C<highest_version>
 
 502 Given a working copy C<$self> this function returns the versioned copy
 
 503 of C<$self> with the highest version number. If such a version exist
 
 504 its instance is returned. Otherwise C<undef> is returned.
 
 506 This can be used for calculating the difference between the working
 
 507 copy and the last version created for it.
 
 509 =item C<invalidate_version>
 
 511 Prerequisites: C<$self> must be a working copy (see the overview),
 
 512 not a versioned copy.
 
 514 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
 
 515 table containing C<$self-E<gt>id> to C<undef>.
 
 517 =item C<is_working_copy>
 
 519 Returns trueish if C<$self> is a working copy and not a versioned
 
 520 copy. The condition for this is that C<working_copy_id> is C<undef>.
 
 522 =item C<next_version_number>
 
 524 Calculates and returns the next version number for this requirement
 
 525 spec. Version numbers start at 1 and are incremented by one for each
 
 526 version created for it, no matter whether or not it has been reverted
 
 527 to a previous version since. It boils down to this pseudo-code:
 
 529   if (has_never_had_a_version)
 
 532     return max(version_number for all versions for this requirement spec) + 1
 
 536 An alias for L</sections_sorted>.
 
 538 =item C<sections_sorted>
 
 540 Returns an array reference of requirement spec items that do not have
 
 541 a parent -- meaning that are sections.
 
 543 This is not a writer. Use the C<items> relationship for that.
 
 545 =item C<text_blocks_sorted %params>
 
 547 Returns an array reference of text blocks sorted by their positional
 
 548 column in ascending order. If the C<output_position> parameter is
 
 549 given then only the text blocks belonging to that C<output_position>
 
 552 =item C<parts_sorted>
 
 554 Returns an array reference of additional parts sorted by their
 
 555 positional column in ascending order.
 
 559 Validate values before saving. Returns list or human-readable error
 
 562 =item C<versioned_copies_sorted %params>
 
 564 Returns an array reference of versioned copies sorted by their version
 
 565 number in ascending order. If the C<max_version_number> parameter is
 
 566 given then only the versioned copies whose version number is less than
 
 567 or equal to C<max_version_number> are returned.
 
 577 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>