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::LinkedRecords;
13 use SL::Locale::String;
14 use SL::Util qw(_hashify);
16 __PACKAGE__->meta->add_relationship(
18 type => 'one to many',
19 class => 'SL::DB::RequirementSpecItem',
20 column_map => { id => 'requirement_spec_id' },
23 type => 'one to many',
24 class => 'SL::DB::RequirementSpecTextBlock',
25 column_map => { id => 'requirement_spec_id' },
28 type => 'one to many',
29 class => 'SL::DB::RequirementSpec',
30 column_map => { id => 'working_copy_id' },
33 type => 'one to many',
34 class => 'SL::DB::RequirementSpecVersion',
35 column_map => { id => 'requirement_spec_id' },
37 working_copy_versions => {
38 type => 'one to many',
39 class => 'SL::DB::RequirementSpecVersion',
40 column_map => { id => 'working_copy_id' },
43 type => 'one to many',
44 class => 'SL::DB::RequirementSpecOrder',
45 column_map => { id => 'requirement_spec_id' },
48 type => 'one to many',
49 class => 'SL::DB::RequirementSpecPart',
50 column_map => { id => 'requirement_spec_id' },
54 __PACKAGE__->meta->initialize;
56 __PACKAGE__->attr_duration(qw(time_estimation));
58 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
64 push @errors, t8('The title is missing.') if !$self->title;
69 sub _before_save_initialize_not_null_columns {
72 for (qw(previous_section_number previous_fb_number previous_picture_number)) {
73 $self->$_(0) if !defined $self->$_;
82 croak "Not a writer" if scalar(@_) > 1;
84 return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
87 sub text_blocks_sorted {
88 my ($self, %params) = _hashify(1, @_);
90 my @text_blocks = @{ $self->text_blocks };
91 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
92 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
98 my ($self, @rest) = @_;
100 croak "This sub is not a writer" if @rest;
102 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
105 sub sections { §ions_sorted; }
108 my ($self, %params) = _hashify(1, @_);
109 my $by = $params{by} || 'itime';
111 return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
114 sub displayable_name {
117 return sprintf('%s: "%s"', $self->type->description, $self->title);
120 sub versioned_copies_sorted {
121 my ($self, %params) = _hashify(1, @_);
123 my @copies = @{ $self->versioned_copies };
124 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
125 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
131 my ($self, @rest) = @_;
133 croak "This sub is not a writer" if @rest;
135 return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
139 my ($self, %params) = @_;
141 return $self->_create_copy(%params) if $self->db->in_transaction;
144 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
145 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
153 my ($self, %params) = @_;
155 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
156 $copy->copy_from($self, %params);
162 my ($self, $params, %attributes) = @_;
164 my $source = $params->{source};
166 croak "Missing parameter 'source'" unless $source;
169 if (!$params->{paste_template}) {
170 $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)),
174 my %paste_template_result;
176 # Clone text blocks and pictures.
177 my $clone_and_reset_position = sub {
179 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($src_obj);
180 $cloned->position(undef);
184 my $clone_text_block = sub {
185 my ($text_block) = @_;
186 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
187 $cloned->position(undef);
188 $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
192 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
194 if (!$params->{paste_template}) {
195 $self->text_blocks($paste_template_result{text_blocks});
197 $self->add_text_blocks($paste_template_result{text_blocks});
200 # Clone additional parts.
201 $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
202 my $accessor = $params->{paste_template} ? "add_parts" : "parts";
203 $self->$accessor($paste_template_result{parts});
205 # Save new object -- we need its ID for the items.
214 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
215 $cloned->requirement_spec_id($self->id);
216 $cloned->position(undef);
217 $cloned->fb_number(undef) if $params->{paste_template};
218 $cloned->children(map { $clone_item->($_) } @{ $item->children });
220 $id_to_clone{ $item->id } = $cloned;
225 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
227 if (!$params->{paste_template}) {
228 $self->items($paste_template_result{sections});
230 $self->add_items($paste_template_result{sections});
233 # Save the items -- need to do that before setting dependencies.
237 foreach my $item (@{ $source->items }) {
238 next unless @{ $item->dependencies };
239 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
242 $self->update_attributes(%attributes) unless $params->{paste_template};
244 return %paste_template_result;
248 my ($self, $source, %attributes) = @_;
250 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
254 my ($self, $template) = @_;
256 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
259 sub highest_version {
262 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
265 sub is_working_copy {
268 return !$self->working_copy_id;
271 sub next_version_number {
274 return 1 if !$self->id;
276 my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
277 SELECT MAX(v.version_number)
278 FROM requirement_spec_versions v
279 WHERE v.requirement_spec_id IN (
281 FROM requirement_specs rs
283 OR (rs.working_copy_id = ?)
287 return ($max_number // 0) + 1;
291 my ($self, %attributes) = @_;
293 croak "Cannot work on a versioned copy" if $self->working_copy_id;
295 my ($copy, $version);
296 my $ok = $self->db->with_transaction(sub {
297 delete $attributes{version_number};
299 SL::DB::Manager::RequirementSpecVersion->update_all(
300 set => [ working_copy_id => undef ],
301 where => [ requirement_spec_id => $self->id ],
304 $copy = $self->create_copy(working_copy_id => $self->id);
305 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
310 return $ok ? ($copy, $version) : ();
313 sub invalidate_version {
314 my ($self, %params) = @_;
316 croak "Cannot work on a versioned copy" if $self->working_copy_id;
318 return if !$self->id;
320 SL::DB::Manager::RequirementSpecVersion->update_all(
321 set => [ working_copy_id => undef ],
322 where => [ working_copy_id => $self->id ],
327 my ($self, $other) = @_;
329 return $self->id <=> $other->id;
341 SL::DB::RequirementSpec - RDBO model for requirement specs
345 The database structure behind requirement specs is a bit involved. The
346 important thing is how working copy/versions are handled.
348 The table contains three important columns: C<id> (which is also the
349 primary key) and C<working_copy_id>. C<working_copy_id> is a
350 self-referencing column: it can be C<NULL>, but if it isn't then it
351 contains another requirement spec C<id>.
353 Versions are represented similarly. The C<requirement_spec_versions>
354 table has three important columns: C<id> (the primary key),
355 C<requirement_spec_id> (references C<requirement_specs.id> and must
356 not be C<NULL>) and C<working_copy_id> (references
357 C<requirement_specs.id> as well but can be
358 C<NULL>). C<working_copy_id> points to the working copy if and only if
359 the working copy is currently equal to a versioned copy.
361 The design is as follows:
365 =item * The user is always working on a working copy. The working copy
366 is identified in the database by having C<working_copy_id> set to
369 =item * All other entries in this table are referred to as I<versioned
370 copies>. A versioned copy is a copy of a working frozen at the moment
371 in time it was created. Each versioned copy refers back to the working
372 copy it belongs to: each has its C<working_copy_id> set.
374 =item * Each versioned copy must be referenced from an entry in the
375 table C<requirement_spec_versions> via
376 C<requirement_spec_id>.
378 =item * Directly after creating a versioned copy even the working copy
379 itself is referenced from a version via that table's
380 C<working_copy_id> column. However, any modification that will be
381 visible to the customer (text, positioning etc but not internal things
382 like time/cost estimation changes) will cause the version to be
383 disassociated from the working copy. This is achieved via before save
388 =head1 DATABASE TRIGGERS AND CHECKS
390 Several database triggers and consistency checks exist that manage
391 requirement specs, their items and their dependencies. These are
392 described here instead of in the individual files for the other RDBO
397 When you delete a requirement spec all of its dependencies (items,
398 text blocks, versions etc.) are deleted by triggers.
400 When you delete an item (either a section or a (sub-)function block)
401 all of its children will be deleted as well. This will trigger the
402 same trigger resulting in a recursive deletion with the bottom-most
403 items being deleted first. Their item dependencies are deleted as
408 Whenever you update a requirement spec item a trigger will fire that
409 will update the parent's C<time_estimation> column. This also happens
410 when an item is deleted or updated.
412 =head2 CONSISTENCY CHECKS
414 Several consistency checks are applied to requirement spec items:
418 =item * Column C<requirement_spec_item.item_type> can only contain one of
419 the values C<section>, C<function-block> or C<sub-function-block>.
421 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
422 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
431 =item C<copy_from $source, %attributes>
433 Copies everything (basic attributes like type/title/customer, items,
434 text blocks, time/cost estimation) save for the versions from the
435 other requirement spec object C<$source> into C<$self> and saves
436 it. This is done within a transaction.
438 C<%attributes> are attributes that are assigned to C<$self> after all
439 the basic attributes from C<$source> have been assigned.
441 This function can be used for resetting a working copy to a specific
444 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
445 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
447 $requirement_spec->copy_from($versioned_copy);
448 $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
452 Creates and returns a copy of C<$self>. The copy is already
453 saved. Creating the copy happens within a transaction.
455 =item C<create_version %attributes>
457 Prerequisites: C<$self> must be a working copy (see the overview),
458 not a versioned copy.
460 This function creates a new version for C<$self>. This involves
465 =item 1. The next version number is calculated using
466 L</next_version_number>.
468 =item 2. A copy of C<$self> is created with L</create_copy>.
470 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
471 created. Its attributes are copied from C<%attributes> save for the
472 version number which is taken from step 1.
474 =item 4. The version instance created in step 3 is referenced to the
475 the copy from step 2 via C<requirement_spec_id> and to the working
476 copy for which the version was created via C<working_copy_id>.
480 All this is done within a transaction.
482 In case of success a two-element list is returned consisting of the
483 copy & version objects created in steps 3 and 2 respectively. In case
484 of a failure an empty list will be returned.
486 =item C<displayable_name>
488 Returns a human-readable name for this instance consisting of the type
491 =item C<highest_version>
493 Given a working copy C<$self> this function returns the versioned copy
494 of C<$self> with the highest version number. If such a version exist
495 its instance is returned. Otherwise C<undef> is returned.
497 This can be used for calculating the difference between the working
498 copy and the last version created for it.
500 =item C<invalidate_version>
502 Prerequisites: C<$self> must be a working copy (see the overview),
503 not a versioned copy.
505 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
506 table containing C<$self-E<gt>id> to C<undef>.
508 =item C<is_working_copy>
510 Returns trueish if C<$self> is a working copy and not a versioned
511 copy. The condition for this is that C<working_copy_id> is C<undef>.
513 =item C<next_version_number>
515 Calculates and returns the next version number for this requirement
516 spec. Version numbers start at 1 and are incremented by one for each
517 version created for it, no matter whether or not it has been reverted
518 to a previous version since. It boils down to this pseudo-code:
520 if (has_never_had_a_version)
523 return max(version_number for all versions for this requirement spec) + 1
527 An alias for L</sections_sorted>.
529 =item C<sections_sorted>
531 Returns an array reference of requirement spec items that do not have
532 a parent -- meaning that are sections.
534 This is not a writer. Use the C<items> relationship for that.
536 =item C<text_blocks_sorted %params>
538 Returns an array reference of text blocks sorted by their positional
539 column in ascending order. If the C<output_position> parameter is
540 given then only the text blocks belonging to that C<output_position>
543 =item C<parts_sorted>
545 Returns an array reference of additional parts sorted by their
546 positional column in ascending order.
550 Validate values before saving. Returns list or human-readable error
553 =item C<versioned_copies_sorted %params>
555 Returns an array reference of versioned copies sorted by their version
556 number in ascending order. If the C<max_version_number> parameter is
557 given then only the versioned copies whose version number is less than
558 or equal to C<max_version_number> are returned.
568 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>