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 for (qw(previous_section_number previous_fb_number previous_picture_number)) {
64 $self->$_(0) if !defined $self->$_;
73 croak "Not a writer" if scalar(@_) > 1;
75 return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
78 sub text_blocks_sorted {
79 my ($self, %params) = _hashify(1, @_);
81 my @text_blocks = @{ $self->text_blocks };
82 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
83 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
89 my ($self, @rest) = @_;
91 croak "This sub is not a writer" if @rest;
93 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
96 sub sections { §ions_sorted; }
99 my ($self, %params) = _hashify(1, @_);
100 my $by = $params{by} || 'itime';
102 return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
105 sub displayable_name {
108 return sprintf('%s: "%s"', $self->type->description, $self->title);
111 sub versioned_copies_sorted {
112 my ($self, %params) = _hashify(1, @_);
114 my @copies = @{ $self->versioned_copies };
115 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
116 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
122 my ($self, %params) = @_;
124 return $self->_create_copy(%params) if $self->db->in_transaction;
127 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
128 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
136 my ($self, %params) = @_;
138 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
139 $copy->copy_from($self, %params);
145 my ($self, $params, %attributes) = @_;
147 my $source = $params->{source};
149 croak "Missing parameter 'source'" unless $source;
152 if (!$params->{paste_template}) {
153 $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)),
157 my %paste_template_result;
159 # Clone text blocks and pictures.
160 my $clone_picture = sub {
162 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($picture);
163 $cloned->position(undef);
167 my $clone_text_block = sub {
168 my ($text_block) = @_;
169 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
170 $cloned->position(undef);
171 $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
175 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
177 if (!$params->{paste_template}) {
178 $self->text_blocks($paste_template_result{text_blocks});
180 $self->add_text_blocks($paste_template_result{text_blocks});
183 # Save new object -- we need its ID for the items.
192 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
193 $cloned->requirement_spec_id($self->id);
194 $cloned->position(undef);
195 $cloned->fb_number(undef) if $params->{paste_template};
196 $cloned->children(map { $clone_item->($_) } @{ $item->children });
198 $id_to_clone{ $item->id } = $cloned;
203 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
205 if (!$params->{paste_template}) {
206 $self->items($paste_template_result{sections});
208 $self->add_items($paste_template_result{sections});
211 # Save the items -- need to do that before setting dependencies.
215 foreach my $item (@{ $source->items }) {
216 next unless @{ $item->dependencies };
217 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
220 $self->update_attributes(%attributes) unless $params->{paste_template};
222 return %paste_template_result;
226 my ($self, $source, %attributes) = @_;
228 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
232 my ($self, $template) = @_;
234 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
237 sub highest_version {
240 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
243 sub is_working_copy {
246 return !$self->working_copy_id;
249 sub next_version_number {
252 return 1 if !$self->id;
254 my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
255 SELECT MAX(v.version_number)
256 FROM requirement_spec_versions v
257 WHERE v.requirement_spec_id IN (
259 FROM requirement_specs rs
261 OR (rs.working_copy_id = ?)
265 return ($max_number // 0) + 1;
269 my ($self, %attributes) = @_;
271 croak "Cannot work on a versioned copy" if $self->working_copy_id;
273 my ($copy, $version);
274 my $ok = $self->db->with_transaction(sub {
275 delete $attributes{version_number};
277 SL::DB::Manager::RequirementSpecVersion->update_all(
278 set => [ working_copy_id => undef ],
279 where => [ requirement_spec_id => $self->id ],
282 $copy = $self->create_copy(working_copy_id => $self->id);
283 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
288 return $ok ? ($copy, $version) : ();
291 sub invalidate_version {
292 my ($self, %params) = @_;
294 croak "Cannot work on a versioned copy" if $self->working_copy_id;
296 return if !$self->id;
298 SL::DB::Manager::RequirementSpecVersion->update_all(
299 set => [ working_copy_id => undef ],
300 where => [ working_copy_id => $self->id ],
313 SL::DB::RequirementSpec - RDBO model for requirement specs
317 The database structure behind requirement specs is a bit involved. The
318 important thing is how working copy/versions are handled.
320 The table contains three important columns: C<id> (which is also the
321 primary key) and C<working_copy_id>. C<working_copy_id> is a
322 self-referencing column: it can be C<NULL>, but if it isn't then it
323 contains another requirement spec C<id>.
325 Versions are represented similarly. The C<requirement_spec_versions>
326 table has three important columns: C<id> (the primary key),
327 C<requirement_spec_id> (references C<requirement_specs.id> and must
328 not be C<NULL>) and C<working_copy_id> (references
329 C<requirement_specs.id> as well but can be
330 C<NULL>). C<working_copy_id> points to the working copy if and only if
331 the working copy is currently equal to a versioned copy.
333 The design is as follows:
337 =item * The user is always working on a working copy. The working copy
338 is identified in the database by having C<working_copy_id> set to
341 =item * All other entries in this table are referred to as I<versioned
342 copies>. A versioned copy is a copy of a working frozen at the moment
343 in time it was created. Each versioned copy refers back to the working
344 copy it belongs to: each has its C<working_copy_id> set.
346 =item * Each versioned copy must be referenced from an entry in the
347 table C<requirement_spec_versions> via
348 C<requirement_spec_id>.
350 =item * Directly after creating a versioned copy even the working copy
351 itself is referenced from a version via that table's
352 C<working_copy_id> column. However, any modification that will be
353 visible to the customer (text, positioning etc but not internal things
354 like time/cost estimation changes) will cause the version to be
355 disassociated from the working copy. This is achieved via before save
360 =head1 DATABASE TRIGGERS AND CHECKS
362 Several database triggers and consistency checks exist that manage
363 requirement specs, their items and their dependencies. These are
364 described here instead of in the individual files for the other RDBO
369 When you delete a requirement spec all of its dependencies (items,
370 text blocks, versions etc.) are deleted by triggers.
372 When you delete an item (either a section or a (sub-)function block)
373 all of its children will be deleted as well. This will trigger the
374 same trigger resulting in a recursive deletion with the bottom-most
375 items being deleted first. Their item dependencies are deleted as
380 Whenever you update a requirement spec item a trigger will fire that
381 will update the parent's C<time_estimation> column. This also happens
382 when an item is deleted or updated.
384 =head2 CONSISTENCY CHECKS
386 Several consistency checks are applied to requirement spec items:
390 =item * Column C<requirement_spec_item.item_type> can only contain one of
391 the values C<section>, C<function-block> or C<sub-function-block>.
393 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
394 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
403 =item C<copy_from $source, %attributes>
405 Copies everything (basic attributes like type/title/customer, items,
406 text blocks, time/cost estimation) save for the versions from the
407 other requirement spec object C<$source> into C<$self> and saves
408 it. This is done within a transaction.
410 C<%attributes> are attributes that are assigned to C<$self> after all
411 the basic attributes from C<$source> have been assigned.
413 This function can be used for resetting a working copy to a specific
416 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
417 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
419 $requirement_spec->copy_from($versioned_copy);
420 $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
424 Creates and returns a copy of C<$self>. The copy is already
425 saved. Creating the copy happens within a transaction.
427 =item C<create_version %attributes>
429 Prerequisites: C<$self> must be a working copy (see the overview),
430 not a versioned copy.
432 This function creates a new version for C<$self>. This involves
437 =item 1. The next version number is calculated using
438 L</next_version_number>.
440 =item 2. A copy of C<$self> is created with L</create_copy>.
442 =item 3. An instance of L<SL::DB::RequirementSpecVersion> is
443 created. Its attributes are copied from C<%attributes> save for the
444 version number which is taken from step 1.
446 =item 4. The version instance created in step 3 is referenced to the
447 the copy from step 2 via C<requirement_spec_id> and to the working
448 copy for which the version was created via C<working_copy_id>.
452 All this is done within a transaction.
454 In case of success a two-element list is returned consisting of the
455 copy & version objects created in steps 3 and 2 respectively. In case
456 of a failure an empty list will be returned.
458 =item C<displayable_name>
460 Returns a human-readable name for this instance consisting of the type
463 =item C<highest_version>
465 Given a working copy C<$self> this function returns the versioned copy
466 of C<$self> with the highest version number. If such a version exist
467 its instance is returned. Otherwise C<undef> is returned.
469 This can be used for calculating the difference between the working
470 copy and the last version created for it.
472 =item C<invalidate_version>
474 Prerequisites: C<$self> must be a working copy (see the overview),
475 not a versioned copy.
477 Sets any C<working_copy_id> field in the C<requirement_spec_versions>
478 table containing C<$self-E<gt>id> to C<undef>.
480 =item C<is_working_copy>
482 Returns trueish if C<$self> is a working copy and not a versioned
483 copy. The condition for this is that C<working_copy_id> is C<undef>.
485 =item C<next_version_number>
487 Calculates and returns the next version number for this requirement
488 spec. Version numbers start at 1 and are incremented by one for each
489 version created for it, no matter whether or not it has been reverted
490 to a previous version since. It boils down to this pseudo-code:
492 if (has_never_had_a_version)
495 return max(version_number for all versions for this requirement spec) + 1
499 An alias for L</sections_sorted>.
501 =item C<sections_sorted>
503 Returns an array reference of requirement spec items that do not have
504 a parent -- meaning that are sections.
506 This is not a writer. Use the C<items> relationship for that.
508 =item C<text_blocks_sorted %params>
510 Returns an array reference of text blocks sorted by their positional
511 column in ascending order. If the C<output_position> parameter is
512 given then only the text blocks belonging to that C<output_position>
517 Validate values before saving. Returns list or human-readable error
520 =item C<versioned_copies_sorted %params>
522 Returns an array reference of versioned copies sorted by their version
523 number in ascending order. If the C<max_version_number> parameter is
524 given then only the versioned copies whose version number is less than
525 or equal to C<max_version_number> are returned.
535 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>