X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FDB%2FRequirementSpec.pm;h=024e60d7b58b8fc633dbf5b4a60d1f25a16ec404;hb=53593baa211863fbf66540cf1bcc36c8fb37257f;hp=31dcfe89d3c8445935da538ac73943d262d84cff;hpb=b2e1809f6b6d8852ab0086dcc61ae9540c18b83f;p=kivitendo-erp.git diff --git a/SL/DB/RequirementSpec.pm b/SL/DB/RequirementSpec.pm index 31dcfe89d..024e60d7b 100644 --- a/SL/DB/RequirementSpec.pm +++ b/SL/DB/RequirementSpec.pm @@ -2,25 +2,63 @@ package SL::DB::RequirementSpec; use strict; +use Carp; +use List::Util qw(max reduce); +use Rose::DB::Object::Helpers; + use SL::DB::MetaSetup::RequirementSpec; use SL::DB::Manager::RequirementSpec; +use SL::DB::Helper::AttrDuration; +use SL::DB::Helper::CustomVariables ( + module => 'RequirementSpecs', + cvars_alias => 1, +); +use SL::DB::Helper::LinkedRecords; use SL::Locale::String; +use SL::Util qw(_hashify); __PACKAGE__->meta->add_relationship( - items => { - type => 'one to many', - class => 'SL::DB::RequirementSpecItem', - column_map => { id => 'requirement_spec_id' }, + items => { + type => 'one to many', + class => 'SL::DB::RequirementSpecItem', + column_map => { id => 'requirement_spec_id' }, + }, + text_blocks => { + type => 'one to many', + class => 'SL::DB::RequirementSpecTextBlock', + column_map => { id => 'requirement_spec_id' }, + }, + versioned_copies => { + type => 'one to many', + 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' }, }, - text_blocks => { - type => 'one to many', - class => 'SL::DB::RequirementSpecTextBlock', - column_map => { id => 'requirement_spec_id' }, + orders => { + type => 'one to many', + class => 'SL::DB::RequirementSpecOrder', + column_map => { id => 'requirement_spec_id' }, + }, + parts => { + type => 'one to many', + class => 'SL::DB::RequirementSpecPart', + column_map => { id => 'requirement_spec_id' }, }, ); __PACKAGE__->meta->initialize; +__PACKAGE__->attr_duration(qw(time_estimation)); + __PACKAGE__->before_save('_before_save_initialize_not_null_columns'); sub validate { @@ -35,10 +73,507 @@ sub validate { sub _before_save_initialize_not_null_columns { my ($self) = @_; - $self->previous_section_number(0) if !defined $self->previous_section_number; - $self->previous_fb_number(0) if !defined $self->previous_fb_number; + for (qw(previous_section_number previous_fb_number previous_picture_number)) { + $self->$_(0) if !defined $self->$_; + } 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, @_); + + my @text_blocks = @{ $self->text_blocks }; + @text_blocks = grep { $_->output_position == $params{output_position} } @text_blocks if exists $params{output_position}; + @text_blocks = sort { $a->position <=> $b->position } @text_blocks; + + return \@text_blocks; +} + +sub sections_sorted { + my ($self, @rest) = @_; + + croak "This sub is not a writer" if @rest; + + return [ sort { $a->position <=> $b->position } grep { !$_->parent_id } @{ $self->items } ]; +} + +sub sections { §ions_sorted; } + +sub orders_sorted { + my ($self, %params) = _hashify(1, @_); + my $by = $params{by} || 'itime'; + + return [ sort { $a->$by cmp $b->$by } @{ $self->orders } ]; +} + +sub displayable_name { + my ($self) = @_; + + return sprintf('%s: "%s"', $self->type->description, $self->title); +} + +sub versioned_copies_sorted { + my ($self, %params) = _hashify(1, @_); + + my @copies = @{ $self->versioned_copies }; + @copies = grep { $_->version->version_number <= $params{max_version_number} } @copies if $params{max_version_number}; + @copies = sort { $a->version->version_number <=> $b->version->version_number } @copies; + + return \@copies; +} + +sub parts_sorted { + my ($self, @rest) = @_; + + croak "This sub is not a writer" if @rest; + + return [ sort { $a->position <=> $b->position } @{ $self->parts } ]; +} + +sub create_copy { + my ($self, %params) = @_; + + return $self->_create_copy(%params) if $self->db->in_transaction; + + my $copy; + if (!$self->db->with_transaction(sub { $copy = $self->_create_copy(%params) })) { + $::lxdebug->message(LXDebug->WARN(), "create_copy failed: " . join("\n", (split(/\n/, $self->db->error))[0..2])); + return undef; + } + + return $copy; +} + +sub _create_copy { + my ($self, %params) = @_; + + my $copy = $self->clone_and_reset; + $copy->copy_from($self, %params); + + return $copy; +} + +sub _copy_from { + my ($self, $params, %attributes) = @_; + + my $source = $params->{source}; + + croak "Missing parameter 'source'" unless $source; + + # Copy attributes. + if (!$params->{paste_template}) { + $self->assign_attributes(map({ ($_ => $source->$_) } qw(type_id status_id customer_id project_id title hourly_rate time_estimation previous_section_number previous_fb_number previous_picture_number is_template)), + %attributes); + } + + # Copy custom variables. + foreach my $var (@{ $source->cvars_by_config }) { + $self->cvar_by_name($var->config->name)->value($var->value); + } + + my %paste_template_result; + + # Clone text blocks and pictures. + my $clone_and_reset_position = sub { + my ($src_obj) = @_; + my $cloned = $src_obj->clone_and_reset; + $cloned->position(undef); + return $cloned; + }; + + my $clone_text_block = sub { + my ($text_block) = @_; + my $cloned = $text_block->clone_and_reset; + $cloned->position(undef); + $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]); + return $cloned; + }; + + $paste_template_result{text_blocks} = [ map { $clone_text_block->($_) } @{ $source->text_blocks_sorted } ]; + + if (!$params->{paste_template}) { + $self->text_blocks($paste_template_result{text_blocks}); + } else { + $self->add_text_blocks($paste_template_result{text_blocks}); + } + + # Clone additional parts. + $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ]; + my $accessor = $params->{paste_template} ? "add_parts" : "parts"; + $self->$accessor($paste_template_result{parts}); + + # Save new object -- we need its ID for the items. + $self->save(cascade => 1); + + my %id_to_clone; + + # Clone items. + my $clone_item; + $clone_item = sub { + my ($item) = @_; + my $cloned = $item->clone_and_reset; + $cloned->requirement_spec_id($self->id); + $cloned->position(undef); + $cloned->fb_number(undef) if $params->{paste_template}; + $cloned->children(map { $clone_item->($_) } @{ $item->children }); + + $id_to_clone{ $item->id } = $cloned; + + return $cloned; + }; + + $paste_template_result{sections} = [ map { $clone_item->($_) } @{ $source->sections_sorted } ]; + + if (!$params->{paste_template}) { + $self->items($paste_template_result{sections}); + } else { + $self->add_items($paste_template_result{sections}); + } + + # Save the items -- need to do that before setting dependencies. + $self->save; + + # Set dependencies. + foreach my $item (@{ $source->items }) { + next unless @{ $item->dependencies }; + $id_to_clone{ $item->id }->update_attributes(dependencies => [ map { $id_to_clone{$_->id} } @{ $item->dependencies } ]); + } + + $self->update_attributes(%attributes) unless $params->{paste_template}; + + return %paste_template_result; +} + +sub copy_from { + my ($self, $source, %attributes) = @_; + + $self->db->with_transaction(sub { $self->_copy_from({ source => $source, paste_template => 0 }, %attributes); }); +} + +sub paste_template { + my ($self, $template) = @_; + + $self->db->with_transaction(sub { $self->_copy_from({ source => $template, paste_template => 1 }); }); +} + +sub highest_version { + my ($self) = @_; + + return reduce { $a->version->version_number > $b->version->version_number ? $a : $b } @{ $self->versioned_copies }; +} + +sub is_working_copy { + my ($self) = @_; + + return !$self->working_copy_id; +} + +sub next_version_number { + my ($self) = @_; + + 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 { + my ($self, %attributes) = @_; + + croak "Cannot work on a versioned copy" if $self->working_copy_id; + + my ($copy, $version); + my $ok = $self->db->with_transaction(sub { + delete $attributes{version_number}; + + 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; + }); + + return $ok ? ($copy, $version) : (); +} + +sub invalidate_version { + my ($self, %params) = @_; + + croak "Cannot work on a versioned copy" if $self->working_copy_id; + + return if !$self->id; + + SL::DB::Manager::RequirementSpecVersion->update_all( + set => [ working_copy_id => undef ], + where => [ working_copy_id => $self->id ], + ); +} + +sub compare_to { + my ($self, $other) = @_; + + return $self->id <=> $other->id; +} + 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 (which is also the +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: + +=over 2 + +=item * The user is always working on a working copy. The working copy +is identified in the database by having C set to +C. + +=item * All other entries in this table are referred to as I. 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 be referenced from an entry in the +table C via +C. + +=item * Directly after creating a versioned copy even the working copy +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 + +=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 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 can only contain one of +the values C
, C or C. + +=item * Column C must be C if +C is set to C
and C otherwise. + +=back + +=head1 FUNCTIONS + +=over 4 + +=item C + +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); + $versioned_copy->version->update_attributes(working_copy_id => $requirement_spec->id); + +=item C + +Creates and returns a copy of C<$self>. The copy is already +saved. Creating the copy happens within a transaction. + +=item C + +Prerequisites: C<$self> must be a working copy (see the overview), +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. + +=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 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 + +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 + +Returns a human-readable name for this instance consisting of the type +and the title. + +=item C + +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 is returned. + +This can be used for calculating the difference between the working +copy and the last version created for it. + +=item C + +Prerequisites: C<$self> must be a working copy (see the overview), +not a versioned copy. + +Sets any C field in the C +table containing C<$self-Eid> to C. + +=item C + +Returns trueish if C<$self> is a working copy and not a versioned +copy. The condition for this is that C is C. + +=item C + +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 + +An alias for L. + +=item C + +Returns an array reference of requirement spec items that do not have +a parent -- meaning that are sections. + +This is not a writer. Use the C relationship for that. + +=item C + +Returns an array reference of text blocks sorted by their positional +column in ascending order. If the C parameter is +given then only the text blocks belonging to that C +are returned. + +=item C + +Returns an array reference of additional parts sorted by their +positional column in ascending order. + +=item C + +Validate values before saving. Returns list or human-readable error +messages (if any). + +=item C + +Returns an array reference of versioned copies sorted by their version +number in ascending order. If the C parameter is +given then only the versioned copies whose version number is less than +or equal to C are returned. + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Moritz Bunkus Em.bunkus@linet-services.deE + +=cut