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::RequirementSpecOrder',
 
  33     column_map     => { id => 'requirement_spec_id' },
 
  37 __PACKAGE__->meta->initialize;
 
  39 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
 
  45   push @errors, t8('The title is missing.') if !$self->title;
 
  50 sub _before_save_initialize_not_null_columns {
 
  53   $self->previous_section_number(0) if !defined $self->previous_section_number;
 
  54   $self->previous_fb_number(0)      if !defined $self->previous_fb_number;
 
  59 sub text_blocks_sorted {
 
  60   my ($self, %params) = _hashify(1, @_);
 
  62   my @text_blocks = @{ $self->text_blocks };
 
  63   @text_blocks    = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
 
  64   @text_blocks    = sort { $a->position        <=> $b->position            } @text_blocks;
 
  70   my ($self, @rest) = @_;
 
  72   croak "This sub is not a writer" if @rest;
 
  74   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
 
  77 sub sections { §ions_sorted; }
 
  80   my ($self, %params) = _hashify(1, @_);
 
  81   my $by              = $params{by} || 'itime';
 
  83   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
 
  86 sub displayable_name {
 
  89   return sprintf('%s: "%s"', $self->type->description, $self->title);
 
  92 sub versioned_copies_sorted {
 
  93   my ($self, %params) = _hashify(1, @_);
 
  95   my @copies = @{ $self->versioned_copies };
 
  96   @copies    = grep { $_->version->version_number <=  $params{max_version_number} } @copies if $params{max_version_number};
 
  97   @copies    = sort { $a->version->version_number <=> $b->version->version_number } @copies;
 
 103   my ($self, %params) = @_;
 
 105   return $self->_create_copy(%params) if $self->db->in_transaction;
 
 108   if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
 
 109     $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
 
 117   my ($self, %params) = @_;
 
 119   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
 
 120   $copy->copy_from($self, %params);
 
 126   my ($self, $params, %attributes) = @_;
 
 128   my $source = $params->{source};
 
 130   croak "Missing parameter 'source'" unless $source;
 
 133   if (!$params->{paste_template}) {
 
 134     $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 is_template)),
 
 138   my %paste_template_result;
 
 141   my $clone_text_block = sub {
 
 142     my ($text_block) = @_;
 
 143     my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
 
 144     $cloned->position(undef);
 
 148   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
 
 150   if (!$params->{paste_template}) {
 
 151     $self->text_blocks($paste_template_result{text_blocks});
 
 153     $self->add_text_blocks($paste_template_result{text_blocks});
 
 156   # Save new object -- we need its ID for the items.
 
 165     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
 
 166     $cloned->requirement_spec_id($self->id);
 
 167     $cloned->position(undef);
 
 168     $cloned->children(map { $clone_item->($_) } @{ $item->children });
 
 170     $id_to_clone{ $item->id } = $cloned;
 
 175   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
 
 177   if (!$params->{paste_template}) {
 
 178     $self->items($paste_template_result{sections});
 
 180     $self->add_items($paste_template_result{sections});
 
 183   # Save the items -- need to do that before setting dependencies.
 
 187   foreach my $item (@{ $source->items }) {
 
 188     next unless @{ $item->dependencies };
 
 189     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
 
 192   $self->update_attributes(%attributes) unless $params->{paste_template};
 
 194   return %paste_template_result;
 
 198   my ($self, $source, %attributes) = @_;
 
 200   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
 
 204   my ($self, $template) = @_;
 
 206   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
 
 209 sub highest_version {
 
 212   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
 
 215 sub is_working_copy {
 
 218   return !$self->working_copy_id;
 
 221 sub next_version_number {
 
 224   return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
 
 228   my ($self, %attributes) = @_;
 
 230   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 232   my ($copy, $version);
 
 233   my $ok = $self->db->with_transaction(sub {
 
 234     delete $attributes{version_number};
 
 236     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
 
 237     $copy    = $self->create_copy;
 
 238     $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
 
 239     $self->update_attributes(version_id => $version->id);
 
 244   return $ok ? ($copy, $version) : ();
 
 247 sub invalidate_version {
 
 248   my ($self, %params) = @_;
 
 250   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
 252   return if !$self->id || !$self->version_id;
 
 253   $self->update_attributes(version_id => undef);
 
 265 SL::DB::RequirementSpec - RDBO model for requirement specs
 
 269 The database structure behind requirement specs is a bit involved. The
 
 270 important thing is how working copy/versions are handled.
 
 272 The table contains three important columns: C<id> (which is also the
 
 273 primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
 
 274 is a self-referencing column: it can be C<NULL>, but if it isn't then
 
 275 it contains another requirement spec C<id>. C<version_id> on the other
 
 276 hand references the table C<requirement_spec_versions>.
 
 278 The design is as follows:
 
 282 =item * The user is always working on a working copy. The working copy
 
 283 is identified in the database by having C<working_copy_id> set to
 
 286 =item * All other entries in this table are referred to as I<versioned
 
 287 copies>. A versioned copy is a copy of a working frozen at the moment
 
 288 in time it was created. Each versioned copy refers back to the working
 
 289 copy it belongs to: each has its C<working_copy_id> set.
 
 291 =item * Each versioned copy must reference an entry in the table
 
 292 C<requirement_spec_versions>. Meaning: for each versioned copy
 
 293 C<version_id> must not be C<NULL>.
 
 295 =item * Directly after creating a versioned copy even the working copy
 
 296 itself points to a certain version via its C<version_id> column: to
 
 297 the same version that the versioned copy just created points
 
 298 to. However, any modification that will be visible to the customer
 
 299 (text, positioning etc but not internal things like time/cost
 
 300 estimation changes) will cause the working copy to be set to 'no
 
 301 version' again. This is achieved via before save hooks in Perl.
 
 305 =head1 DATABASE TRIGGERS AND CHECKS
 
 307 Several database triggers and consistency checks exist that manage
 
 308 requirement specs, their items and their dependencies. These are
 
 309 described here instead of in the individual files for the other RDBO
 
 314 When you delete a requirement spec all of its dependencies (items,
 
 315 text blocks, versions etc.) are deleted by triggers.
 
 317 When you delete an item (either a section or a (sub-)function block)
 
 318 all of its children will be deleted as well. This will trigger the
 
 319 same trigger resulting in a recursive deletion with the bottom-most
 
 320 items being deleted first. Their item dependencies are deleted as
 
 325 Whenever you update a requirement spec item a trigger will fire that
 
 326 will update the parent's C<time_estimation> column. This also happens
 
 327 when an item is deleted or updated.
 
 329 =head2 CONSISTENCY CHECKS
 
 331 Several consistency checks are applied to requirement spec items:
 
 335 =item * Column C<requirement_spec_item.item_type> can only contain one of
 
 336 the values C<section>, C<function-block> or C<sub-function-block>.
 
 338 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
 
 339 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
 
 348 =item C<copy_from $source, %attributes>
 
 350 Copies everything (basic attributes like type/title/customer, items,
 
 351 text blocks, time/cost estimation) save for the versions from the
 
 352 other requirement spec object C<$source> into C<$self> and saves
 
 353 it. This is done within a transaction.
 
 355 C<%attributes> are attributes that are assigned to C<$self> after all
 
 356 the basic attributes from C<$source> have been assigned.
 
 358 This function can be used for resetting a working copy to a specific
 
 361  my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
 
 362  my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
 
 364   $requirement_spec->copy_from(
 
 366     version_id => $versioned_copy->version_id,
 
 371 Creates and returns a copy of C<$self>. The copy is already
 
 372 saved. Creating the copy happens within a transaction.
 
 374 =item C<create_version %attributes>
 
 376 Prerequisites: C<$self> must be a working copy (see the overview),
 
 377 not a versioned copy.
 
 379 This function creates a new version for C<$self>. This involves
 
 384 =item 1. The next version number is calculated using
 
 385 L</next_version_number>.
 
 387 =item 2. An instance of L<SL::DB::RequirementSpecVersion> is
 
 388 created. Its attributes are copied from C<%attributes> save for the
 
 389 version number which is taken from step 1.
 
 391 =item 3. A copy of C<$self> is created with L</create_copy>.
 
 393 =item 4. The version instance created in step is assigned to the copy
 
 396 =item 5. The C<version_id> in C<$self> is set to the copy's ID from
 
 401 All this is done within a transaction.
 
 403 In case of success a two-element list is returned consisting of the
 
 404 copy & version objects created in steps 3 and 2 respectively. In case
 
 405 of a failure an empty list will be returned.
 
 407 =item C<displayable_name>
 
 409 Returns a human-readable name for this instance consisting of the type
 
 412 =item C<highest_version>
 
 414 Given a working copy C<$self> this function returns the versioned copy
 
 415 of C<$self> with the highest version number. If such a version exist
 
 416 its instance is returned. Otherwise C<undef> is returned.
 
 418 This can be used for calculating the difference between the working
 
 419 copy and the last version created for it.
 
 421 =item C<invalidate_version>
 
 423 Prerequisites: C<$self> must be a working copy (see the overview),
 
 424 not a versioned copy.
 
 426 Sets the C<version_id> field to C<undef> and saves C<$self>.
 
 428 =item C<is_working_copy>
 
 430 Returns trueish if C<$self> is a working copy and not a versioned
 
 431 copy. The condition for this is that C<working_copy_id> is C<undef>.
 
 433 =item C<next_version_number>
 
 435 Calculates and returns the next version number for this requirement
 
 436 spec. Version numbers start at 1 and are incremented by one for each
 
 437 version created for it, no matter whether or not it has been reverted
 
 438 to a previous version since. It boils down to this pseudo-code:
 
 440   if (has_never_had_a_version)
 
 443     return max(version_number for all versions for this requirement spec) + 1
 
 447 An alias for L</sections_sorted>.
 
 449 =item C<sections_sorted>
 
 451 Returns an array reference of requirement spec items that do not have
 
 452 a parent -- meaning that are sections.
 
 454 This is not a writer. Use the C<items> relationship for that.
 
 456 =item C<text_blocks_sorted %params>
 
 458 Returns an array reference of text blocks sorted by their positional
 
 459 column in ascending order. If the C<output_position> parameter is
 
 460 given then only the text blocks belonging to that C<output_position>
 
 465 Validate values before saving. Returns list or human-readable error
 
 468 =item C<versioned_copies_sorted %params>
 
 470 Returns an array reference of versioned copies sorted by their version
 
 471 number in ascending order. If the C<max_version_number> parameter is
 
 472 given then only the versioned copies whose version number is less than
 
 473 or equal to C<max_version_number> are returned.
 
 483 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>