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' },
48 __PACKAGE__->meta->initialize;
50 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
56 push @errors, t8('The title is missing.') if !$self->title;
61 sub _before_save_initialize_not_null_columns {
64 for (qw(previous_section_number previous_fb_number previous_picture_number)) {
65 $self->$_(0) if !defined $self->$_;
74 croak "Not a writer" if scalar(@_) > 1;
76 return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
79 sub text_blocks_sorted {
80 my ($self, %params) = _hashify(1, @_);
82 my @text_blocks = @{ $self->text_blocks };
83 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
84 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
90 my ($self, @rest) = @_;
92 croak "This sub is not a writer" if @rest;
94 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
97 sub sections { §ions_sorted; }
100 my ($self, %params) = _hashify(1, @_);
101 my $by = $params{by} || 'itime';
103 return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
106 sub displayable_name {
109 return sprintf('%s: "%s"', $self->type->description, $self->title);
112 sub versioned_copies_sorted {
113 my ($self, %params) = _hashify(1, @_);
115 my @copies = @{ $self->versioned_copies };
116 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
117 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
123 my ($self, %params) = @_;
125 return $self->_create_copy(%params) if $self->db->in_transaction;
128 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
129 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
137 my ($self, %params) = @_;
139 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
140 $copy->copy_from($self, %params);
146 my ($self, $params, %attributes) = @_;
148 my $source = $params->{source};
150 croak "Missing parameter 'source'" unless $source;
153 if (!$params->{paste_template}) {
154 $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)),
158 my %paste_template_result;
160 # Clone text blocks and pictures.
161 my $clone_picture = sub {
163 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($picture);
164 $cloned->position(undef);
168 my $clone_text_block = sub {
169 my ($text_block) = @_;
170 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
171 $cloned->position(undef);
172 $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
176 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
178 if (!$params->{paste_template}) {
179 $self->text_blocks($paste_template_result{text_blocks});
181 $self->add_text_blocks($paste_template_result{text_blocks});
184 # Save new object -- we need its ID for the items.
193 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
194 $cloned->requirement_spec_id($self->id);
195 $cloned->position(undef);
196 $cloned->fb_number(undef) if $params->{paste_template};
197 $cloned->children(map { $clone_item->($_) } @{ $item->children });
199 $id_to_clone{ $item->id } = $cloned;
204 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
206 if (!$params->{paste_template}) {
207 $self->items($paste_template_result{sections});
209 $self->add_items($paste_template_result{sections});
212 # Save the items -- need to do that before setting dependencies.
216 foreach my $item (@{ $source->items }) {
217 next unless @{ $item->dependencies };
218 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
221 $self->update_attributes(%attributes) unless $params->{paste_template};
223 return %paste_template_result;
227 my ($self, $source, %attributes) = @_;
229 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
233 my ($self, $template) = @_;
235 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
238 sub highest_version {
241 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
244 sub is_working_copy {
247 return !$self->working_copy_id;
250 sub next_version_number {
253 return 1 if !$self->id;
255 my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
256 SELECT MAX(v.version_number)
257 FROM requirement_spec_versions v
258 WHERE v.requirement_spec_id IN (
260 FROM requirement_specs rs
262 OR (rs.working_copy_id = ?)
266 return ($max_number // 0) + 1;
270 my ($self, %attributes) = @_;
272 croak "Cannot work on a versioned copy" if $self->working_copy_id;
274 my ($copy, $version);
275 my $ok = $self->db->with_transaction(sub {
276 delete $attributes{version_number};
278 SL::DB::Manager::RequirementSpecVersion->update_all(
279 set => [ working_copy_id => undef ],
280 where => [ requirement_spec_id => $self->id ],
283 $copy = $self->create_copy(working_copy_id => $self->id);
284 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
289 return $ok ? ($copy, $version) : ();
292 sub invalidate_version {
293 my ($self, %params) = @_;
295 croak "Cannot work on a versioned copy" if $self->working_copy_id;
297 return if !$self->id;
299 SL::DB::Manager::RequirementSpecVersion->update_all(
300 set => [ working_copy_id => undef ],
301 where => [ working_copy_id => $self->id ],
314 SL::DB::RequirementSpec - RDBO model for requirement specs
318 The database structure behind requirement specs is a bit involved. The
319 important thing is how working copy/versions are handled.
321 The table contains three important columns: C<id> (which is also the
322 primary key) and C<working_copy_id>. C<working_copy_id> is a
323 self-referencing column: it can be C<NULL>, but if it isn't then it
324 contains another requirement spec C<id>.
326 Versions are represented similarly. The C<requirement_spec_versions>
327 table has three important columns: C<id> (the primary key),
328 C<requirement_spec_id> (references C<requirement_specs.id> and must
329 not be C<NULL>) and C<working_copy_id> (references
330 C<requirement_specs.id> as well but can be
331 C<NULL>). C<working_copy_id> points to the working copy if and only if
332 the working copy is currently equal to a versioned copy.
334 The design is as follows:
338 =item * The user is always working on a working copy. The working copy
339 is identified in the database by having C<working_copy_id> set to
342 =item * All other entries in this table are referred to as I<versioned
343 copies>. A versioned copy is a copy of a working frozen at the moment
344 in time it was created. Each versioned copy refers back to the working
345 copy it belongs to: each has its C<working_copy_id> set.
347 =item * Each versioned copy must be referenced from an entry in the
348 table C<requirement_spec_versions> via
349 C<requirement_spec_id>.
351 =item * Directly after creating a versioned copy even the working copy
352 itself is referenced from a version via that table's
353 C<working_copy_id> column. However, any modification that will be
354 visible to the customer (text, positioning etc but not internal things
355 like time/cost estimation changes) will cause the version to be
356 disassociated from the working copy. This is achieved via before save
361 =head1 DATABASE TRIGGERS AND CHECKS
363 Several database triggers and consistency checks exist that manage
364 requirement specs, their items and their dependencies. These are
365 described here instead of in the individual files for the other RDBO
370 When you delete a requirement spec all of its dependencies (items,
371 text blocks, versions etc.) are deleted by triggers.
373 When you delete an item (either a section or a (sub-)function block)
374 all of its children will be deleted as well. This will trigger the
375 same trigger resulting in a recursive deletion with the bottom-most
376 items being deleted first. Their item dependencies are deleted as
381 Whenever you update a requirement spec item a trigger will fire that
382 will update the parent's C<time_estimation> column. This also happens
383 when an item is deleted or updated.
385 =head2 CONSISTENCY CHECKS
387 Several consistency checks are applied to requirement spec items:
391 =item * Column C<requirement_spec_item.item_type> can only contain one of
392 the values C<section>, C<function-block> or C<sub-function-block>.
394 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
395 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
404 =item C<copy_from $source, %attributes>
406 Copies everything (basic attributes like type/title/customer, items,
407 text blocks, time/cost estimation) save for the versions from the
408 other requirement spec object C<$source> into C<$self> and saves
409 it. This is done within a transaction.
411 C<%attributes> are attributes that are assigned to C<$self> after all
412 the basic attributes from C<$source> have been assigned.
414 This function can be used for resetting a working copy to a specific
417 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
418 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
420 $requirement_spec->copy_from($versioned_copy);
421 $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
425 Creates and returns a copy of C<$self>. The copy is already
426 saved. Creating the copy happens within a transaction.
428 =item C<create_version %attributes>
430 Prerequisites: C<$self> must be a working copy (see the overview),
431 not a versioned copy.
433 This function creates a new version for C<$self>. This involves
438 =item 1. The next version number is calculated using
439 L</next_version_number>.
441 =item 2. A copy of C<$self> is created with L</create_copy>.
443 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
444 created. Its attributes are copied from C<%attributes> save for the
445 version number which is taken from step 1.
447 =item 4. The version instance created in step 3 is referenced to the
448 the copy from step 2 via C<requirement_spec_id> and to the working
449 copy for which the version was created via C<working_copy_id>.
453 All this is done within a transaction.
455 In case of success a two-element list is returned consisting of the
456 copy & version objects created in steps 3 and 2 respectively. In case
457 of a failure an empty list will be returned.
459 =item C<displayable_name>
461 Returns a human-readable name for this instance consisting of the type
464 =item C<highest_version>
466 Given a working copy C<$self> this function returns the versioned copy
467 of C<$self> with the highest version number. If such a version exist
468 its instance is returned. Otherwise C<undef> is returned.
470 This can be used for calculating the difference between the working
471 copy and the last version created for it.
473 =item C<invalidate_version>
475 Prerequisites: C<$self> must be a working copy (see the overview),
476 not a versioned copy.
478 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
479 table containing C<$self-E<gt>id> to C<undef>.
481 =item C<is_working_copy>
483 Returns trueish if C<$self> is a working copy and not a versioned
484 copy. The condition for this is that C<working_copy_id> is C<undef>.
486 =item C<next_version_number>
488 Calculates and returns the next version number for this requirement
489 spec. Version numbers start at 1 and are incremented by one for each
490 version created for it, no matter whether or not it has been reverted
491 to a previous version since. It boils down to this pseudo-code:
493 if (has_never_had_a_version)
496 return max(version_number for all versions for this requirement spec) + 1
500 An alias for L</sections_sorted>.
502 =item C<sections_sorted>
504 Returns an array reference of requirement spec items that do not have
505 a parent -- meaning that are sections.
507 This is not a writer. Use the C<items> relationship for that.
509 =item C<text_blocks_sorted %params>
511 Returns an array reference of text blocks sorted by their positional
512 column in ascending order. If the C<output_position> parameter is
513 given then only the text blocks belonging to that C<output_position>
518 Validate values before saving. Returns list or human-readable error
521 =item C<versioned_copies_sorted %params>
523 Returns an array reference of versioned copies sorted by their version
524 number in ascending order. If the C<max_version_number> parameter is
525 given then only the versioned copies whose version number is less than
526 or equal to C<max_version_number> are returned.
536 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>