Pflichtenhefte: Unterstützung für an Textblöcke angehängte Bilder
[kivitendo-erp.git] / SL / DB / RequirementSpec.pm
1 package SL::DB::RequirementSpec;
2
3 use strict;
4
5 use Carp;
6 use List::Util qw(max reduce);
7 use Rose::DB::Object::Helpers;
8
9 use SL::DB::MetaSetup::RequirementSpec;
10 use SL::DB::Manager::RequirementSpec;
11 use SL::Locale::String;
12 use SL::Util qw(_hashify);
13
14 __PACKAGE__->meta->add_relationship(
15   items            => {
16     type           => 'one to many',
17     class          => 'SL::DB::RequirementSpecItem',
18     column_map     => { id => 'requirement_spec_id' },
19   },
20   text_blocks      => {
21     type           => 'one to many',
22     class          => 'SL::DB::RequirementSpecTextBlock',
23     column_map     => { id => 'requirement_spec_id' },
24   },
25   versioned_copies => {
26     type           => 'one to many',
27     class          => 'SL::DB::RequirementSpec',
28     column_map     => { id => 'working_copy_id' },
29   },
30   versions         => {
31     type           => 'one to many',
32     class          => 'SL::DB::RequirementSpecVersion',
33     column_map     => { id => 'requirement_spec_id' },
34   },
35   working_copy_versions => {
36     type           => 'one to many',
37     class          => 'SL::DB::RequirementSpecVersion',
38     column_map     => { id => 'working_copy_id' },
39   },
40   orders           => {
41     type           => 'one to many',
42     class          => 'SL::DB::RequirementSpecOrder',
43     column_map     => { id => 'requirement_spec_id' },
44   },
45 );
46
47 __PACKAGE__->meta->initialize;
48
49 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
50
51 sub validate {
52   my ($self) = @_;
53
54   my @errors;
55   push @errors, t8('The title is missing.') if !$self->title;
56
57   return @errors;
58 }
59
60 sub _before_save_initialize_not_null_columns {
61   my ($self) = @_;
62
63   for (qw(previous_section_number previous_fb_number previous_picture_number)) {
64     $self->$_(0) if !defined $self->$_;
65   }
66
67   return 1;
68 }
69
70 sub version {
71   my ($self) = @_;
72
73   croak "Not a writer" if scalar(@_) > 1;
74
75   return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
76 }
77
78 sub text_blocks_sorted {
79   my ($self, %params) = _hashify(1, @_);
80
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;
84
85   return \@text_blocks;
86 }
87
88 sub sections_sorted {
89   my ($self, @rest) = @_;
90
91   croak "This sub is not a writer" if @rest;
92
93   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
94 }
95
96 sub sections { &sections_sorted; }
97
98 sub orders_sorted {
99   my ($self, %params) = _hashify(1, @_);
100   my $by              = $params{by} || 'itime';
101
102   return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ];
103 }
104
105 sub displayable_name {
106   my ($self) = @_;
107
108   return sprintf('%s: "%s"', $self->type->description, $self->title);
109 }
110
111 sub versioned_copies_sorted {
112   my ($self, %params) = _hashify(1, @_);
113
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;
117
118   return \@copies;
119 }
120
121 sub create_copy {
122   my ($self, %params) = @_;
123
124   return $self->_create_copy(%params) if $self->db->in_transaction;
125
126   my $copy;
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]));
129     return undef;
130   }
131
132   return $copy;
133 }
134
135 sub _create_copy {
136   my ($self, %params) = @_;
137
138   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
139   $copy->copy_from($self, %params);
140
141   return $copy;
142 }
143
144 sub _copy_from {
145   my ($self, $params, %attributes) = @_;
146
147   my $source = $params->{source};
148
149   croak "Missing parameter 'source'" unless $source;
150
151   # Copy attributes.
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)),
154                              %attributes);
155   }
156
157   my %paste_template_result;
158
159   # Clone text blocks and pictures.
160   my $clone_picture = sub {
161     my ($picture) = @_;
162     my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($picture);
163     $cloned->position(undef);
164     return $cloned;
165   };
166
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 } ]);
172     return $cloned;
173   };
174
175   $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ];
176
177   if (!$params->{paste_template}) {
178     $self->text_blocks($paste_template_result{text_blocks});
179   } else {
180     $self->add_text_blocks($paste_template_result{text_blocks});
181   }
182
183   # Save new object -- we need its ID for the items.
184   $self->save;
185
186   my %id_to_clone;
187
188   # Clone items.
189   my $clone_item;
190   $clone_item = sub {
191     my ($item) = @_;
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 });
197
198     $id_to_clone{ $item->id } = $cloned;
199
200     return $cloned;
201   };
202
203   $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ];
204
205   if (!$params->{paste_template}) {
206     $self->items($paste_template_result{sections});
207   } else {
208     $self->add_items($paste_template_result{sections});
209   }
210
211   # Save the items -- need to do that before setting dependencies.
212   $self->save;
213
214   # Set 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 } ]);
218   }
219
220   $self->update_attributes(%attributes) unless $params->{paste_template};
221
222   return %paste_template_result;
223 }
224
225 sub copy_from {
226   my ($self, $source, %attributes) = @_;
227
228   $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); });
229 }
230
231 sub paste_template {
232   my ($self, $template) = @_;
233
234   $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); });
235 }
236
237 sub highest_version {
238   my ($self) = @_;
239
240   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
241 }
242
243 sub is_working_copy {
244   my ($self) = @_;
245
246   return !$self->working_copy_id;
247 }
248
249 sub next_version_number {
250   my ($self) = @_;
251
252   return 1 if !$self->id;
253
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 (
258       SELECT rs.id
259       FROM requirement_specs rs
260       WHERE (rs.id              = ?)
261          OR (rs.working_copy_id = ?)
262     )
263 SQL
264
265   return ($max_number // 0) + 1;
266 }
267
268 sub create_version {
269   my ($self, %attributes) = @_;
270
271   croak "Cannot work on a versioned copy" if $self->working_copy_id;
272
273   my ($copy, $version);
274   my $ok = $self->db->with_transaction(sub {
275     delete $attributes{version_number};
276
277     SL::DB::Manager::RequirementSpecVersion->update_all(
278       set   => [ working_copy_id     => undef     ],
279       where => [ requirement_spec_id => $self->id ],
280     );
281
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;
284
285     1;
286   });
287
288   return $ok ? ($copy, $version) : ();
289 }
290
291 sub invalidate_version {
292   my ($self, %params) = @_;
293
294   croak "Cannot work on a versioned copy" if $self->working_copy_id;
295
296   return if !$self->id;
297
298   SL::DB::Manager::RequirementSpecVersion->update_all(
299     set   => [ working_copy_id => undef     ],
300     where => [ working_copy_id => $self->id ],
301   );
302 }
303
304 1;
305 __END__
306
307 =pod
308
309 =encoding utf8
310
311 =head1 NAME
312
313 SL::DB::RequirementSpec - RDBO model for requirement specs
314
315 =head1 OVERVIEW
316
317 The database structure behind requirement specs is a bit involved. The
318 important thing is how working copy/versions are handled.
319
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>.
324
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.
332
333 The design is as follows:
334
335 =over 2
336
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
339 C<NULL>.
340
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.
345
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>.
349
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
356 hooks in Perl.
357
358 =back
359
360 =head1 DATABASE TRIGGERS AND CHECKS
361
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
365 models.
366
367 =head2 DELETION
368
369 When you delete a requirement spec all of its dependencies (items,
370 text blocks, versions etc.) are deleted by triggers.
371
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
376 well.
377
378 =head2 UPDATING
379
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.
383
384 =head2 CONSISTENCY CHECKS
385
386 Several consistency checks are applied to requirement spec items:
387
388 =over 2
389
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>.
392
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
395 NULL> otherwise.
396
397 =back
398
399 =head1 FUNCTIONS
400
401 =over 4
402
403 =item C<copy_from $source, %attributes>
404
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.
409
410 C<%attributes> are attributes that are assigned to C<$self> after all
411 the basic attributes from C<$source> have been assigned.
412
413 This function can be used for resetting a working copy to a specific
414 version. Example:
415
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;
418
419   $requirement_spec->copy_from($versioned_copy);
420   $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
421
422 =item C<create_copy>
423
424 Creates and returns a copy of C<$self>. The copy is already
425 saved. Creating the copy happens within a transaction.
426
427 =item C<create_version %attributes>
428
429 Prerequisites: C<$self> must be a working copy (see the overview),
430 not a versioned copy.
431
432 This function creates a new version for C<$self>. This involves
433 several steps:
434
435 =over 2
436
437 =item 1. The next version number is calculated using
438 L</next_version_number>.
439
440 =item 2. A copy of C<$self> is created with L</create_copy>.
441
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.
445
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>.
449
450 =back
451
452 All this is done within a transaction.
453
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.
457
458 =item C<displayable_name>
459
460 Returns a human-readable name for this instance consisting of the type
461 and the title.
462
463 =item C<highest_version>
464
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.
468
469 This can be used for calculating the difference between the working
470 copy and the last version created for it.
471
472 =item C<invalidate_version>
473
474 Prerequisites: C<$self> must be a working copy (see the overview),
475 not a versioned copy.
476
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>.
479
480 =item C<is_working_copy>
481
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>.
484
485 =item C<next_version_number>
486
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:
491
492   if (has_never_had_a_version)
493     return 1
494   else
495     return max(version_number for all versions for this requirement spec) + 1
496
497 =item C<sections>
498
499 An alias for L</sections_sorted>.
500
501 =item C<sections_sorted>
502
503 Returns an array reference of requirement spec items that do not have
504 a parent -- meaning that are sections.
505
506 This is not a writer. Use the C<items> relationship for that.
507
508 =item C<text_blocks_sorted %params>
509
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>
513 are returned.
514
515 =item C<validate>
516
517 Validate values before saving. Returns list or human-readable error
518 messages (if any).
519
520 =item C<versioned_copies_sorted %params>
521
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.
526
527 =back
528
529 =head1 BUGS
530
531 Nothing here yet.
532
533 =head1 AUTHOR
534
535 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
536
537 =cut