e97d2252b6effc7a020ac2d98390af750144387b
[kivitendo-erp.git] / SL / Controller / RequirementSpecVersion.pm
1 package SL::Controller::RequirementSpecVersion;
2
3 use strict;
4
5 use parent qw(SL::Controller::Base);
6
7 use Carp;
8 use List::MoreUtils qw(any);
9
10 use SL::ClientJS;
11 use SL::DB::Customer;
12 use SL::DB::Project;
13 use SL::DB::RequirementSpec;
14 use SL::DB::RequirementSpecVersion;
15 use SL::Helper::Flash;
16 use SL::Locale::String;
17
18 use Rose::Object::MakeMethods::Generic
19 (
20   'scalar --get_set_init' => [ qw(requirement_spec version js) ],
21 );
22
23 __PACKAGE__->run_before('check_auth');
24
25 #
26 # actions
27 #
28
29 sub action_list {
30   my ($self, %params) = @_;
31
32   $self->render('requirement_spec_version/list', { layout => 0 });
33 }
34
35 sub action_new {
36   my ($self) = @_;
37
38   $self->version(SL::DB::RequirementSpecVersion->new);
39
40   my $previous_version = $self->requirement_spec->highest_version;
41
42   if (!$previous_version) {
43     $self->version->description(t8('Initial version.'));
44
45   } else {
46     my %differences = $self->calculate_differences(current => $self->requirement_spec, previous => $previous_version);
47
48     my @lines;
49
50     my $fb_diff = $differences{function_blocks};
51     push @lines, t8('Added sections and function blocks: #1',   $::locale->language_join([ map { $_->fb_number         } @{ $fb_diff->{additions} } ])) if @{ $fb_diff->{additions} };
52     push @lines, t8('Changed sections and function blocks: #1', $::locale->language_join([ map { $_->fb_number         } @{ $fb_diff->{changes}   } ])) if @{ $fb_diff->{changes}   };
53     push @lines, t8('Removed sections and function blocks: #1', $::locale->language_join([ map { $_->fb_number         } @{ $fb_diff->{removals}  } ])) if @{ $fb_diff->{removals}  };
54
55     my $tb_diff = $differences{text_blocks};
56     push @lines, t8('Added text blocks: #1',                    $::locale->language_join([ map { '"' . $_->title . '"' } @{ $tb_diff->{additions} } ])) if @{ $tb_diff->{additions} };
57     push @lines, t8('Changed text blocks: #1',                  $::locale->language_join([ map { '"' . $_->title . '"' } @{ $tb_diff->{changes}   } ])) if @{ $tb_diff->{changes}   };
58     push @lines, t8('Removed text blocks: #1',                  $::locale->language_join([ map { '"' . $_->title . '"' } @{ $tb_diff->{removals}  } ])) if @{ $tb_diff->{removals}  };
59
60     $self->version->description(@lines ? join("\n", @lines) : t8('No changes since previous version.'));
61   }
62
63   $self->render('requirement_spec_version/new', { layout => 0 });
64 }
65
66 sub action_create {
67   my ($self, %params) = @_;
68
69   my %attributes = %{ delete($::form->{rs_version}) || {} };
70   my @errors     = SL::DB::RequirementSpecVersion->new(%attributes, version_number => 1)->validate;
71
72   return $self->js->error(@errors)->render($self) if @errors;
73
74   my $db     = $self->requirement_spec->db;
75   my @result = $self->requirement_spec->create_version(%attributes);
76
77   if (!@result) {
78     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
79     return $self->js->error($::locale->text('Saving failed. Error message from the database: #1'), $db->error)->render($self);
80   }
81
82   $self->version($result[0]);
83   my $version_info_html = $self->render('requirement_spec/_version',     { output => 0 }, requirement_spec => $self->requirement_spec);
84   my $version_list_html = $self->render('requirement_spec_version/list', { output => 0 });
85
86   $self->js
87     ->html('#requirement_spec_version', $version_info_html)
88     ->html('#versioned_copies_list',    $version_list_html)
89     ->dialog->close('#jqueryui_popup_dialog')
90     ->render($self);
91 }
92
93 #
94 # filters
95 #
96
97 sub check_auth {
98   my ($self, %params) = @_;
99   $::auth->assert('requirement_spec_edit');
100 }
101
102 #
103 # helpers
104 #
105
106 sub init_requirement_spec {
107   my ($self) = @_;
108   $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{requirement_spec_id})->load) if $::form->{requirement_spec_id};
109 }
110
111 sub init_version {
112   my ($self) = @_;
113   $self->version(SL::DB::RequirementSpecVersion->new(id => $::form->{id})->load) if $::form->{id};
114 }
115
116 sub init_js {
117   my ($self, %params) = @_;
118   $self->js(SL::ClientJS->new);
119 }
120
121 sub has_item_changed {
122   my ($previous, $current) = @_;
123   croak "Missing previous/current" if !$previous || !$current;
124
125   return 1 if any { ($previous->$_ || '') ne ($current->$_ || '') } qw(item_type fb_number title description complexity_id risk_id);
126   return 0 if !$current->parent_id;
127   return $previous->parent->fb_number ne $current->parent->fb_number;
128 }
129
130 sub has_text_block_changed {
131   my ($previous, $current) = @_;
132   croak "Missing previous/current" if !$previous || !$current;
133   return any { ($previous->$_ || '') ne ($current->$_ || '') } qw(title text);
134 }
135
136 sub compare_items {
137   return -1 if ($a->item_type eq 'section') && ($b->item_type ne 'section');
138   return +1 if ($a->item_type ne 'section') && ($b->item_type eq 'section');
139   return $a->fb_number cmp $b->fb_number;
140 }
141
142 sub calculate_differences {
143   my ($self, %params) = @_;
144
145   my %differences = (
146     function_blocks => {
147       additions => [],
148       changes   => [],
149       removals  => [],
150     },
151     text_blocks => {
152       additions => [],
153       changes   => [],
154       removals  => [],
155     },
156   );
157
158   return %differences if !$params{previous} || !$params{current};
159
160   my @previous_items                         = sort compare_items @{ $params{previous}->items };
161   my @current_items                          = sort compare_items @{ $params{current}->items  };
162
163   my @previous_text_blocks                   = sort { lc $a->title cmp lc $b->title } @{ $params{previous}->text_blocks };
164   my @current_text_blocks                    = sort { lc $a->title cmp lc $b->title } @{ $params{current}->text_blocks  };
165
166   my %previous_items_map                     = map { $_->fb_number => $_ } @previous_items;
167   my %current_items_map                      = map { $_->fb_number => $_ } @current_items;
168
169   my %previous_text_blocks_map               = map { $_->title     => $_ } @previous_text_blocks;
170   my %current_text_blocks_map                = map { $_->title     => $_ } @current_text_blocks;
171
172   $differences{function_blocks}->{additions} = [ grep { !$previous_items_map{ $_->fb_number }                                                                         } @current_items        ];
173   $differences{function_blocks}->{removals}  = [ grep { !$current_items_map{  $_->fb_number }                                                                         } @previous_items       ];
174   $differences{function_blocks}->{changes}   = [ grep {  $previous_items_map{ $_->fb_number }       && has_item_changed($previous_items_map{ $_->fb_number }, $_)     } @current_items        ];
175
176   $differences{text_blocks}->{additions}     = [ grep { !$previous_text_blocks_map{ $_->title }                                                                       } @current_text_blocks  ];
177   $differences{text_blocks}->{removals}      = [ grep { !$current_text_blocks_map{  $_->title }                                                                       } @previous_text_blocks ];
178   $differences{text_blocks}->{changes}       = [ grep {  $previous_text_blocks_map{ $_->title } && has_text_block_changed($previous_text_blocks_map{ $_->title }, $_) } @current_text_blocks  ];
179
180   return %differences;
181 }
182
183 1;