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);
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') } },
);
}
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');
$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;
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' },
);
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' },
__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;
;
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',
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, @_);
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 {
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;
});
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;
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:
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
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>
=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
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>
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);
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));
--- /dev/null
+-- @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;
$( 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
\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%
\Large
$( LxLatex.filter(rspec.title) )$
\normalsize
-%$( IF rspec.version_id )$
+%$( IF rspec.version )$
Version $( LxLatex.filter(rspec.version.version_number) )$
%$( END )$
\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 )$
[%- 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) %]
<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>