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::RequirementSpecOrder',
33 column_map => { id => 'requirement_spec_id' },
37 __PACKAGE__->meta->initialize;
39 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
45 push @errors, t8('The title is missing.') if !$self->title;
50 sub _before_save_initialize_not_null_columns {
53 $self->previous_section_number(0) if !defined $self->previous_section_number;
54 $self->previous_fb_number(0) if !defined $self->previous_fb_number;
59 sub text_blocks_sorted {
60 my ($self, %params) = _hashify(1, @_);
62 my @text_blocks = @{ $self->text_blocks };
63 @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position};
64 @text_blocks = sort { $a->position <=> $b->position } @text_blocks;
70 my ($self, @rest) = @_;
72 croak "This sub is not a writer" if @rest;
74 return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
77 sub sections { §ions_sorted; }
80 my ($self, %params) = _hashify(1, @_);
81 my $by = $params{by} || 'itime';
83 return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
86 sub displayable_name {
89 return sprintf('%s: "%s"', $self->type->description, $self->title);
92 sub versioned_copies_sorted {
93 my ($self, %params) = _hashify(1, @_);
95 my @copies = @{ $self->versioned_copies };
96 @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number};
97 @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies;
103 my ($self, %params) = @_;
105 return $self->_create_copy(%params) if $self->db->in_transaction;
108 if (!$self->db->do_transaction(sub { $copy = $self->_create_copy(%params) })) {
109 $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
117 my ($self, %params) = @_;
119 my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
120 $copy->copy_from($self, %params);
126 my ($self, $params, %attributes) = @_;
128 my $source = $params->{source};
130 croak "Missing parameter 'source'" unless $source;
133 if (!$params->{paste_template}) {
134 $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)),
138 my %paste_template_result;
141 my $clone_text_block = sub {
142 my ($text_block) = @_;
143 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block);
144 $cloned->position(undef);
148 $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
150 if (!$params->{paste_template}) {
151 $self->text_blocks($paste_template_result{text_blocks});
153 $self->add_text_blocks($paste_template_result{text_blocks});
156 # Save new object -- we need its ID for the items.
165 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
166 $cloned->requirement_spec_id($self->id);
167 $cloned->position(undef);
168 $cloned->fb_number(undef) if $params->{paste_template};
169 $cloned->children(map { $clone_item->($_) } @{ $item->children });
171 $id_to_clone{ $item->id } = $cloned;
176 $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
178 if (!$params->{paste_template}) {
179 $self->items($paste_template_result{sections});
181 $self->add_items($paste_template_result{sections});
184 # Save the items -- need to do that before setting dependencies.
188 foreach my $item (@{ $source->items }) {
189 next unless @{ $item->dependencies };
190 $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
193 $self->update_attributes(%attributes) unless $params->{paste_template};
195 return %paste_template_result;
199 my ($self, $source, %attributes) = @_;
201 $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
205 my ($self, $template) = @_;
207 $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
210 sub highest_version {
213 return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
216 sub is_working_copy {
219 return !$self->working_copy_id;
222 sub next_version_number {
225 return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
229 my ($self, %attributes) = @_;
231 croak "Cannot work on a versioned copy" if $self->working_copy_id;
233 my ($copy, $version);
234 my $ok = $self->db->with_transaction(sub {
235 delete $attributes{version_number};
237 $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
238 $copy = $self->create_copy;
239 $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
240 $self->update_attributes(version_id => $version->id);
245 return $ok ? ($copy, $version) : ();
248 sub invalidate_version {
249 my ($self, %params) = @_;
251 croak "Cannot work on a versioned copy" if $self->working_copy_id;
253 return if !$self->id || !$self->version_id;
254 $self->update_attributes(version_id => undef);
266 SL::DB::RequirementSpec - RDBO model for requirement specs
270 The database structure behind requirement specs is a bit involved. The
271 important thing is how working copy/versions are handled.
273 The table contains three important columns: C<id> (which is also the
274 primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
275 is a self-referencing column: it can be C<NULL>, but if it isn't then
276 it contains another requirement spec C<id>. C<version_id> on the other
277 hand references the table C<requirement_spec_versions>.
279 The design is as follows:
283 =item * The user is always working on a working copy. The working copy
284 is identified in the database by having C<working_copy_id> set to
287 =item * All other entries in this table are referred to as I<versioned
288 copies>. A versioned copy is a copy of a working frozen at the moment
289 in time it was created. Each versioned copy refers back to the working
290 copy it belongs to: each has its C<working_copy_id> set.
292 =item * Each versioned copy must reference an entry in the table
293 C<requirement_spec_versions>. Meaning: for each versioned copy
294 C<version_id> must not be C<NULL>.
296 =item * Directly after creating a versioned copy even the working copy
297 itself points to a certain version via its C<version_id> column: to
298 the same version that the versioned copy just created points
299 to. However, any modification that will be visible to the customer
300 (text, positioning etc but not internal things like time/cost
301 estimation changes) will cause the working copy to be set to 'no
302 version' again. This is achieved via before save hooks in Perl.
306 =head1 DATABASE TRIGGERS AND CHECKS
308 Several database triggers and consistency checks exist that manage
309 requirement specs, their items and their dependencies. These are
310 described here instead of in the individual files for the other RDBO
315 When you delete a requirement spec all of its dependencies (items,
316 text blocks, versions etc.) are deleted by triggers.
318 When you delete an item (either a section or a (sub-)function block)
319 all of its children will be deleted as well. This will trigger the
320 same trigger resulting in a recursive deletion with the bottom-most
321 items being deleted first. Their item dependencies are deleted as
326 Whenever you update a requirement spec item a trigger will fire that
327 will update the parent's C<time_estimation> column. This also happens
328 when an item is deleted or updated.
330 =head2 CONSISTENCY CHECKS
332 Several consistency checks are applied to requirement spec items:
336 =item * Column C<requirement_spec_item.item_type> can only contain one of
337 the values C<section>, C<function-block> or C<sub-function-block>.
339 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
340 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
349 =item C<copy_from $source, %attributes>
351 Copies everything (basic attributes like type/title/customer, items,
352 text blocks, time/cost estimation) save for the versions from the
353 other requirement spec object C<$source> into C<$self> and saves
354 it. This is done within a transaction.
356 C<%attributes> are attributes that are assigned to C<$self> after all
357 the basic attributes from C<$source> have been assigned.
359 This function can be used for resetting a working copy to a specific
362 my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
363 my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
365 $requirement_spec->copy_from(
367 version_id => $versioned_copy->version_id,
372 Creates and returns a copy of C<$self>. The copy is already
373 saved. Creating the copy happens within a transaction.
375 =item C<create_version %attributes>
377 Prerequisites: C<$self> must be a working copy (see the overview),
378 not a versioned copy.
380 This function creates a new version for C<$self>. This involves
385 =item 1. The next version number is calculated using
386 L</next_version_number>.
388 =item 2. An instance of L<SL::DB::RequirementSpecVersion> is
389 created. Its attributes are copied from C<%attributes> save for the
390 version number which is taken from step 1.
392 =item 3. A copy of C<$self> is created with L</create_copy>.
394 =item 4. The version instance created in step is assigned to the copy
397 =item 5. The C<version_id> in C<$self> is set to the copy's ID from
402 All this is done within a transaction.
404 In case of success a two-element list is returned consisting of the
405 copy & version objects created in steps 3 and 2 respectively. In case
406 of a failure an empty list will be returned.
408 =item C<displayable_name>
410 Returns a human-readable name for this instance consisting of the type
413 =item C<highest_version>
415 Given a working copy C<$self> this function returns the versioned copy
416 of C<$self> with the highest version number. If such a version exist
417 its instance is returned. Otherwise C<undef> is returned.
419 This can be used for calculating the difference between the working
420 copy and the last version created for it.
422 =item C<invalidate_version>
424 Prerequisites: C<$self> must be a working copy (see the overview),
425 not a versioned copy.
427 Sets the C<version_id> field to C<undef> and saves C<$self>.
429 =item C<is_working_copy>
431 Returns trueish if C<$self> is a working copy and not a versioned
432 copy. The condition for this is that C<working_copy_id> is C<undef>.
434 =item C<next_version_number>
436 Calculates and returns the next version number for this requirement
437 spec. Version numbers start at 1 and are incremented by one for each
438 version created for it, no matter whether or not it has been reverted
439 to a previous version since. It boils down to this pseudo-code:
441 if (has_never_had_a_version)
444 return max(version_number for all versions for this requirement spec) + 1
448 An alias for L</sections_sorted>.
450 =item C<sections_sorted>
452 Returns an array reference of requirement spec items that do not have
453 a parent -- meaning that are sections.
455 This is not a writer. Use the C<items> relationship for that.
457 =item C<text_blocks_sorted %params>
459 Returns an array reference of text blocks sorted by their positional
460 column in ascending order. If the C<output_position> parameter is
461 given then only the text blocks belonging to that C<output_position>
466 Validate values before saving. Returns list or human-readable error
469 =item C<versioned_copies_sorted %params>
471 Returns an array reference of versioned copies sorted by their version
472 number in ascending order. If the C<max_version_number> parameter is
473 given then only the versioned copies whose version number is less than
474 or equal to C<max_version_number> are returned.
484 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>