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::RequirementSpecVersion',
33 column_map => { id => 'requirement_spec_id' },
35 working_copy_versions => {
36 type => 'one to many',
37 class => 'SL::DB::RequirementSpecVersion',
38 column_map => { id => 'working_copy_id' },
41 type => 'one to many',
42 class => 'SL::DB::RequirementSpecOrder',
43 column_map => { id => 'requirement_spec_id' },
47 __PACKAGE__->meta->initialize;
49 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
55 push @errors, t8('The title is missing.') if !$self->title;
60 sub _before_save_initialize_not_null_columns {
63 $self->previous_section_number(0) if !defined $self->previous_section_number;
64 $self->previous_fb_number(0) if !defined $self->previous_fb_number;
72 croak "Not a writer" if scalar(@_) > 1;
74 return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
77 sub text_blocks_sorted {
78 my ($self, %params) = _hashify(1, @_);
80 my @text_blocks = @{ $self->text_blocks };
81 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
82 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
88 my ($self, @rest) = @_;
90 croak "This sub is not a writer" if @rest;
92 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
95 sub sections { §ions_sorted; }
98 my ($self, %params) = _hashify(1, @_);
99 my $by = $params{by} || 'itime';
101 return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
104 sub displayable_name {
107 return sprintf('%s: "%s"', $self->type->description, $self->title);
110 sub versioned_copies_sorted {
111 my ($self, %params) = _hashify(1, @_);
113 my @copies = @{ $self->versioned_copies };
114 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
115 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
121 my ($self, %params) = @_;
123 return $self->_create_copy(%params) if $self->db->in_transaction;
126 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
127 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
135 my ($self, %params) = @_;
137 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
138 $copy->copy_from($self, %params);
144 my ($self, $params, %attributes) = @_;
146 my $source = $params->{source};
148 croak "Missing parameter 'source'" unless $source;
151 if (!$params->{paste_template}) {
152 $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)),
156 my %paste_template_result;
159 my $clone_text_block = sub {
160 my ($text_block) = @_;
161 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
162 $cloned->position(undef);
166 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
168 if (!$params->{paste_template}) {
169 $self->text_blocks($paste_template_result{text_blocks});
171 $self->add_text_blocks($paste_template_result{text_blocks});
174 # Save new object -- we need its ID for the items.
183 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
184 $cloned->requirement_spec_id($self->id);
185 $cloned->position(undef);
186 $cloned->fb_number(undef) if $params->{paste_template};
187 $cloned->children(map { $clone_item->($_) } @{ $item->children });
189 $id_to_clone{ $item->id } = $cloned;
194 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
196 if (!$params->{paste_template}) {
197 $self->items($paste_template_result{sections});
199 $self->add_items($paste_template_result{sections});
202 # Save the items -- need to do that before setting dependencies.
206 foreach my $item (@{ $source->items }) {
207 next unless @{ $item->dependencies };
208 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
211 $self->update_attributes(%attributes) unless $params->{paste_template};
213 return %paste_template_result;
217 my ($self, $source, %attributes) = @_;
219 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
223 my ($self, $template) = @_;
225 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
228 sub highest_version {
231 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
234 sub is_working_copy {
237 return !$self->working_copy_id;
240 sub next_version_number {
243 return 1 if !$self->id;
245 my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
246 SELECT MAX(v.version_number)
247 FROM requirement_spec_versions v
248 WHERE v.requirement_spec_id IN (
250 FROM requirement_specs rs
252 OR (rs.working_copy_id = ?)
256 return ($max_number // 0) + 1;
260 my ($self, %attributes) = @_;
262 croak "Cannot work on a versioned copy" if $self->working_copy_id;
264 my ($copy, $version);
265 my $ok = $self->db->with_transaction(sub {
266 delete $attributes{version_number};
268 SL::DB::Manager::RequirementSpecVersion->update_all(
269 set => [ working_copy_id => undef ],
270 where => [ requirement_spec_id => $self->id ],
273 $copy = $self->create_copy(working_copy_id => $self->id);
274 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
279 return $ok ? ($copy, $version) : ();
282 sub invalidate_version {
283 my ($self, %params) = @_;
285 croak "Cannot work on a versioned copy" if $self->working_copy_id;
287 return if !$self->id;
289 SL::DB::Manager::RequirementSpecVersion->update_all(
290 set => [ working_copy_id => undef ],
291 where => [ working_copy_id => $self->id ],
304 SL::DB::RequirementSpec - RDBO model for requirement specs
308 The database structure behind requirement specs is a bit involved. The
309 important thing is how working copy/versions are handled.
311 The table contains three important columns: C<id> (which is also the
312 primary key) and C<working_copy_id>. C<working_copy_id> is a
313 self-referencing column: it can be C<NULL>, but if it isn't then it
314 contains another requirement spec C<id>.
316 Versions are represented similarly. The C<requirement_spec_versions>
317 table has three important columns: C<id> (the primary key),
318 C<requirement_spec_id> (references C<requirement_specs.id> and must
319 not be C<NULL>) and C<working_copy_id> (references
320 C<requirement_specs.id> as well but can be
321 C<NULL>). C<working_copy_id> points to the working copy if and only if
322 the working copy is currently equal to a versioned copy.
324 The design is as follows:
328 =item * The user is always working on a working copy. The working copy
329 is identified in the database by having C<working_copy_id> set to
332 =item * All other entries in this table are referred to as I<versioned
333 copies>. A versioned copy is a copy of a working frozen at the moment
334 in time it was created. Each versioned copy refers back to the working
335 copy it belongs to: each has its C<working_copy_id> set.
337 =item * Each versioned copy must be referenced from an entry in the
338 table C<requirement_spec_versions> via
339 C<requirement_spec_id>.
341 =item * Directly after creating a versioned copy even the working copy
342 itself is referenced from a version via that table's
343 C<working_copy_id> column. However, any modification that will be
344 visible to the customer (text, positioning etc but not internal things
345 like time/cost estimation changes) will cause the version to be
346 disassociated from the working copy. This is achieved via before save
351 =head1 DATABASE TRIGGERS AND CHECKS
353 Several database triggers and consistency checks exist that manage
354 requirement specs, their items and their dependencies. These are
355 described here instead of in the individual files for the other RDBO
360 When you delete a requirement spec all of its dependencies (items,
361 text blocks, versions etc.) are deleted by triggers.
363 When you delete an item (either a section or a (sub-)function block)
364 all of its children will be deleted as well. This will trigger the
365 same trigger resulting in a recursive deletion with the bottom-most
366 items being deleted first. Their item dependencies are deleted as
371 Whenever you update a requirement spec item a trigger will fire that
372 will update the parent's C<time_estimation> column. This also happens
373 when an item is deleted or updated.
375 =head2 CONSISTENCY CHECKS
377 Several consistency checks are applied to requirement spec items:
381 =item * Column C<requirement_spec_item.item_type> can only contain one of
382 the values C<section>, C<function-block> or C<sub-function-block>.
384 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
385 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
394 =item C<copy_from $source, %attributes>
396 Copies everything (basic attributes like type/title/customer, items,
397 text blocks, time/cost estimation) save for the versions from the
398 other requirement spec object C<$source> into C<$self> and saves
399 it. This is done within a transaction.
401 C<%attributes> are attributes that are assigned to C<$self> after all
402 the basic attributes from C<$source> have been assigned.
404 This function can be used for resetting a working copy to a specific
407 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
408 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
410 $requirement_spec->copy_from($versioned_copy);
411 $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
415 Creates and returns a copy of C<$self>. The copy is already
416 saved. Creating the copy happens within a transaction.
418 =item C<create_version %attributes>
420 Prerequisites: C<$self> must be a working copy (see the overview),
421 not a versioned copy.
423 This function creates a new version for C<$self>. This involves
428 =item 1. The next version number is calculated using
429 L</next_version_number>.
431 =item 2. A copy of C<$self> is created with L</create_copy>.
433 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
434 created. Its attributes are copied from C<%attributes> save for the
435 version number which is taken from step 1.
437 =item 4. The version instance created in step 3 is referenced to the
438 the copy from step 2 via C<requirement_spec_id> and to the working
439 copy for which the version was created via C<working_copy_id>.
443 All this is done within a transaction.
445 In case of success a two-element list is returned consisting of the
446 copy & version objects created in steps 3 and 2 respectively. In case
447 of a failure an empty list will be returned.
449 =item C<displayable_name>
451 Returns a human-readable name for this instance consisting of the type
454 =item C<highest_version>
456 Given a working copy C<$self> this function returns the versioned copy
457 of C<$self> with the highest version number. If such a version exist
458 its instance is returned. Otherwise C<undef> is returned.
460 This can be used for calculating the difference between the working
461 copy and the last version created for it.
463 =item C<invalidate_version>
465 Prerequisites: C<$self> must be a working copy (see the overview),
466 not a versioned copy.
468 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
469 table containing C<$self-E<gt>id> to C<undef>.
471 =item C<is_working_copy>
473 Returns trueish if C<$self> is a working copy and not a versioned
474 copy. The condition for this is that C<working_copy_id> is C<undef>.
476 =item C<next_version_number>
478 Calculates and returns the next version number for this requirement
479 spec. Version numbers start at 1 and are incremented by one for each
480 version created for it, no matter whether or not it has been reverted
481 to a previous version since. It boils down to this pseudo-code:
483 if (has_never_had_a_version)
486 return max(version_number for all versions for this requirement spec) + 1
490 An alias for L</sections_sorted>.
492 =item C<sections_sorted>
494 Returns an array reference of requirement spec items that do not have
495 a parent -- meaning that are sections.
497 This is not a writer. Use the C<items> relationship for that.
499 =item C<text_blocks_sorted %params>
501 Returns an array reference of text blocks sorted by their positional
502 column in ascending order. If the C<output_position> parameter is
503 given then only the text blocks belonging to that C<output_position>
508 Validate values before saving. Returns list or human-readable error
511 =item C<versioned_copies_sorted %params>
513 Returns an array reference of versioned copies sorted by their version
514 number in ascending order. If the C<max_version_number> parameter is
515 given then only the versioned copies whose version number is less than
516 or equal to C<max_version_number> are returned.
526 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>