Pflichtenhefte: Dokumentation; Refactoring; Bugfix Diff-Berechnung
authorMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 3 May 2013 09:22:49 +0000 (11:22 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 1 Apr 2014 11:03:21 +0000 (13:03 +0200)
SL/Controller/RequirementSpecVersion.pm
SL/DB/RequirementSpec.pm
SL/DB/RequirementSpecItem.pm

index 0588847..97b5bf3 100644 (file)
@@ -37,13 +37,15 @@ sub action_new {
 
   $self->version(SL::DB::RequirementSpecVersion->new);
 
-  my $previous_version = $self->requirement_spec->previous_version;
-  my %differences      = $self->calculate_differences(current => $self->requirement_spec, previous => $previous_version);
+  my $previous_version = $self->requirement_spec->highest_version;
+  $::lxdebug->message(0, "vers num " . $previous_version->version->version_number . " id " . $previous_version->version_id);
 
   if (!$previous_version) {
     $self->version->description(t8('Initial version.'));
 
   } else {
+    my %differences = $self->calculate_differences(current => $self->requirement_spec, previous => $previous_version);
+
     my @lines;
 
     my $fb_diff = $differences{function_blocks};
@@ -124,7 +126,10 @@ sub init_versioned_copies {
 sub has_item_changed {
   my ($previous, $current) = @_;
   croak "Missing previous/current" if !$previous || !$current;
-  return any { ($previous->$_ || '') ne ($current->$_ || '') } qw(item_type parent_id fb_number title description complexity_id risk_id time_estimation net_sum);
+
+  return 1 if any { ($previous->$_ || '') ne ($current->$_ || '') } qw(item_type fb_number title description complexity_id risk_id);
+  return 0 if !$current->parent_id;
+  return $previous->parent->fb_number ne $current->parent->fb_number;
 }
 
 sub has_text_block_changed {
index 12afb24..d8329b9 100644 (file)
@@ -3,6 +3,7 @@ package SL::DB::RequirementSpec;
 use strict;
 
 use Carp;
+use List::Util qw(max reduce);
 use Rose::DB::Object::Helpers;
 
 use SL::DB::MetaSetup::RequirementSpec;
@@ -110,7 +111,7 @@ sub _create_copy {
   return $copy;
 }
 
-sub copy_from {
+sub _copy_from {
   my ($self, $source, %attributes) = @_;
 
   croak "Missing parameter 'source'" unless $source;
@@ -156,17 +157,16 @@ sub copy_from {
   return $self;
 }
 
-sub previous_version {
-  my ($self) = @_;
+sub copy_from {
+  my ($self, $source, %attributes) = @_;
 
-  my $and    = $self->version_id ? " AND (version_id <> ?)" : "";
-  my $id     = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id, ($self->version_id) x !!$self->version_id);
-   SELECT MAX(id)
-   FROM requirement_specs
-   WHERE (working_copy_id = ?) $and
-SQL
+  $self->db->with_transaction(sub { $self->_copy_from($source, %attributes); });
+}
 
-  return $id ? SL::DB::RequirementSpec->new(id => $id)->load : undef;
+sub highest_version {
+  my ($self) = @_;
+
+  return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies };
 }
 
 sub is_working_copy {
@@ -177,14 +177,8 @@ sub is_working_copy {
 
 sub next_version_number {
   my ($self) = @_;
-  my $max_number = $self->db->dbh->selectrow_array(<<SQL, undef, $self->id);
-    SELECT COALESCE(MAX(ver.version_number), 0)
-    FROM requirement_spec_versions ver
-    JOIN requirement_specs rs ON (rs.version_id = ver.id)
-    WHERE rs.working_copy_id = ?
-SQL
-
-  return $max_number + 1;
+
+  return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
 }
 
 sub create_version {
@@ -193,7 +187,7 @@ sub create_version {
   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
   my ($copy, $version);
-  my $ok = $self->db->do_transaction(sub {
+  my $ok = $self->db->with_transaction(sub {
     delete $attributes{version_number};
 
     $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
@@ -217,3 +211,232 @@ sub invalidate_version {
 }
 
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::RequirementSpec - RDBO model for requirement specs
+
+=head1 OVERVIEW
+
+The database structure behind requirement specs is a bit involved. The
+important thing is how working copy/versions are handled.
+
+The table contains three important columns: C<id> (which is also the
+primary key), C<working_copy_id> and C<version_id>. C<working_copy_id>
+is a self-referencing column: it can be C<NULL>, but if it isn't then
+it contains another requirement spec C<id>. C<version_id> on the other
+hand references the table C<requirement_spec_versions>.
+
+The design is as follows:
+
+=over 2
+
+=item * The user is always working on a working copy. The working copy
+is identified in the database by having C<working_copy_id> set to
+C<NULL>.
+
+=item * All other entries in this table are referred to as I<versioned
+copies>. A versioned copy is a copy of a working frozen at the moment
+in time it was created. Each versioned copy refers back to the working
+copy it belongs to: each has its C<working_copy_id> set.
+
+=item * Each versioned copy must reference an entry in the table
+C<requirement_spec_versions>. Meaning: for each versioned copy
+C<version_id> must not be C<NULL>.
+
+=item * Directly after creating a versioned copy even the working copy
+itself points to a certain version via its C<version_id> column: to
+the same version that the versioned copy just created points
+to. However, any modification that will be visible to the customer
+(text, positioning etc but not internal things like time/cost
+estimation changes) will cause the working copy to be set to 'no
+version' again. This is achieved via before save hooks in Perl.
+
+=back
+
+=head1 DATABASE TRIGGERS AND CHECKS
+
+Several database triggers and consistency checks exist that manage
+requirement specs, their items and their dependencies. These are
+described here instead of in the individual files for the other RDBO
+models.
+
+=head2 DELETION
+
+When you delete a requirement spec all of its dependencies (items,
+text blocks, versions etc.) are deleted by triggers.
+
+When you delete an item (either a section or a (sub-)function block)
+all of its children will be deleted as well. This will trigger the
+same trigger resulting in a recursive deletion with the bottom-most
+items being deleted first. Their item dependencies are deleted as
+well.
+
+=head2 UPDATING
+
+Whenever you update a requirement spec item a trigger will fire that
+will update the parent's C<time_estimation> column. This also happens
+when an item is deleted or updated.
+
+=head2 CONSISTENCY CHECKS
+
+Several consistency checks are applied to requirement spec items:
+
+=over 2
+
+=item * Column C<requirement_spec_item.item_type> can only contain one of
+the values C<section>, C<function-block> or C<sub-function-block>.
+
+=item * Column C<requirement_spec_item.parent_id> must be C<NULL> if
+C<requirement_spec_item.item_type> is set to C<section> and C<NOT
+NULL> otherwise.
+
+=back
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<copy_from $source, %attributes>
+
+Copies everything (basic attributes like type/title/customer, items,
+text blocks, time/cost estimation) save for the versions from the
+other requirement spec object C<$source> into C<$self> and saves
+it. This is done within a transaction.
+
+C<%attributes> are attributes that are assigned to C<$self> after all
+the basic attributes from C<$source> have been assigned.
+
+This function can be used for resetting a working copy to a specific
+version. Example:
+
+ my $requirement_spec = SL::DB::RequirementSpec->new(id => $::form->{id})->load;
+ my $versioned_copy   = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
+
+  $requirement_spec->copy_from(
+    $versioned_copy,
+    version_id => $versioned_copy->version_id,
+  );
+
+=item C<create_copy>
+
+Creates and returns a copy of C<$self>. The copy is already
+saved. Creating the copy happens within a transaction.
+
+=item C<create_version %attributes>
+
+Prerequisites: C<$self> must be a working copy (see L</working_copy>),
+not a versioned copy.
+
+This function creates a new version for C<$self>. This involves
+several steps:
+
+=over 2
+
+=item 1. The next version number is calculated using
+L</next_version_number>.
+
+=item 2. An instance of L<SL::DB::RequirementSpecVersion> is
+created. Its attributes are copied from C<%attributes> save for the
+version number which is taken from step 1.
+
+=item 3. A copy of C<$self> is created with L</create_copy>.
+
+=item 4. The version instance created in step is assigned to the copy
+from step 3.
+
+=item 5. The C<version_id> in C<$self> is set to the copy's ID from
+step 3.
+
+=back
+
+All this is done within a transaction.
+
+In case of success a two-element list is returned consisting of the
+copy & version objects created in steps 3 and 2 respectively. In case
+of a failure an empty list will be returned.
+
+=item C<displayable_name>
+
+Returns a human-readable name for this instance consisting of the type
+and the title.
+
+=item C<highest_version>
+
+Given a working copy C<$self> this function returns the versioned copy
+of C<$self> with the highest version number. If such a version exist
+its instance is returned. Otherwise C<undef> is returned.
+
+This can be used for calculating the difference between the working
+copy and the last version created for it.
+
+=item C<invalidate_version>
+
+Prerequisites: C<$self> must be a working copy (see L</working_copy>),
+not a versioned copy.
+
+Sets the C<version_id> field to C<undef> and saves C<$self>.
+
+=item C<is_working_copy>
+
+Returns trueish if C<$self> is a working copy and not a versioned
+copy. The condition for this is that C<working_copy_id> is C<undef>.
+
+=item C<next_version_number>
+
+Calculates and returns the next version number for this requirement
+spec. Version numbers start at 1 and are incremented by one for each
+version created for it, no matter whether or not it has been reverted
+to a previous version since. It boils down to this pseudo-code:
+
+  if (has_never_had_a_version)
+    return 1
+  else
+    return max(version_number for all versions for this requirement spec) + 1
+
+=item C<sections>
+
+An alias for L</sections_sorted>.
+
+=item C<sections_sorted>
+
+Returns a list of requirement spec items that
+
+This is not a writer. Use the C<items> relationship for that.
+
+=item C<text_blocks_sorted %params>
+
+Returns an array (or an array reference depending on context) of text
+blocks sorted by their positional column in ascending order. If the
+C<output_position> parameter is given then only the text blocks
+belonging to that C<output_position> are returned.
+
+=item C<validate>
+
+Validate values before saving. Returns list or human-readable error
+messages (if any).
+
+=item C<versioned_copies_sorted %params>
+
+Returns an array (or an array reference depending on context) of
+versioned copies sorted by their version number in ascending order. If
+the C<max_version_number> parameter is given then only the versioned
+copies whose version number is less than or equal to
+C<max_version_number> are returned.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 641982c..5e7d46c 100644 (file)
@@ -126,3 +126,52 @@ sub child_type {
 }
 
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::RequirementSpecItem - Items for requirement specs
+
+=head1 OVERVIEW
+
+Please see L<SL::DB::RequirementSpec> for the architectual overview.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<child_type>
+
+Returns the C<item_type> for children of C<$self>.
+
+=item C<children_sorted>
+
+Returns an array (or an array reference depending on context) of
+direct children (not of grandchildren) for C<$self> ordered by their
+positional column in ascending order.
+
+=item C<section>
+
+Returns the section this item belongs to. It can be C<$self> if
+C<$self> is already a section, its parent or grandparent.
+
+=item C<validate>
+
+Validates before saving and returns an array of human-readable error
+messages in case of an error.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut