From: Moritz Bunkus Date: Wed, 7 Aug 2013 17:10:15 +0000 (+0200) Subject: Pflichtenheftversionen: Datenbankstruktur zu Pflichtenheften geändert X-Git-Tag: release-3.2.0beta~467^2~80 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=13fbd33663c229ec309a858ac1d393fdd508ddbe;p=kivitendo-erp.git Pflichtenheftversionen: Datenbankstruktur zu Pflichtenheften geändert requirement_specs.version_id wurde durch requirement_spec_versions.requirement_spec_id und requirement_spec_versions.working_copy_id ersetzt. --- diff --git a/SL/Controller/RequirementSpec.pm b/SL/Controller/RequirementSpec.pm index 6afe99730..188162b14 100644 --- a/SL/Controller/RequirementSpec.pm +++ b/SL/Controller/RequirementSpec.pm @@ -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') } }, ); } diff --git a/SL/Controller/RequirementSpecVersion.pm b/SL/Controller/RequirementSpecVersion.pm index 9258698e8..e1d737ddb 100644 --- a/SL/Controller/RequirementSpecVersion.pm +++ b/SL/Controller/RequirementSpecVersion.pm @@ -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; diff --git a/SL/DB/MetaSetup/RequirementSpec.pm b/SL/DB/MetaSetup/RequirementSpec.pm index 797137afe..bb37ba6e3 100644 --- a/SL/DB/MetaSetup/RequirementSpec.pm +++ b/SL/DB/MetaSetup/RequirementSpec.pm @@ -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' }, diff --git a/SL/DB/MetaSetup/RequirementSpecVersion.pm b/SL/DB/MetaSetup/RequirementSpecVersion.pm index 3c285e30a..21e23b726 100644 --- a/SL/DB/MetaSetup/RequirementSpecVersion.pm +++ b/SL/DB/MetaSetup/RequirementSpecVersion.pm @@ -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; ; diff --git a/SL/DB/RequirementSpec.pm b/SL/DB/RequirementSpec.pm index 0ad61aa69..52ae4eca7 100644 --- a/SL/DB/RequirementSpec.pm +++ b/SL/DB/RequirementSpec.pm @@ -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(<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 (which is also the -primary key), C and C. C -is a self-referencing column: it can be C, but if it isn't then -it contains another requirement spec C. C on the other -hand references the table C. +primary key) and C. C is a +self-referencing column: it can be C, but if it isn't then it +contains another requirement spec C. + +Versions are represented similarly. The C +table has three important columns: C (the primary key), +C (references C and must +not be C) and C (references +C as well but can be +C). C 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 set. -=item * Each versioned copy must reference an entry in the table -C. Meaning: for each versioned copy -C must not be C. +=item * Each versioned copy must be referenced from an entry in the +table C via +C. =item * Directly after creating a versioned copy even the working copy -itself points to a certain version via its C 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 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 @@ -385,17 +428,15 @@ several steps: =item 1. The next version number is calculated using L. -=item 2. An instance of L is +=item 2. A copy of C<$self> is created with L. + +=item 3. An instance of L 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. - -=item 4. The version instance created in step is assigned to the copy -from step 3. - -=item 5. The C 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 and to the working +copy for which the version was created via C. =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 field to C and saves C<$self>. +Sets any C field in the C +table containing C<$self-Eid> to C. =item C diff --git a/SL/DB/RequirementSpecItem.pm b/SL/DB/RequirementSpecItem.pm index 35acefcb6..49d915845 100644 --- a/SL/DB/RequirementSpecItem.pm +++ b/SL/DB/RequirementSpecItem.pm @@ -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); diff --git a/SL/DB/RequirementSpecTextBlock.pm b/SL/DB/RequirementSpecTextBlock.pm index 50851ad78..832d3414f 100644 --- a/SL/DB/RequirementSpecTextBlock.pm +++ b/SL/DB/RequirementSpecTextBlock.pm @@ -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 index 000000000..4ce694caa --- /dev/null +++ b/sql/Pg-upgrade2/requirement_spec_delete_trigger_fix2.sql @@ -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; diff --git a/templates/print/Standard/requirement_spec.tex b/templates/print/Standard/requirement_spec.tex index 6580a8ea0..b36ad08be 100644 --- a/templates/print/Standard/requirement_spec.tex +++ b/templates/print/Standard/requirement_spec.tex @@ -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 )$ diff --git a/templates/webpages/requirement_spec/_version.html b/templates/webpages/requirement_spec/_version.html index f6d9ca35d..8eba53f02 100644 --- a/templates/webpages/requirement_spec/_version.html +++ b/templates/webpages/requirement_spec/_version.html @@ -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) %] diff --git a/templates/webpages/requirement_spec_version/list.html b/templates/webpages/requirement_spec_version/list.html index 7f6ab52b9..74ef179a7 100644 --- a/templates/webpages/requirement_spec_version/list.html +++ b/templates/webpages/requirement_spec_version/list.html @@ -12,8 +12,8 @@ - [%- 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) %] [%- LxERP.t8("Working copy identical to version number #1", SELF.requirement_spec.version.version_number) %] [%- ELSE %] [%- LxERP.t8("Working copy without version") %] @@ -23,13 +23,14 @@ [% SELF.requirement_spec.mtime.to_kivitendo(precision='minute') %] - [%- FOREACH versioned = SELF.versioned_copies %] + [%- FOREACH versioned_copy = SELF.requirement_spec.versioned_copies_sorted %] + [%- SET version = versioned_copy.version %] - [% L.hidden_tag('versioned_copy_id', versioned.id, no_id=1) %] - [% HTML.escape(versioned.version.version_number) %] - [% HTML.escape(P.truncate(versioned.description)) %] - [% HTML.escape(P.truncate(versioned.comment)) %] - [% versioned.mtime.to_kivitendo(precision='minute') %] + [% L.hidden_tag('versioned_copy_id', versioned_copy.id, no_id=1) %] + [% HTML.escape(version.version_number) %] + [% HTML.escape(P.truncate(version.description)) %] + [% HTML.escape(P.truncate(version.comment)) %] + [% version.itime.to_kivitendo(precision='minute') %] [%- END %]