Pflichtenheftversionen: Datenbankstruktur zu Pflichtenheften geändert
authorMoritz Bunkus <m.bunkus@linet-services.de>
Wed, 7 Aug 2013 17:10:15 +0000 (19:10 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Tue, 1 Apr 2014 11:09:10 +0000 (13:09 +0200)
requirement_specs.version_id wurde durch
requirement_spec_versions.requirement_spec_id und
requirement_spec_versions.working_copy_id ersetzt.

SL/Controller/RequirementSpec.pm
SL/Controller/RequirementSpecVersion.pm
SL/DB/MetaSetup/RequirementSpec.pm
SL/DB/MetaSetup/RequirementSpecVersion.pm
SL/DB/RequirementSpec.pm
SL/DB/RequirementSpecItem.pm
SL/DB/RequirementSpecTextBlock.pm
sql/Pg-upgrade2/requirement_spec_delete_trigger_fix2.sql [new file with mode: 0644]
templates/print/Standard/requirement_spec.tex
templates/webpages/requirement_spec/_version.html
templates/webpages/requirement_spec_version/list.html

index 6afe997..188162b 100644 (file)
@@ -192,10 +192,9 @@ sub action_revert_to {
 
   my $versioned_copy = SL::DB::RequirementSpec->new(id => $::form->{versioned_copy_id})->load;
 
-  $self->requirement_spec->copy_from(
-    $versioned_copy,
-    version_id => $versioned_copy->version_id,
-  );
+  $self->requirement_spec->copy_from($versioned_copy);
+  my $version = $versioned_copy->versions->[0];
+  $version->update_attributes(working_copy_id => $self->requirement_spec->id);
 
   flash_later('info', t8('The requirement spec has been reverted to version #1.', $self->requirement_spec->version->version_number));
   $self->js->redirect_to($self->url_for(action => 'show', id => $self->requirement_spec->id))->render($self);
@@ -410,7 +409,7 @@ sub prepare_report {
                          sub      => sub { $_[0]->project_id ? $_[0]->project->projectnumber : '' } },
       status        => { sub      => sub { $_[0]->status->description } },
       type          => { sub      => sub { $_[0]->type->description } },
-      version       => { sub      => sub { $_[0]->version_id ? $_[0]->version->version_number : t8('Working copy without version') } },
+      version       => { sub      => sub { $_[0]->version ? $_[0]->version->version_number : t8('Working copy without version') } },
     );
   }
 
index 9258698..e1d737d 100644 (file)
@@ -17,7 +17,7 @@ use SL::Locale::String;
 
 use Rose::Object::MakeMethods::Generic
 (
-  'scalar --get_set_init' => [ qw(requirement_spec version js versioned_copies) ],
+  'scalar --get_set_init' => [ qw(requirement_spec version js) ],
 );
 
 __PACKAGE__->run_before('check_auth');
@@ -117,13 +117,6 @@ sub init_js {
   $self->js(SL::ClientJS->new);
 }
 
-sub init_versioned_copies {
-  my ($self) = @_;
-  $self->versioned_copies([
-    sort { $b->mtime <=> $a->mtime } @{ $self->requirement_spec->versioned_copies }
-  ]);
-}
-
 sub has_item_changed {
   my ($previous, $current) = @_;
   croak "Missing previous/current" if !$previous || !$current;
index 797137a..bb37ba6 100644 (file)
@@ -22,7 +22,6 @@ __PACKAGE__->meta->columns(
   time_estimation         => { type => 'numeric', default => '0', not_null => 1, precision => 2, scale => 12 },
   title                   => { type => 'text', not_null => 1 },
   type_id                 => { type => 'integer' },
-  version_id              => { type => 'integer' },
   working_copy_id         => { type => 'integer' },
 );
 
@@ -51,11 +50,6 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { type_id => 'id' },
   },
 
-  version => {
-    class       => 'SL::DB::RequirementSpecVersion',
-    key_columns => { version_id => 'id' },
-  },
-
   working_copy => {
     class       => 'SL::DB::RequirementSpec',
     key_columns => { working_copy_id => 'id' },
index 3c285e3..21e23b7 100644 (file)
@@ -9,17 +9,31 @@ use base qw(SL::DB::Object);
 __PACKAGE__->meta->table('requirement_spec_versions');
 
 __PACKAGE__->meta->columns(
-  comment        => { type => 'text' },
-  description    => { type => 'text', not_null => 1 },
-  id             => { type => 'serial', not_null => 1 },
-  itime          => { type => 'timestamp', default => 'now()' },
-  mtime          => { type => 'timestamp' },
-  version_number => { type => 'integer' },
+  comment             => { type => 'text' },
+  description         => { type => 'text', not_null => 1 },
+  id                  => { type => 'serial', not_null => 1 },
+  itime               => { type => 'timestamp', default => 'now()' },
+  mtime               => { type => 'timestamp' },
+  requirement_spec_id => { type => 'integer', not_null => 1 },
+  version_number      => { type => 'integer' },
+  working_copy_id     => { type => 'integer' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
 __PACKAGE__->meta->allow_inline_column_values(1);
 
+__PACKAGE__->meta->foreign_keys(
+  requirement_spec => {
+    class       => 'SL::DB::RequirementSpec',
+    key_columns => { requirement_spec_id => 'id' },
+  },
+
+  working_copy => {
+    class       => 'SL::DB::RequirementSpec',
+    key_columns => { working_copy_id => 'id' },
+  },
+);
+
 1;
 ;
index 0ad61aa..52ae4ec 100644 (file)
@@ -27,6 +27,16 @@ __PACKAGE__->meta->add_relationship(
     class          => 'SL::DB::RequirementSpec',
     column_map     => { id => 'working_copy_id' },
   },
+  versions         => {
+    type           => 'one to many',
+    class          => 'SL::DB::RequirementSpecVersion',
+    column_map     => { id => 'requirement_spec_id' },
+  },
+  working_copy_versions => {
+    type           => 'one to many',
+    class          => 'SL::DB::RequirementSpecVersion',
+    column_map     => { id => 'working_copy_id' },
+  },
   orders           => {
     type           => 'one to many',
     class          => 'SL::DB::RequirementSpecOrder',
@@ -56,6 +66,14 @@ sub _before_save_initialize_not_null_columns {
   return 1;
 }
 
+sub version {
+  my ($self) = @_;
+
+  croak "Not a writer" if scalar(@_) > 1;
+
+  return $self->is_working_copy ? $self->working_copy_versions->[0] : $self->versions->[0];
+}
+
 sub text_blocks_sorted {
   my ($self, %params) = _hashify(1, @_);
 
@@ -222,7 +240,20 @@ sub is_working_copy {
 sub next_version_number {
   my ($self) = @_;
 
-  return max(0, map { $_->version->version_number } @{ $self->versioned_copies }) + 1;
+  return 1 if !$self->id;
+
+  my ($max_number) = $self->db->dbh->selectrow_array(<<SQL, {}, $self->id, $self->id);
+    SELECT MAX(v.version_number)
+    FROM requirement_spec_versions v
+    WHERE v.requirement_spec_id IN (
+      SELECT rs.id
+      FROM requirement_specs rs
+      WHERE (rs.id              = ?)
+         OR (rs.working_copy_id = ?)
+    )
+SQL
+
+  return ($max_number // 0) + 1;
 }
 
 sub create_version {
@@ -234,10 +265,13 @@ sub create_version {
   my $ok = $self->db->with_transaction(sub {
     delete $attributes{version_number};
 
-    $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number)->save;
-    $copy    = $self->create_copy;
-    $copy->update_attributes(version_id => $version->id, working_copy_id => $self->id);
-    $self->update_attributes(version_id => $version->id);
+    SL::DB::Manager::RequirementSpecVersion->update_all(
+      set   => [ working_copy_id     => undef     ],
+      where => [ requirement_spec_id => $self->id ],
+    );
+
+    $copy    = $self->create_copy(working_copy_id => $self->id);
+    $version = SL::DB::RequirementSpecVersion->new(%attributes, version_number => $self->next_version_number, requirement_spec_id => $copy->id, working_copy_id => $self->id)->save;
 
     1;
   });
@@ -250,8 +284,12 @@ sub invalidate_version {
 
   croak "Cannot work on a versioned copy" if $self->working_copy_id;
 
-  return if !$self->id || !$self->version_id;
-  $self->update_attributes(version_id => undef);
+  return if !$self->id;
+
+  SL::DB::Manager::RequirementSpecVersion->update_all(
+    set   => [ working_copy_id => undef     ],
+    where => [ working_copy_id => $self->id ],
+  );
 }
 
 1;
@@ -271,10 +309,17 @@ 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>.
+primary key) and C<working_copy_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>.
+
+Versions are represented similarly. The C<requirement_spec_versions>
+table has three important columns: C<id> (the primary key),
+C<requirement_spec_id> (references C<requirement_specs.id> and must
+not be C<NULL>) and C<working_copy_id> (references
+C<requirement_specs.id> as well but can be
+C<NULL>). C<working_copy_id> points to the working copy if and only if
+the working copy is currently equal to a versioned copy.
 
 The design is as follows:
 
@@ -289,17 +334,17 @@ 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 * Each versioned copy must be referenced from an entry in the
+table C<requirement_spec_versions> via
+C<requirement_spec_id>.
 
 =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.
+itself is referenced from a version via that table's
+C<working_copy_id> column. 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 version to be
+disassociated from the working copy. This is achieved via before save
+hooks in Perl.
 
 =back
 
@@ -359,13 +404,11 @@ 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;
 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,
-  );
+  $requirement_spec->copy_from($versioned_copy);
+  $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id);
 
 =item C<create_copy>
 
@@ -385,17 +428,15 @@ several steps:
 =item 1. The next version number is calculated using
 L</next_version_number>.
 
-=item 2. An instance of L<SL::DB::RequirementSpecVersion> is
+=item 2. A copy of C<$self> is created with L</create_copy>.
+
+=item 3. 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.
+=item 4. The version instance created in step 3 is referenced to the
+the copy from step 2 via C<requirement_spec_id> and to the working
+copy for which the version was created via C<working_copy_id>.
 
 =back
 
@@ -424,7 +465,8 @@ copy and the last version created for it.
 Prerequisites: C<$self> must be a working copy (see the overview),
 not a versioned copy.
 
-Sets the C<version_id> field to C<undef> and saves C<$self>.
+Sets any C<working_copy_id> field in the C<requirement_spec_versions>
+table containing C<$self-E<gt>id> to C<undef>.
 
 =item C<is_working_copy>
 
index 35acefc..49d9158 100644 (file)
@@ -66,7 +66,7 @@ sub _before_save_create_fb_number {
 sub _before_save_invalidate_requirement_spec_version {
   my ($self, %params) = @_;
 
-  return 1 if !$self->requirement_spec_id;
+  return 1 if !$self->requirement_spec_id || $self->requirement_spec->working_copy_id;
 
   my %changed_columns = map { $_ => 1 } (Rose::DB::Object::Helpers::dirty_columns($self));
   my $has_changed     = !Rose::DB::Object::Util::is_in_db($self);
index 50851ad..832d341 100644 (file)
@@ -30,8 +30,7 @@ sub validate {
 sub _before_save_invalidate_requirement_spec_version {
   my ($self, %params) = @_;
 
-
-  return 1 if !$self->requirement_spec_id;
+  return 1 if !$self->requirement_spec_id || $self->requirement_spec->working_copy_id;
 
   my %changed_columns = map { $_ => 1 } (Rose::DB::Object::Helpers::dirty_columns($self));
 
diff --git a/sql/Pg-upgrade2/requirement_spec_delete_trigger_fix2.sql b/sql/Pg-upgrade2/requirement_spec_delete_trigger_fix2.sql
new file mode 100644 (file)
index 0000000..4ce694c
--- /dev/null
@@ -0,0 +1,77 @@
+-- @tag: requirement_spec_delete_trigger_fix2
+-- @description: Fixes für Delete-Trigger bei Pflichtenheften
+-- @depends: requirement_spec_delete_trigger_fix
+
+-- requirement_spec_id: link to requirement specs (the versioned
+-- document) working_copy_id: link to requirement spec working copy
+-- (only set if working copy is currently at a version level)
+ALTER TABLE requirement_spec_versions ADD COLUMN requirement_spec_id INTEGER;
+ALTER TABLE requirement_spec_versions ADD COLUMN working_copy_id     INTEGER;
+
+UPDATE requirement_spec_versions ver
+SET requirement_spec_id = (
+  SELECT MAX(rs.id)
+  FROM requirement_specs rs
+  WHERE rs.version_id = ver.id
+);
+
+UPDATE requirement_spec_versions ver
+SET working_copy_id = (
+  SELECT rs.id
+  FROM requirement_specs rs
+  WHERE (rs.version_id = ver.id)
+    AND (rs.working_copy_id IS NULL)
+);
+
+ALTER TABLE requirement_spec_versions ALTER COLUMN requirement_spec_id SET NOT NULL;
+ALTER TABLE requirement_spec_versions ADD FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs (id) ON DELETE CASCADE;
+ALTER TABLE requirement_spec_versions ADD FOREIGN KEY (working_copy_id)     REFERENCES requirement_specs (id) ON DELETE CASCADE;
+
+ALTER TABLE requirement_specs DROP COLUMN version_id;
+ALTER TABLE requirement_specs DROP CONSTRAINT requirement_specs_working_copy_id_fkey;
+ALTER TABLE requirement_specs ADD FOREIGN KEY (working_copy_id) REFERENCES requirement_specs (id) ON DELETE CASCADE;
+
+ALTER TABLE requirement_spec_items DROP CONSTRAINT requirement_spec_items_requirement_spec_id_fkey;
+ALTER TABLE requirement_spec_items ADD FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs (id) ON DELETE CASCADE;
+
+ALTER TABLE requirement_spec_item_dependencies DROP CONSTRAINT requirement_spec_item_dependencies_depended_item_id_fkey;
+ALTER TABLE requirement_spec_item_dependencies ADD FOREIGN KEY (depended_item_id) REFERENCES requirement_spec_items (id) ON DELETE CASCADE;
+ALTER TABLE requirement_spec_item_dependencies DROP CONSTRAINT requirement_spec_item_dependencies_depending_item_id_fkey;
+ALTER TABLE requirement_spec_item_dependencies ADD FOREIGN KEY (depending_item_id) REFERENCES requirement_spec_items (id) ON DELETE CASCADE;
+
+ALTER TABLE requirement_spec_text_blocks DROP CONSTRAINT requirement_spec_text_blocks_requirement_spec_id_fkey;
+ALTER TABLE requirement_spec_text_blocks ADD FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs (id) ON DELETE CASCADE;
+
+-- Trigger for deleting depending stuff if a requirement spec is deleted.
+CREATE OR REPLACE FUNCTION requirement_spec_delete_trigger() RETURNS trigger AS $$
+  BEGIN
+    IF TG_WHEN = 'AFTER' THEN
+      DELETE FROM trigger_information WHERE (key = 'deleting_requirement_spec') AND (value = CAST(OLD.id AS TEXT));
+
+      RETURN OLD;
+    END IF;
+
+    RAISE DEBUG 'before delete trigger on %', OLD.id;
+
+    INSERT INTO trigger_information (key, value) VALUES ('deleting_requirement_spec', CAST(OLD.id AS TEXT));
+
+    RAISE DEBUG '  Converting items into sections items for %', OLD.id;
+    UPDATE requirement_spec_items SET item_type  = 'section', parent_id = NULL WHERE requirement_spec_id = OLD.id;
+
+    RAISE DEBUG '  And we out for %', OLD.id;
+
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
+
+-- Trigger for deleting depending stuff if a requirement spec item is deleted.
+CREATE OR REPLACE FUNCTION requirement_spec_item_before_delete_trigger() RETURNS trigger AS $$
+  BEGIN
+    RAISE DEBUG 'delete trig RSitem old id %', OLD.id;
+    INSERT INTO trigger_information (key, value) VALUES ('deleting_requirement_spec_item', CAST(OLD.id AS TEXT));
+    DELETE FROM requirement_spec_items WHERE (parent_id         = OLD.id);
+    DELETE FROM trigger_information    WHERE (key = 'deleting_requirement_spec_item') AND (value = CAST(OLD.id AS TEXT));
+    RAISE DEBUG 'delete trig END %', OLD.id;
+    RETURN OLD;
+  END;
+$$ LANGUAGE plpgsql;
index 6580a8e..b36ad08 100644 (file)
@@ -4,7 +4,7 @@ $( USE LxLatex )$
 $( USE P )$
 \documentclass{scrartcl}
 
-\usepackage[reqspeclogo,$( IF !rspec.version_id )$draftlogo$( ELSE )$secondpagelogo$( END )$]{kivitendo}
+\usepackage[reqspeclogo,$( IF !rspec.version )$draftlogo$( ELSE )$secondpagelogo$( END )$]{kivitendo}
 
 \kivitendobgsettings
 
@@ -15,7 +15,7 @@ $( USE P )$
   \parbox{12cm}{%
     \defaultfont\scriptsize%
     $( LxLatex.filter(rspec.displayable_name) )$\\
-    $( !rspec.version_id ? "Arbeitskopie ohne Version" : "Version " _ rspec.version.version_number _ " vom " _ rspec.version.itime.to_kivitendo(precision='minute') )$
+    $( !rspec.version ? "Arbeitskopie ohne Version" : "Version " _ rspec.version.version_number _ " vom " _ rspec.version.itime.to_kivitendo(precision='minute') )$
 
     \vspace*{0.2cm}%
     Seite \thepage%
@@ -44,7 +44,7 @@ $( USE P )$
       \Large
       $( LxLatex.filter(rspec.title) )$
       \normalsize
-%$( IF rspec.version_id )$
+%$( IF rspec.version )$
 
     Version $( LxLatex.filter(rspec.version.version_number) )$
 %$( END )$
@@ -66,7 +66,7 @@ $( USE P )$
 \vspace*{0.7cm}
 
 %$( SET working_copy     = rspec.working_copy_id ? rspec.working_copy : rspec )$
-%$( SET versioned_copies = rspec.version_id ? working_copy.versioned_copies_sorted(max_version_number = rspec.version.version_number) : working_copy.versioned_copies_sorted )$
+%$( SET versioned_copies = rspec.version ? working_copy.versioned_copies_sorted(max_version_number = rspec.version.version_number) : working_copy.versioned_copies_sorted )$
 %$( IF !versioned_copies.size )$
   Bisher wurden noch keine Versionen angelegt.
 %$( ELSE )$
index f6d9ca3..8eba53f 100644 (file)
@@ -1,7 +1,7 @@
 [%- USE L -%][%- USE LxERP -%][%- USE HTML -%]
-[% L.hidden_tag('current_version_id', requirement_spec.version_id) %]
+[% L.hidden_tag('current_version_id', requirement_spec.version.id) %]
 [% LxERP.t8("Current version") %]:
-[% IF !requirement_spec.version_id %]
+[% IF !requirement_spec.version.id %]
  [% LxERP.t8("Working copy without version") %]
 [% ELSE %]
  [% LxERP.t8("Version") %] [% HTML.escape(requirement_spec.version.version_number) %]
index 7f6ab52..74ef179 100644 (file)
@@ -12,8 +12,8 @@
 
  <tbody>
   <tr class="listrow versioned-copy-context-menu">
-   [%- IF SELF.requirement_spec.version_id %]
-    [% L.hidden_tag('versioned_copy_id', SELF.requirement_spec.version_id, no_id=1) %]
+   [%- IF SELF.requirement_spec.version %]
+    [% L.hidden_tag('versioned_copy_id', SELF.requirement_spec.version.requirement_spec_id, no_id=1) %]
     <td>[%- LxERP.t8("Working copy identical to version number #1", SELF.requirement_spec.version.version_number) %]</td>
    [%- ELSE %]
     <td>[%- LxERP.t8("Working copy without version") %]</td>
    <td>[% SELF.requirement_spec.mtime.to_kivitendo(precision='minute') %]</td>
   </tr>
 
-  [%- FOREACH versioned = SELF.versioned_copies %]
+  [%- FOREACH versioned_copy = SELF.requirement_spec.versioned_copies_sorted %]
+   [%- SET version = versioned_copy.version %]
    <tr class="listrow versioned-copy-context-menu">
-    [% L.hidden_tag('versioned_copy_id', versioned.id, no_id=1) %]
-    <td>[% HTML.escape(versioned.version.version_number) %]</td>
-    <td>[% HTML.escape(P.truncate(versioned.description)) %]</td>
-    <td>[% HTML.escape(P.truncate(versioned.comment)) %]</td>
-    <td>[% versioned.mtime.to_kivitendo(precision='minute') %]</td>
+    [% L.hidden_tag('versioned_copy_id', versioned_copy.id, no_id=1) %]
+    <td>[% HTML.escape(version.version_number) %]</td>
+    <td>[% HTML.escape(P.truncate(version.description)) %]</td>
+    <td>[% HTML.escape(P.truncate(version.comment)) %]</td>
+    <td>[% version.itime.to_kivitendo(precision='minute') %]</td>
    </tr>
   [%- END %]
  </tbody>