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' },
47 type => 'one to many',
48 class => 'SL::DB::RequirementSpecPart',
49 column_map => { id => 'requirement_spec_id' },
53 __PACKAGE__->meta->initialize;
55 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
61 push @errors, t8('The title is missing.') if !$self->title;
66 sub _before_save_initialize_not_null_columns {
69 for (qw(previous_section_number previous_fb_number previous_picture_number)) {
70 $self->$_(0) if !defined $self->$_;
79 croak "Not a writer" if scalar(@_) > 1;
81 return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
84 sub text_blocks_sorted {
85 my ($self, %params) = _hashify(1, @_);
87 my @text_blocks = @{ $self->text_blocks };
88 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
89 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
95 my ($self, @rest) = @_;
97 croak "This sub is not a writer" if @rest;
99 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
102 sub sections { §ions_sorted; }
105 my ($self, %params) = _hashify(1, @_);
106 my $by = $params{by} || 'itime';
108 return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
111 sub displayable_name {
114 return sprintf('%s: "%s"', $self->type->description, $self->title);
117 sub versioned_copies_sorted {
118 my ($self, %params) = _hashify(1, @_);
120 my @copies = @{ $self->versioned_copies };
121 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
122 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
128 my ($self, @rest) = @_;
130 croak "This sub is not a writer" if @rest;
132 return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
136 my ($self, %params) = @_;
138 return $self->_create_copy(%params) if $self->db->in_transaction;
141 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
142 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
150 my ($self, %params) = @_;
152 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
153 $copy->copy_from($self, %params);
159 my ($self, $params, %attributes) = @_;
161 my $source = $params->{source};
163 croak "Missing parameter 'source'" unless $source;
166 if (!$params->{paste_template}) {
167 $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)),
171 my %paste_template_result;
173 # Clone text blocks and pictures.
174 my $clone_picture = sub {
176 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($picture);
177 $cloned->position(undef);
181 my $clone_text_block = sub {
182 my ($text_block) = @_;
183 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
184 $cloned->position(undef);
185 $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
189 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
191 if (!$params->{paste_template}) {
192 $self->text_blocks($paste_template_result{text_blocks});
194 $self->add_text_blocks($paste_template_result{text_blocks});
197 # Save new object -- we need its ID for the items.
206 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
207 $cloned->requirement_spec_id($self->id);
208 $cloned->position(undef);
209 $cloned->fb_number(undef) if $params->{paste_template};
210 $cloned->children(map { $clone_item->($_) } @{ $item->children });
212 $id_to_clone{ $item->id } = $cloned;
217 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
219 if (!$params->{paste_template}) {
220 $self->items($paste_template_result{sections});
222 $self->add_items($paste_template_result{sections});
225 # Save the items -- need to do that before setting dependencies.
229 foreach my $item (@{ $source->items }) {
230 next unless @{ $item->dependencies };
231 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
234 $self->update_attributes(%attributes) unless $params->{paste_template};
236 return %paste_template_result;
240 my ($self, $source, %attributes) = @_;
242 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
246 my ($self, $template) = @_;
248 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
251 sub highest_version {
254 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
257 sub is_working_copy {
260 return !$self->working_copy_id;
263 sub next_version_number {
266 return 1 if !$self->id;
268 my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
269 SELECT MAX(v.version_number)
270 FROM requirement_spec_versions v
271 WHERE v.requirement_spec_id IN (
273 FROM requirement_specs rs
275 OR (rs.working_copy_id = ?)
279 return ($max_number // 0) + 1;
283 my ($self, %attributes) = @_;
285 croak "Cannot work on a versioned copy" if $self->working_copy_id;
287 my ($copy, $version);
288 my $ok = $self->db->with_transaction(sub {
289 delete $attributes{version_number};
291 SL::DB::Manager::RequirementSpecVersion->update_all(
292 set => [ working_copy_id => undef ],
293 where => [ requirement_spec_id => $self->id ],
296 $copy = $self->create_copy(working_copy_id => $self->id);
297 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
302 return $ok ? ($copy, $version) : ();
305 sub invalidate_version {
306 my ($self, %params) = @_;
308 croak "Cannot work on a versioned copy" if $self->working_copy_id;
310 return if !$self->id;
312 SL::DB::Manager::RequirementSpecVersion->update_all(
313 set => [ working_copy_id => undef ],
314 where => [ working_copy_id => $self->id ],
319 my ($self, $other) = @_;
321 return $self->id <=> $other->id;
333 SL::DB::RequirementSpec - RDBO model for requirement specs
337 The database structure behind requirement specs is a bit involved. The
338 important thing is how working copy/versions are handled.
340 The table contains three important columns: C<id> (which is also the
341 primary key) and C<working_copy_id>. C<working_copy_id> is a
342 self-referencing column: it can be C<NULL>, but if it isn't then it
343 contains another requirement spec C<id>.
345 Versions are represented similarly. The C<requirement_spec_versions>
346 table has three important columns: C<id> (the primary key),
347 C<requirement_spec_id> (references C<requirement_specs.id> and must
348 not be C<NULL>) and C<working_copy_id> (references
349 C<requirement_specs.id> as well but can be
350 C<NULL>). C<working_copy_id> points to the working copy if and only if
351 the working copy is currently equal to a versioned copy.
353 The design is as follows:
357 =item * The user is always working on a working copy. The working copy
358 is identified in the database by having C<working_copy_id> set to
361 =item * All other entries in this table are referred to as I<versioned
362 copies>. A versioned copy is a copy of a working frozen at the moment
363 in time it was created. Each versioned copy refers back to the working
364 copy it belongs to: each has its C<working_copy_id> set.
366 =item * Each versioned copy must be referenced from an entry in the
367 table C<requirement_spec_versions> via
368 C<requirement_spec_id>.
370 =item * Directly after creating a versioned copy even the working copy
371 itself is referenced from a version via that table's
372 C<working_copy_id> column. However, any modification that will be
373 visible to the customer (text, positioning etc but not internal things
374 like time/cost estimation changes) will cause the version to be
375 disassociated from the working copy. This is achieved via before save
380 =head1 DATABASE TRIGGERS AND CHECKS
382 Several database triggers and consistency checks exist that manage
383 requirement specs, their items and their dependencies. These are
384 described here instead of in the individual files for the other RDBO
389 When you delete a requirement spec all of its dependencies (items,
390 text blocks, versions etc.) are deleted by triggers.
392 When you delete an item (either a section or a (sub-)function block)
393 all of its children will be deleted as well. This will trigger the
394 same trigger resulting in a recursive deletion with the bottom-most
395 items being deleted first. Their item dependencies are deleted as
400 Whenever you update a requirement spec item a trigger will fire that
401 will update the parent's C<time_estimation> column. This also happens
402 when an item is deleted or updated.
404 =head2 CONSISTENCY CHECKS
406 Several consistency checks are applied to requirement spec items:
410 =item * Column C<requirement_spec_item.item_type> can only contain one of
411 the values C<section>, C<function-block> or C<sub-function-block>.
413 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
414 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
423 =item C<copy_from $source, %attributes>
425 Copies everything (basic attributes like type/title/customer, items,
426 text blocks, time/cost estimation) save for the versions from the
427 other requirement spec object C<$source> into C<$self> and saves
428 it. This is done within a transaction.
430 C<%attributes> are attributes that are assigned to C<$self> after all
431 the basic attributes from C<$source> have been assigned.
433 This function can be used for resetting a working copy to a specific
436 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
437 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
439 $requirement_spec->copy_from($versioned_copy);
440 $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
444 Creates and returns a copy of C<$self>. The copy is already
445 saved. Creating the copy happens within a transaction.
447 =item C<create_version %attributes>
449 Prerequisites: C<$self> must be a working copy (see the overview),
450 not a versioned copy.
452 This function creates a new version for C<$self>. This involves
457 =item 1. The next version number is calculated using
458 L</next_version_number>.
460 =item 2. A copy of C<$self> is created with L</create_copy>.
462 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
463 created. Its attributes are copied from C<%attributes> save for the
464 version number which is taken from step 1.
466 =item 4. The version instance created in step 3 is referenced to the
467 the copy from step 2 via C<requirement_spec_id> and to the working
468 copy for which the version was created via C<working_copy_id>.
472 All this is done within a transaction.
474 In case of success a two-element list is returned consisting of the
475 copy & version objects created in steps 3 and 2 respectively. In case
476 of a failure an empty list will be returned.
478 =item C<displayable_name>
480 Returns a human-readable name for this instance consisting of the type
483 =item C<highest_version>
485 Given a working copy C<$self> this function returns the versioned copy
486 of C<$self> with the highest version number. If such a version exist
487 its instance is returned. Otherwise C<undef> is returned.
489 This can be used for calculating the difference between the working
490 copy and the last version created for it.
492 =item C<invalidate_version>
494 Prerequisites: C<$self> must be a working copy (see the overview),
495 not a versioned copy.
497 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
498 table containing C<$self-E<gt>id> to C<undef>.
500 =item C<is_working_copy>
502 Returns trueish if C<$self> is a working copy and not a versioned
503 copy. The condition for this is that C<working_copy_id> is C<undef>.
505 =item C<next_version_number>
507 Calculates and returns the next version number for this requirement
508 spec. Version numbers start at 1 and are incremented by one for each
509 version created for it, no matter whether or not it has been reverted
510 to a previous version since. It boils down to this pseudo-code:
512 if (has_never_had_a_version)
515 return max(version_number for all versions for this requirement spec) + 1
519 An alias for L</sections_sorted>.
521 =item C<sections_sorted>
523 Returns an array reference of requirement spec items that do not have
524 a parent -- meaning that are sections.
526 This is not a writer. Use the C<items> relationship for that.
528 =item C<text_blocks_sorted %params>
530 Returns an array reference of text blocks sorted by their positional
531 column in ascending order. If the C<output_position> parameter is
532 given then only the text blocks belonging to that C<output_position>
535 =item C<parts_sorted>
537 Returns an array reference of additional parts sorted by their
538 positional column in ascending order.
542 Validate values before saving. Returns list or human-readable error
545 =item C<versioned_copies_sorted %params>
547 Returns an array reference of versioned copies sorted by their version
548 number in ascending order. If the C<max_version_number> parameter is
549 given then only the versioned copies whose version number is less than
550 or equal to C<max_version_number> are returned.
560 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>