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->do_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 = Rose::DB::Object::Helpers::clone_and_reset($self);
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 my %paste_template_result;
180 # Clone text blocks and pictures.
181 my $clone_and_reset_position = sub {
183 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($src_obj);
184 $cloned->position(undef);
188 my $clone_text_block = sub {
189 my ($text_block) = @_;
190 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
191 $cloned->position(undef);
192 $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
196 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
198 if (!$params->{paste_template}) {
199 $self->text_blocks($paste_template_result{text_blocks});
201 $self->add_text_blocks($paste_template_result{text_blocks});
204 # Clone additional parts.
205 $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
206 my $accessor = $params->{paste_template} ? "add_parts" : "parts";
207 $self->$accessor($paste_template_result{parts});
209 # Save new object -- we need its ID for the items.
218 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
219 $cloned->requirement_spec_id($self->id);
220 $cloned->position(undef);
221 $cloned->fb_number(undef) if $params->{paste_template};
222 $cloned->children(map { $clone_item->($_) } @{ $item->children });
224 $id_to_clone{ $item->id } = $cloned;
229 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
231 if (!$params->{paste_template}) {
232 $self->items($paste_template_result{sections});
234 $self->add_items($paste_template_result{sections});
237 # Save the items -- need to do that before setting dependencies.
241 foreach my $item (@{ $source->items }) {
242 next unless @{ $item->dependencies };
243 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
246 $self->update_attributes(%attributes) unless $params->{paste_template};
248 return %paste_template_result;
252 my ($self, $source, %attributes) = @_;
254 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
258 my ($self, $template) = @_;
260 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
263 sub highest_version {
266 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
269 sub is_working_copy {
272 return !$self->working_copy_id;
275 sub next_version_number {
278 return 1 if !$self->id;
280 my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
281 SELECT MAX(v.version_number)
282 FROM requirement_spec_versions v
283 WHERE v.requirement_spec_id IN (
285 FROM requirement_specs rs
287 OR (rs.working_copy_id = ?)
291 return ($max_number // 0) + 1;
295 my ($self, %attributes) = @_;
297 croak "Cannot work on a versioned copy" if $self->working_copy_id;
299 my ($copy, $version);
300 my $ok = $self->db->with_transaction(sub {
301 delete $attributes{version_number};
303 SL::DB::Manager::RequirementSpecVersion->update_all(
304 set => [ working_copy_id => undef ],
305 where => [ requirement_spec_id => $self->id ],
308 $copy = $self->create_copy(working_copy_id => $self->id);
309 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
314 return $ok ? ($copy, $version) : ();
317 sub invalidate_version {
318 my ($self, %params) = @_;
320 croak "Cannot work on a versioned copy" if $self->working_copy_id;
322 return if !$self->id;
324 SL::DB::Manager::RequirementSpecVersion->update_all(
325 set => [ working_copy_id => undef ],
326 where => [ working_copy_id => $self->id ],
331 my ($self, $other) = @_;
333 return $self->id <=> $other->id;
345 SL::DB::RequirementSpec - RDBO model for requirement specs
349 The database structure behind requirement specs is a bit involved. The
350 important thing is how working copy/versions are handled.
352 The table contains three important columns: C<id> (which is also the
353 primary key) and C<working_copy_id>. C<working_copy_id> is a
354 self-referencing column: it can be C<NULL>, but if it isn't then it
355 contains another requirement spec C<id>.
357 Versions are represented similarly. The C<requirement_spec_versions>
358 table has three important columns: C<id> (the primary key),
359 C<requirement_spec_id> (references C<requirement_specs.id> and must
360 not be C<NULL>) and C<working_copy_id> (references
361 C<requirement_specs.id> as well but can be
362 C<NULL>). C<working_copy_id> points to the working copy if and only if
363 the working copy is currently equal to a versioned copy.
365 The design is as follows:
369 =item * The user is always working on a working copy. The working copy
370 is identified in the database by having C<working_copy_id> set to
373 =item * All other entries in this table are referred to as I<versioned
374 copies>. A versioned copy is a copy of a working frozen at the moment
375 in time it was created. Each versioned copy refers back to the working
376 copy it belongs to: each has its C<working_copy_id> set.
378 =item * Each versioned copy must be referenced from an entry in the
379 table C<requirement_spec_versions> via
380 C<requirement_spec_id>.
382 =item * Directly after creating a versioned copy even the working copy
383 itself is referenced from a version via that table's
384 C<working_copy_id> column. However, any modification that will be
385 visible to the customer (text, positioning etc but not internal things
386 like time/cost estimation changes) will cause the version to be
387 disassociated from the working copy. This is achieved via before save
392 =head1 DATABASE TRIGGERS AND CHECKS
394 Several database triggers and consistency checks exist that manage
395 requirement specs, their items and their dependencies. These are
396 described here instead of in the individual files for the other RDBO
401 When you delete a requirement spec all of its dependencies (items,
402 text blocks, versions etc.) are deleted by triggers.
404 When you delete an item (either a section or a (sub-)function block)
405 all of its children will be deleted as well. This will trigger the
406 same trigger resulting in a recursive deletion with the bottom-most
407 items being deleted first. Their item dependencies are deleted as
412 Whenever you update a requirement spec item a trigger will fire that
413 will update the parent's C<time_estimation> column. This also happens
414 when an item is deleted or updated.
416 =head2 CONSISTENCY CHECKS
418 Several consistency checks are applied to requirement spec items:
422 =item * Column C<requirement_spec_item.item_type> can only contain one of
423 the values C<section>, C<function-block> or C<sub-function-block>.
425 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
426 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
435 =item C<copy_from $source, %attributes>
437 Copies everything (basic attributes like type/title/customer, items,
438 text blocks, time/cost estimation) save for the versions from the
439 other requirement spec object C<$source> into C<$self> and saves
440 it. This is done within a transaction.
442 C<%attributes> are attributes that are assigned to C<$self> after all
443 the basic attributes from C<$source> have been assigned.
445 This function can be used for resetting a working copy to a specific
448 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
449 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
451 $requirement_spec->copy_from($versioned_copy);
452 $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
456 Creates and returns a copy of C<$self>. The copy is already
457 saved. Creating the copy happens within a transaction.
459 =item C<create_version %attributes>
461 Prerequisites: C<$self> must be a working copy (see the overview),
462 not a versioned copy.
464 This function creates a new version for C<$self>. This involves
469 =item 1. The next version number is calculated using
470 L</next_version_number>.
472 =item 2. A copy of C<$self> is created with L</create_copy>.
474 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
475 created. Its attributes are copied from C<%attributes> save for the
476 version number which is taken from step 1.
478 =item 4. The version instance created in step 3 is referenced to the
479 the copy from step 2 via C<requirement_spec_id> and to the working
480 copy for which the version was created via C<working_copy_id>.
484 All this is done within a transaction.
486 In case of success a two-element list is returned consisting of the
487 copy & version objects created in steps 3 and 2 respectively. In case
488 of a failure an empty list will be returned.
490 =item C<displayable_name>
492 Returns a human-readable name for this instance consisting of the type
495 =item C<highest_version>
497 Given a working copy C<$self> this function returns the versioned copy
498 of C<$self> with the highest version number. If such a version exist
499 its instance is returned. Otherwise C<undef> is returned.
501 This can be used for calculating the difference between the working
502 copy and the last version created for it.
504 =item C<invalidate_version>
506 Prerequisites: C<$self> must be a working copy (see the overview),
507 not a versioned copy.
509 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
510 table containing C<$self-E<gt>id> to C<undef>.
512 =item C<is_working_copy>
514 Returns trueish if C<$self> is a working copy and not a versioned
515 copy. The condition for this is that C<working_copy_id> is C<undef>.
517 =item C<next_version_number>
519 Calculates and returns the next version number for this requirement
520 spec. Version numbers start at 1 and are incremented by one for each
521 version created for it, no matter whether or not it has been reverted
522 to a previous version since. It boils down to this pseudo-code:
524 if (has_never_had_a_version)
527 return max(version_number for all versions for this requirement spec) + 1
531 An alias for L</sections_sorted>.
533 =item C<sections_sorted>
535 Returns an array reference of requirement spec items that do not have
536 a parent -- meaning that are sections.
538 This is not a writer. Use the C<items> relationship for that.
540 =item C<text_blocks_sorted %params>
542 Returns an array reference of text blocks sorted by their positional
543 column in ascending order. If the C<output_position> parameter is
544 given then only the text blocks belonging to that C<output_position>
547 =item C<parts_sorted>
549 Returns an array reference of additional parts sorted by their
550 positional column in ascending order.
554 Validate values before saving. Returns list or human-readable error
557 =item C<versioned_copies_sorted %params>
559 Returns an array reference of versioned copies sorted by their version
560 number in ascending order. If the C<max_version_number> parameter is
561 given then only the versioned copies whose version number is less than
562 or equal to C<max_version_number> are returned.
572 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>