RequirementSpec(Item): Arrays nur als Referenz zurückgeben
[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 );
31
32 __PACKAGE__->meta->initialize;
33
34 __PACKAGE__->before_save('_before_save_initialize_not_null_columns');
35
36 sub validate {
37   my ($self) = @_;
38
39   my @errors;
40   push @errors, t8('The title is missing.') if !$self->title;
41
42   return @errors;
43 }
44
45 sub _before_save_initialize_not_null_columns {
46   my ($self) = @_;
47
48   $self->previous_section_number(0) if !defined $self->previous_section_number;
49   $self->previous_fb_number(0)      if !defined $self->previous_fb_number;
50
51   return 1;
52 }
53
54 sub text_blocks_sorted {
55   my ($self, %params) = _hashify(1, @_);
56
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;
60
61   return \@text_blocks;
62 }
63
64 sub sections_sorted {
65   my ($self, @rest) = @_;
66
67   croak "This sub is not a writer" if @rest;
68
69   return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ];
70 }
71
72 sub sections { &sections_sorted; }
73
74 sub displayable_name {
75   my ($self) = @_;
76
77   return sprintf('%s: "%s"', $self->type->description, $self->title);
78 }
79
80 sub versioned_copies_sorted {
81   my ($self, %params) = _hashify(1, @_);
82
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;
86
87   return \@copies;
88 }
89
90 sub create_copy {
91   my ($self, %params) = @_;
92
93   return $self->_create_copy(%params) if $self->db->in_transaction;
94
95   my $copy;
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]));
98     return undef;
99   }
100
101   return $copy;
102 }
103
104 sub _create_copy {
105   my ($self, %params) = @_;
106
107   my $copy = Rose::DB::Object::Helpers::clone_and_reset($self);
108   $copy->copy_from($self, %params);
109
110   return $copy;
111 }
112
113 sub _copy_from {
114   my ($self, $source, %attributes) = @_;
115
116   croak "Missing parameter 'source'" unless $source;
117
118   # Copy attributes.
119   $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)),
120                            %attributes);
121
122   # Clone text blocks.
123   $self->text_blocks(map { Rose::DB::Object::Helpers::clone_and_reset($_) } @{ $source->text_blocks });
124
125   # Save new object -- we need its ID for the items.
126   $self->save;
127
128   my %id_to_clone;
129
130   # Clone items.
131   my $clone_item;
132   $clone_item = sub {
133     my ($item) = @_;
134     my $cloned = Rose::DB::Object::Helpers::clone_and_reset($item);
135     $cloned->requirement_spec_id($self->id);
136     $cloned->children(map { $clone_item->($_) } @{ $item->children });
137
138     $id_to_clone{ $item->id } = $cloned;
139
140     return $cloned;
141   };
142
143   $self->items(map { $clone_item->($_) } @{ $source->sections });
144
145   # Save the items -- need to do that before setting dependencies.
146   $self->save;
147
148   # Set dependencies.
149   foreach my $item (@{ $source->items }) {
150     next unless @{ $item->dependencies };
151     $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]);
152   }
153
154   $self->update_attributes(%attributes);
155
156   return $self;
157 }
158
159 sub copy_from {
160   my ($self, $source, %attributes) = @_;
161
162   $self->db->with_transaction(sub { $self->_copy_from($source, %attributes); });
163 }
164
165 sub highest_version {
166   my ($self) = @_;
167
168   return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
169 }
170
171 sub is_working_copy {
172   my ($self) = @_;
173
174   return !$self->working_copy_id;
175 }
176
177 sub next_version_number {
178   my ($self) = @_;
179
180   return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
181 }
182
183 sub create_version {
184   my ($self, %attributes) = @_;
185
186   croak "Cannot work on a versioned copy" if $self->working_copy_id;
187
188   my ($copy, $version);
189   my $ok = $self->db->with_transaction(sub {
190     delete $attributes{version_number};
191
192     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
193     $copy    = $self->create_copy;
194     $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
195     $self->update_attributes(version_id => $version->id);
196
197     1;
198   });
199
200   return $ok ? ($copy, $version) : ();
201 }
202
203 sub invalidate_version {
204   my ($self, %params) = @_;
205
206   croak "Cannot work on a versioned copy" if $self->working_copy_id;
207
208   return if !$self->id || !$self->version_id;
209   $self->update_attributes(version_id => undef);
210 }
211
212 1;
213 __END__
214
215 =pod
216
217 =encoding utf8
218
219 =head1 NAME
220
221 SL::DB::RequirementSpec - RDBO model for requirement specs
222
223 =head1 OVERVIEW
224
225 The database structure behind requirement specs is a bit involved. The
226 important thing is how working copy/versions are handled.
227
228 The table contains three important columns: C<id> (which is also the
229 primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
230 is a self-referencing column: it can be C<NULL>, but if it isn't then
231 it contains another requirement spec C<id>. C<version_id> on the other
232 hand references the table C<requirement_spec_versions>.
233
234 The design is as follows:
235
236 =over 2
237
238 =item * The user is always working on a working copy. The working copy
239 is identified in the database by having C<working_copy_id> set to
240 C<NULL>.
241
242 =item * All other entries in this table are referred to as I<versioned
243 copies>. A versioned copy is a copy of a working frozen at the moment
244 in time it was created. Each versioned copy refers back to the working
245 copy it belongs to: each has its C<working_copy_id> set.
246
247 =item * Each versioned copy must reference an entry in the table
248 C<requirement_spec_versions>. Meaning: for each versioned copy
249 C<version_id> must not be C<NULL>.
250
251 =item * Directly after creating a versioned copy even the working copy
252 itself points to a certain version via its C<version_id> column: to
253 the same version that the versioned copy just created points
254 to. However, any modification that will be visible to the customer
255 (text, positioning etc but not internal things like time/cost
256 estimation changes) will cause the working copy to be set to 'no
257 version' again. This is achieved via before save hooks in Perl.
258
259 =back
260
261 =head1 DATABASE TRIGGERS AND CHECKS
262
263 Several database triggers and consistency checks exist that manage
264 requirement specs, their items and their dependencies. These are
265 described here instead of in the individual files for the other RDBO
266 models.
267
268 =head2 DELETION
269
270 When you delete a requirement spec all of its dependencies (items,
271 text blocks, versions etc.) are deleted by triggers.
272
273 When you delete an item (either a section or a (sub-)function block)
274 all of its children will be deleted as well. This will trigger the
275 same trigger resulting in a recursive deletion with the bottom-most
276 items being deleted first. Their item dependencies are deleted as
277 well.
278
279 =head2 UPDATING
280
281 Whenever you update a requirement spec item a trigger will fire that
282 will update the parent's C<time_estimation> column. This also happens
283 when an item is deleted or updated.
284
285 =head2 CONSISTENCY CHECKS
286
287 Several consistency checks are applied to requirement spec items:
288
289 =over 2
290
291 =item * Column C<requirement_spec_item.item_type> can only contain one of
292 the values C<section>, C<function-block> or C<sub-function-block>.
293
294 =item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
295 C<requirement_spec_item.item_type> is set to C<section> and C<NOT
296 NULL> otherwise.
297
298 =back
299
300 =head1 FUNCTIONS
301
302 =over 4
303
304 =item C<copy_from $source, %attributes>
305
306 Copies everything (basic attributes like type/title/customer, items,
307 text blocks, time/cost estimation) save for the versions from the
308 other requirement spec object C<$source> into C<$self> and saves
309 it. This is done within a transaction.
310
311 C<%attributes> are attributes that are assigned to C<$self> after all
312 the basic attributes from C<$source> have been assigned.
313
314 This function can be used for resetting a working copy to a specific
315 version. Example:
316
317  my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
318  my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
319
320   $requirement_spec->copy_from(
321     $versioned_copy,
322     version_id => $versioned_copy->version_id,
323   );
324
325 =item C<create_copy>
326
327 Creates and returns a copy of C<$self>. The copy is already
328 saved. Creating the copy happens within a transaction.
329
330 =item C<create_version %attributes>
331
332 Prerequisites: C<$self> must be a working copy (see L</working_copy>),
333 not a versioned copy.
334
335 This function creates a new version for C<$self>. This involves
336 several steps:
337
338 =over 2
339
340 =item 1. The next version number is calculated using
341 L</next_version_number>.
342
343 =item 2. An instance of L<SL::DB::RequirementSpecVersion> is
344 created. Its attributes are copied from C<%attributes> save for the
345 version number which is taken from step 1.
346
347 =item 3. A copy of C<$self> is created with L</create_copy>.
348
349 =item 4. The version instance created in step is assigned to the copy
350 from step 3.
351
352 =item 5. The C<version_id> in C<$self> is set to the copy's ID from
353 step 3.
354
355 =back
356
357 All this is done within a transaction.
358
359 In case of success a two-element list is returned consisting of the
360 copy & version objects created in steps 3 and 2 respectively. In case
361 of a failure an empty list will be returned.
362
363 =item C<displayable_name>
364
365 Returns a human-readable name for this instance consisting of the type
366 and the title.
367
368 =item C<highest_version>
369
370 Given a working copy C<$self> this function returns the versioned copy
371 of C<$self> with the highest version number. If such a version exist
372 its instance is returned. Otherwise C<undef> is returned.
373
374 This can be used for calculating the difference between the working
375 copy and the last version created for it.
376
377 =item C<invalidate_version>
378
379 Prerequisites: C<$self> must be a working copy (see L</working_copy>),
380 not a versioned copy.
381
382 Sets the C<version_id> field to C<undef> and saves C<$self>.
383
384 =item C<is_working_copy>
385
386 Returns trueish if C<$self> is a working copy and not a versioned
387 copy. The condition for this is that C<working_copy_id> is C<undef>.
388
389 =item C<next_version_number>
390
391 Calculates and returns the next version number for this requirement
392 spec. Version numbers start at 1 and are incremented by one for each
393 version created for it, no matter whether or not it has been reverted
394 to a previous version since. It boils down to this pseudo-code:
395
396   if (has_never_had_a_version)
397     return 1
398   else
399     return max(version_number for all versions for this requirement spec) + 1
400
401 =item C<sections>
402
403 An alias for L</sections_sorted>.
404
405 =item C<sections_sorted>
406
407 Returns an array reference of requirement spec items that do not have
408 a parent -- meaning that are sections.
409
410 This is not a writer. Use the C<items> relationship for that.
411
412 =item C<text_blocks_sorted %params>
413
414 Returns an array reference of text blocks sorted by their positional
415 column in ascending order. If the C<output_position> parameter is
416 given then only the text blocks belonging to that C<output_position>
417 are returned.
418
419 =item C<validate>
420
421 Validate values before saving. Returns list or human-readable error
422 messages (if any).
423
424 =item C<versioned_copies_sorted %params>
425
426 Returns an array reference of versioned copies sorted by their version
427 number in ascending order. If the C<max_version_number> parameter is
428 given then only the versioned copies whose version number is less than
429 or equal to C<max_version_number> are returned.
430
431 =back
432
433 =head1 BUGS
434
435 Nothing here yet.
436
437 =head1 AUTHOR
438
439 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
440
441 =cut