3 |
3 |
use strict;
|
4 |
4 |
|
5 |
5 |
use Carp;
|
|
6 |
use List::Util qw(max reduce);
|
6 |
7 |
use Rose::DB::Object::Helpers;
|
7 |
8 |
|
8 |
9 |
use SL::DB::MetaSetup::RequirementSpec;
|
... | ... | |
110 |
111 |
return $copy;
|
111 |
112 |
}
|
112 |
113 |
|
113 |
|
sub copy_from {
|
|
114 |
sub _copy_from {
|
114 |
115 |
my ($self, $source, %attributes) = @_;
|
115 |
116 |
|
116 |
117 |
croak "Missing parameter 'source'" unless $source;
|
... | ... | |
156 |
157 |
return $self;
|
157 |
158 |
}
|
158 |
159 |
|
159 |
|
sub previous_version {
|
160 |
|
my ($self) = @_;
|
|
160 |
sub copy_from {
|
|
161 |
my ($self, $source, %attributes) = @_;
|
161 |
162 |
|
162 |
|
my $and = $self->version_id ? " AND (version_id <> ?)" : "";
|
163 |
|
my $id = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id, ($self->version_id) x !!$self->version_id);
|
164 |
|
SELECT MAX(id)
|
165 |
|
FROM requirement_specs
|
166 |
|
WHERE (working_copy_id = ?) $and
|
167 |
|
SQL
|
|
163 |
$self->db->with_transaction(sub { $self->_copy_from($source, %attributes); });
|
|
164 |
}
|
168 |
165 |
|
169 |
|
return $id ? SL::DB::RequirementSpec->new(id => $id)->load : undef;
|
|
166 |
sub highest_version {
|
|
167 |
my ($self) = @_;
|
|
168 |
|
|
169 |
return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
|
170 |
170 |
}
|
171 |
171 |
|
172 |
172 |
sub is_working_copy {
|
... | ... | |
177 |
177 |
|
178 |
178 |
sub next_version_number {
|
179 |
179 |
my ($self) = @_;
|
180 |
|
my $max_number = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id);
|
181 |
|
SELECT COALESCE(MAX(ver.version_number), 0)
|
182 |
|
FROM requirement_spec_versions ver
|
183 |
|
JOIN requirement_specs rs ON (rs.version_id = ver.id)
|
184 |
|
WHERE rs.working_copy_id = ?
|
185 |
|
SQL
|
186 |
|
|
187 |
|
return $max_number + 1;
|
|
180 |
|
|
181 |
return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
|
188 |
182 |
}
|
189 |
183 |
|
190 |
184 |
sub create_version {
|
... | ... | |
193 |
187 |
croak "Cannot work on a versioned copy" if $self->working_copy_id;
|
194 |
188 |
|
195 |
189 |
my ($copy, $version);
|
196 |
|
my $ok = $self->db->do_transaction(sub {
|
|
190 |
my $ok = $self->db->with_transaction(sub {
|
197 |
191 |
delete $attributes{version_number};
|
198 |
192 |
|
199 |
193 |
$version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
|
... | ... | |
217 |
211 |
}
|
218 |
212 |
|
219 |
213 |
1;
|
|
214 |
__END__
|
|
215 |
|
|
216 |
=pod
|
|
217 |
|
|
218 |
=encoding utf8
|
|
219 |
|
|
220 |
=head1 NAME
|
|
221 |
|
|
222 |
SL::DB::RequirementSpec - RDBO model for requirement specs
|
|
223 |
|
|
224 |
=head1 OVERVIEW
|
|
225 |
|
|
226 |
The database structure behind requirement specs is a bit involved. The
|
|
227 |
important thing is how working copy/versions are handled.
|
|
228 |
|
|
229 |
The table contains three important columns: C<id> (which is also the
|
|
230 |
primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
|
|
231 |
is a self-referencing column: it can be C<NULL>, but if it isn't then
|
|
232 |
it contains another requirement spec C<id>. C<version_id> on the other
|
|
233 |
hand references the table C<requirement_spec_versions>.
|
|
234 |
|
|
235 |
The design is as follows:
|
|
236 |
|
|
237 |
=over 2
|
|
238 |
|
|
239 |
=item * The user is always working on a working copy. The working copy
|
|
240 |
is identified in the database by having C<working_copy_id> set to
|
|
241 |
C<NULL>.
|
|
242 |
|
|
243 |
=item * All other entries in this table are referred to as I<versioned
|
|
244 |
copies>. A versioned copy is a copy of a working frozen at the moment
|
|
245 |
in time it was created. Each versioned copy refers back to the working
|
|
246 |
copy it belongs to: each has its C<working_copy_id> set.
|
|
247 |
|
|
248 |
=item * Each versioned copy must reference an entry in the table
|
|
249 |
C<requirement_spec_versions>. Meaning: for each versioned copy
|
|
250 |
C<version_id> must not be C<NULL>.
|
|
251 |
|
|
252 |
=item * Directly after creating a versioned copy even the working copy
|
|
253 |
itself points to a certain version via its C<version_id> column: to
|
|
254 |
the same version that the versioned copy just created points
|
|
255 |
to. However, any modification that will be visible to the customer
|
|
256 |
(text, positioning etc but not internal things like time/cost
|
|
257 |
estimation changes) will cause the working copy to be set to 'no
|
|
258 |
version' again. This is achieved via before save hooks in Perl.
|
|
259 |
|
|
260 |
=back
|
|
261 |
|
|
262 |
=head1 DATABASE TRIGGERS AND CHECKS
|
|
263 |
|
|
264 |
Several database triggers and consistency checks exist that manage
|
|
265 |
requirement specs, their items and their dependencies. These are
|
|
266 |
described here instead of in the individual files for the other RDBO
|
|
267 |
models.
|
|
268 |
|
|
269 |
=head2 DELETION
|
|
270 |
|
|
271 |
When you delete a requirement spec all of its dependencies (items,
|
|
272 |
text blocks, versions etc.) are deleted by triggers.
|
|
273 |
|
|
274 |
When you delete an item (either a section or a (sub-)function block)
|
|
275 |
all of its children will be deleted as well. This will trigger the
|
|
276 |
same trigger resulting in a recursive deletion with the bottom-most
|
|
277 |
items being deleted first. Their item dependencies are deleted as
|
|
278 |
well.
|
|
279 |
|
|
280 |
=head2 UPDATING
|
|
281 |
|
|
282 |
Whenever you update a requirement spec item a trigger will fire that
|
|
283 |
will update the parent's C<time_estimation> column. This also happens
|
|
284 |
when an item is deleted or updated.
|
|
285 |
|
|
286 |
=head2 CONSISTENCY CHECKS
|
|
287 |
|
|
288 |
Several consistency checks are applied to requirement spec items:
|
|
289 |
|
|
290 |
=over 2
|
|
291 |
|
|
292 |
=item * Column C<requirement_spec_item.item_type> can only contain one of
|
|
293 |
the values C<section>, C<function-block> or C<sub-function-block>.
|
|
294 |
|
|
295 |
=item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
|
|
296 |
C<requirement_spec_item.item_type> is set to C<section> and C<NOT
|
|
297 |
NULL> otherwise.
|
|
298 |
|
|
299 |
=back
|
|
300 |
|
|
301 |
=head1 FUNCTIONS
|
|
302 |
|
|
303 |
=over 4
|
|
304 |
|
|
305 |
=item C<copy_from $source, %attributes>
|
|
306 |
|
|
307 |
Copies everything (basic attributes like type/title/customer, items,
|
|
308 |
text blocks, time/cost estimation) save for the versions from the
|
|
309 |
other requirement spec object C<$source> into C<$self> and saves
|
|
310 |
it. This is done within a transaction.
|
|
311 |
|
|
312 |
C<%attributes> are attributes that are assigned to C<$self> after all
|
|
313 |
the basic attributes from C<$source> have been assigned.
|
|
314 |
|
|
315 |
This function can be used for resetting a working copy to a specific
|
|
316 |
version. Example:
|
|
317 |
|
|
318 |
my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
|
|
319 |
my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
|
|
320 |
|
|
321 |
$requirement_spec->copy_from(
|
|
322 |
$versioned_copy,
|
|
323 |
version_id => $versioned_copy->version_id,
|
|
324 |
);
|
|
325 |
|
|
326 |
=item C<create_copy>
|
|
327 |
|
|
328 |
Creates and returns a copy of C<$self>. The copy is already
|
|
329 |
saved. Creating the copy happens within a transaction.
|
|
330 |
|
|
331 |
=item C<create_version %attributes>
|
|
332 |
|
|
333 |
Prerequisites: C<$self> must be a working copy (see L</working_copy>),
|
|
334 |
not a versioned copy.
|
|
335 |
|
|
336 |
This function creates a new version for C<$self>. This involves
|
|
337 |
several steps:
|
|
338 |
|
|
339 |
=over 2
|
|
340 |
|
|
341 |
=item 1. The next version number is calculated using
|
|
342 |
L</next_version_number>.
|
|
343 |
|
|
344 |
=item 2. An instance of L<SL::DB::RequirementSpecVersion> is
|
|
345 |
created. Its attributes are copied from C<%attributes> save for the
|
|
346 |
version number which is taken from step 1.
|
|
347 |
|
|
348 |
=item 3. A copy of C<$self> is created with L</create_copy>.
|
|
349 |
|
|
350 |
=item 4. The version instance created in step is assigned to the copy
|
|
351 |
from step 3.
|
|
352 |
|
|
353 |
=item 5. The C<version_id> in C<$self> is set to the copy's ID from
|
|
354 |
step 3.
|
|
355 |
|
|
356 |
=back
|
|
357 |
|
|
358 |
All this is done within a transaction.
|
|
359 |
|
|
360 |
In case of success a two-element list is returned consisting of the
|
|
361 |
copy & version objects created in steps 3 and 2 respectively. In case
|
|
362 |
of a failure an empty list will be returned.
|
|
363 |
|
|
364 |
=item C<displayable_name>
|
|
365 |
|
|
366 |
Returns a human-readable name for this instance consisting of the type
|
|
367 |
and the title.
|
|
368 |
|
|
369 |
=item C<highest_version>
|
|
370 |
|
|
371 |
Given a working copy C<$self> this function returns the versioned copy
|
|
372 |
of C<$self> with the highest version number. If such a version exist
|
|
373 |
its instance is returned. Otherwise C<undef> is returned.
|
|
374 |
|
|
375 |
This can be used for calculating the difference between the working
|
|
376 |
copy and the last version created for it.
|
|
377 |
|
|
378 |
=item C<invalidate_version>
|
|
379 |
|
|
380 |
Prerequisites: C<$self> must be a working copy (see L</working_copy>),
|
|
381 |
not a versioned copy.
|
|
382 |
|
|
383 |
Sets the C<version_id> field to C<undef> and saves C<$self>.
|
|
384 |
|
|
385 |
=item C<is_working_copy>
|
|
386 |
|
|
387 |
Returns trueish if C<$self> is a working copy and not a versioned
|
|
388 |
copy. The condition for this is that C<working_copy_id> is C<undef>.
|
|
389 |
|
|
390 |
=item C<next_version_number>
|
|
391 |
|
|
392 |
Calculates and returns the next version number for this requirement
|
|
393 |
spec. Version numbers start at 1 and are incremented by one for each
|
|
394 |
version created for it, no matter whether or not it has been reverted
|
|
395 |
to a previous version since. It boils down to this pseudo-code:
|
|
396 |
|
|
397 |
if (has_never_had_a_version)
|
|
398 |
return 1
|
|
399 |
else
|
|
400 |
return max(version_number for all versions for this requirement spec) + 1
|
|
401 |
|
|
402 |
=item C<sections>
|
|
403 |
|
|
404 |
An alias for L</sections_sorted>.
|
|
405 |
|
|
406 |
=item C<sections_sorted>
|
|
407 |
|
|
408 |
Returns a list of requirement spec items that
|
|
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 (or an array reference depending on context) of text
|
|
415 |
blocks sorted by their positional column in ascending order. If the
|
|
416 |
C<output_position> parameter is given then only the text blocks
|
|
417 |
belonging to that C<output_position> 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 (or an array reference depending on context) of
|
|
427 |
versioned copies sorted by their version number in ascending order. If
|
|
428 |
the C<max_version_number> parameter is given then only the versioned
|
|
429 |
copies whose version number is less than or equal to
|
|
430 |
C<max_version_number> are returned.
|
|
431 |
|
|
432 |
=back
|
|
433 |
|
|
434 |
=head1 BUGS
|
|
435 |
|
|
436 |
Nothing here yet.
|
|
437 |
|
|
438 |
=head1 AUTHOR
|
|
439 |
|
|
440 |
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
|
|
441 |
|
|
442 |
=cut
|
Pflichtenhefte: Dokumentation; Refactoring; Bugfix Diff-Berechnung