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' },
32 __PACKAGE__->meta->initialize;
34 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
40 push @errors, t8('The title is missing.') if !$self->title;
45 sub _before_save_initialize_not_null_columns {
48 $self->previous_section_number(0) if !defined $self->previous_section_number;
49 $self->previous_fb_number(0) if !defined $self->previous_fb_number;
54 sub text_blocks_sorted {
55 my ($self, %params) = _hashify(1, @_);
57 my @text_blocks = @{ $self->text_blocks };
58 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
59 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
65 my ($self, @rest) = @_;
67 croak "This sub is not a writer" if @rest;
69 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
72 sub sections { §ions_sorted; }
74 sub displayable_name {
77 return sprintf('%s: "%s"', $self->type->description, $self->title);
80 sub versioned_copies_sorted {
81 my ($self, %params) = _hashify(1, @_);
83 my @copies = @{ $self->versioned_copies };
84 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
85 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
91 my ($self, %params) = @_;
93 return $self->_create_copy(%params) if $self->db->in_transaction;
96 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
97 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
105 my ($self, %params) = @_;
107 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
108 $copy->copy_from($self, %params);
114 my ($self, $params, %attributes) = @_;
116 my $source = $params->{source};
118 croak "Missing parameter 'source'" unless $source;
121 if (!$params->{paste_template}) {
122 $self->assign_attributes(map({ ($_ => $source->$_) } qw(type_id status_id customer_id project_id title hourly_rate net_sum previous_section_number previous_fb_number is_template)),
126 my %paste_template_result;
129 my $clone_text_block = sub {
130 my ($text_block) = @_;
131 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
132 $cloned->position(undef);
136 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
138 if (!$params->{paste_template}) {
139 $self->text_blocks($paste_template_result{text_blocks});
141 $self->add_text_blocks($paste_template_result{text_blocks});
144 # Save new object -- we need its ID for the items.
153 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
154 $cloned->requirement_spec_id($self->id);
155 $cloned->position(undef);
156 $cloned->children(map { $clone_item->($_) } @{ $item->children });
158 $id_to_clone{ $item->id } = $cloned;
163 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
165 if (!$params->{paste_template}) {
166 $self->items($paste_template_result{sections});
168 $self->add_items($paste_template_result{sections});
171 # Save the items -- need to do that before setting dependencies.
175 foreach my $item (@{ $source->items }) {
176 next unless @{ $item->dependencies };
177 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
180 $self->update_attributes(%attributes) unless $params->{paste_template};
182 return %paste_template_result;
186 my ($self, $source, %attributes) = @_;
188 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
192 my ($self, $template) = @_;
194 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
197 sub highest_version {
200 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
203 sub is_working_copy {
206 return !$self->working_copy_id;
209 sub next_version_number {
212 return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
216 my ($self, %attributes) = @_;
218 croak "Cannot work on a versioned copy" if $self->working_copy_id;
220 my ($copy, $version);
221 my $ok = $self->db->with_transaction(sub {
222 delete $attributes{version_number};
224 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
225 $copy = $self->create_copy;
226 $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
227 $self->update_attributes(version_id => $version->id);
232 return $ok ? ($copy, $version) : ();
235 sub invalidate_version {
236 my ($self, %params) = @_;
238 croak "Cannot work on a versioned copy" if $self->working_copy_id;
240 return if !$self->id || !$self->version_id;
241 $self->update_attributes(version_id => undef);
253 SL::DB::RequirementSpec - RDBO model for requirement specs
257 The database structure behind requirement specs is a bit involved. The
258 important thing is how working copy/versions are handled.
260 The table contains three important columns: C<id> (which is also the
261 primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
262 is a self-referencing column: it can be C<NULL>, but if it isn't then
263 it contains another requirement spec C<id>. C<version_id> on the other
264 hand references the table C<requirement_spec_versions>.
266 The design is as follows:
270 =item * The user is always working on a working copy. The working copy
271 is identified in the database by having C<working_copy_id> set to
274 =item * All other entries in this table are referred to as I<versioned
275 copies>. A versioned copy is a copy of a working frozen at the moment
276 in time it was created. Each versioned copy refers back to the working
277 copy it belongs to: each has its C<working_copy_id> set.
279 =item * Each versioned copy must reference an entry in the table
280 C<requirement_spec_versions>. Meaning: for each versioned copy
281 C<version_id> must not be C<NULL>.
283 =item * Directly after creating a versioned copy even the working copy
284 itself points to a certain version via its C<version_id> column: to
285 the same version that the versioned copy just created points
286 to. However, any modification that will be visible to the customer
287 (text, positioning etc but not internal things like time/cost
288 estimation changes) will cause the working copy to be set to 'no
289 version' again. This is achieved via before save hooks in Perl.
293 =head1 DATABASE TRIGGERS AND CHECKS
295 Several database triggers and consistency checks exist that manage
296 requirement specs, their items and their dependencies. These are
297 described here instead of in the individual files for the other RDBO
302 When you delete a requirement spec all of its dependencies (items,
303 text blocks, versions etc.) are deleted by triggers.
305 When you delete an item (either a section or a (sub-)function block)
306 all of its children will be deleted as well. This will trigger the
307 same trigger resulting in a recursive deletion with the bottom-most
308 items being deleted first. Their item dependencies are deleted as
313 Whenever you update a requirement spec item a trigger will fire that
314 will update the parent's C<time_estimation> column. This also happens
315 when an item is deleted or updated.
317 =head2 CONSISTENCY CHECKS
319 Several consistency checks are applied to requirement spec items:
323 =item * Column C<requirement_spec_item.item_type> can only contain one of
324 the values C<section>, C<function-block> or C<sub-function-block>.
326 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
327 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
336 =item C<copy_from $source, %attributes>
338 Copies everything (basic attributes like type/title/customer, items,
339 text blocks, time/cost estimation) save for the versions from the
340 other requirement spec object C<$source> into C<$self> and saves
341 it. This is done within a transaction.
343 C<%attributes> are attributes that are assigned to C<$self> after all
344 the basic attributes from C<$source> have been assigned.
346 This function can be used for resetting a working copy to a specific
349 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
350 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
352 $requirement_spec->copy_from(
354 version_id => $versioned_copy->version_id,
359 Creates and returns a copy of C<$self>. The copy is already
360 saved. Creating the copy happens within a transaction.
362 =item C<create_version %attributes>
364 Prerequisites: C<$self> must be a working copy (see the overview),
365 not a versioned copy.
367 This function creates a new version for C<$self>. This involves
372 =item 1. The next version number is calculated using
373 L</next_version_number>.
375 =item 2. An instance of L<SL::DB::RequirementSpecVersion> is
376 created. Its attributes are copied from C<%attributes> save for the
377 version number which is taken from step 1.
379 =item 3. A copy of C<$self> is created with L</create_copy>.
381 =item 4. The version instance created in step is assigned to the copy
384 =item 5. The C<version_id> in C<$self> is set to the copy's ID from
389 All this is done within a transaction.
391 In case of success a two-element list is returned consisting of the
392 copy & version objects created in steps 3 and 2 respectively. In case
393 of a failure an empty list will be returned.
395 =item C<displayable_name>
397 Returns a human-readable name for this instance consisting of the type
400 =item C<highest_version>
402 Given a working copy C<$self> this function returns the versioned copy
403 of C<$self> with the highest version number. If such a version exist
404 its instance is returned. Otherwise C<undef> is returned.
406 This can be used for calculating the difference between the working
407 copy and the last version created for it.
409 =item C<invalidate_version>
411 Prerequisites: C<$self> must be a working copy (see the overview),
412 not a versioned copy.
414 Sets the C<version_id> field to C<undef> and saves C<$self>.
416 =item C<is_working_copy>
418 Returns trueish if C<$self> is a working copy and not a versioned
419 copy. The condition for this is that C<working_copy_id> is C<undef>.
421 =item C<next_version_number>
423 Calculates and returns the next version number for this requirement
424 spec. Version numbers start at 1 and are incremented by one for each
425 version created for it, no matter whether or not it has been reverted
426 to a previous version since. It boils down to this pseudo-code:
428 if (has_never_had_a_version)
431 return max(version_number for all versions for this requirement spec) + 1
435 An alias for L</sections_sorted>.
437 =item C<sections_sorted>
439 Returns an array reference of requirement spec items that do not have
440 a parent -- meaning that are sections.
442 This is not a writer. Use the C<items> relationship for that.
444 =item C<text_blocks_sorted %params>
446 Returns an array reference of text blocks sorted by their positional
447 column in ascending order. If the C<output_position> parameter is
448 given then only the text blocks belonging to that C<output_position>
453 Validate values before saving. Returns list or human-readable error
456 =item C<versioned_copies_sorted %params>
458 Returns an array reference of versioned copies sorted by their version
459 number in ascending order. If the C<max_version_number> parameter is
460 given then only the versioned copies whose version number is less than
461 or equal to C<max_version_number> are returned.
471 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>