Revision c805dfd9
Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt
SL/Clipboard.pm | ||
---|---|---|
1 |
package SL::Clipboard; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use parent qw(Rose::Object); |
|
6 |
|
|
7 |
use Rose::Object::MakeMethods::Generic ( |
|
8 |
'scalar --get_set_init' => [ qw(content) ], |
|
9 |
); |
|
10 |
|
|
11 |
use Carp; |
|
12 |
use List::MoreUtils qw(apply); |
|
13 |
use List::Util qw(first); |
|
14 |
use Scalar::Util qw(blessed); |
|
15 |
|
|
16 |
use SL::Clipboard::RequirementSpecItem; |
|
17 |
use SL::Clipboard::RequirementSpecTextBlock; |
|
18 |
|
|
19 |
sub init_content { |
|
20 |
my $value = $::auth->get_session_value('clipboard-content'); |
|
21 |
return ref($value) eq 'HASH' ? $value : { entries => [] }; |
|
22 |
} |
|
23 |
|
|
24 |
sub copy { |
|
25 |
my ($self, $object) = @_; |
|
26 |
|
|
27 |
my $copied = $self->_create_copy_of($object); |
|
28 |
push @{ $self->content->{entries} }, $copied; |
|
29 |
|
|
30 |
$self->_save_content; |
|
31 |
|
|
32 |
return $copied; |
|
33 |
} |
|
34 |
|
|
35 |
sub get_entry { |
|
36 |
my ($self, $type) = @_; |
|
37 |
|
|
38 |
$type ||= qr/./; |
|
39 |
|
|
40 |
return first { $_->type =~ $type } |
|
41 |
reverse @{ $self->content->{entries} }; |
|
42 |
} |
|
43 |
|
|
44 |
sub get_entries { |
|
45 |
my ($self, $type) = @_; |
|
46 |
|
|
47 |
$type ||= qr/./; |
|
48 |
|
|
49 |
return grep { $_->{type} =~ $type } |
|
50 |
reverse @{ $self->content->{entries} }; |
|
51 |
} |
|
52 |
|
|
53 |
sub clear { |
|
54 |
my ($self) = @_; |
|
55 |
|
|
56 |
$self->content->{entries} = []; |
|
57 |
$self->_save_content; |
|
58 |
|
|
59 |
return $self; |
|
60 |
} |
|
61 |
|
|
62 |
sub _log_entries { |
|
63 |
my ($self) = @_; |
|
64 |
|
|
65 |
$::lxdebug->message(0, "Clipboard entries: " . scalar(@{ $self->content->{entries} })); |
|
66 |
foreach (@{ $self->content->{entries} }) { |
|
67 |
$::lxdebug->message(0, " " . $_->type . ' ' . $_->timestamp . ' ' . $_->describe); |
|
68 |
} |
|
69 |
} |
|
70 |
|
|
71 |
sub _create_copy_of { |
|
72 |
my ($self, $object) = @_; |
|
73 |
|
|
74 |
croak "\$object is not a blessed reference." unless blessed($object); |
|
75 |
|
|
76 |
my $type = (split(m/::/, ref($object)))[-1]; |
|
77 |
my $copied = eval { "SL::Clipboard::${type}"->new(timestamp => DateTime->now_local) }; |
|
78 |
|
|
79 |
croak "Class '" . ref($object) . "' not supported for copy/paste operations" if !$copied; |
|
80 |
|
|
81 |
$copied->content($copied->dump($object)); |
|
82 |
|
|
83 |
return $copied; |
|
84 |
} |
|
85 |
|
|
86 |
sub _save_content { |
|
87 |
my ($self) = @_; |
|
88 |
|
|
89 |
$::auth->set_session_value('clipboard-content', $self->content); |
|
90 |
|
|
91 |
return $self; |
|
92 |
} |
|
93 |
|
|
94 |
1; |
|
95 |
|
|
96 |
__END__ |
|
97 |
|
|
98 |
=pod |
|
99 |
|
|
100 |
=encoding utf8 |
|
101 |
|
|
102 |
=head1 NAME |
|
103 |
|
|
104 |
SL::Clipboard - A session-based clipboard mechanism for |
|
105 |
Rose::DB::Object instances |
|
106 |
|
|
107 |
=head1 SYNOPSIS |
|
108 |
|
|
109 |
# In a controller, e.g. for customers, you can react to a "copy" operation: |
|
110 |
my $customer = SL::DB::Customer->new(id => $::form->{id}); |
|
111 |
SL::Clipboard->new->copy($customer); |
|
112 |
|
|
113 |
# Later in a paste action: |
|
114 |
my $copied = SL::Clipboard->new->get_entry(qr/^Customer$/); |
|
115 |
if ($copied) { |
|
116 |
my $customer = $copied->to_object; |
|
117 |
$customer->save; |
|
118 |
} |
|
119 |
|
|
120 |
=head1 OVERVIEW |
|
121 |
|
|
122 |
The clipboard can store an unlimited number of copies of |
|
123 |
Rose::DB::Object instance. The instances are dumped into trees using |
|
124 |
L<Rose::DB::Object::Helpers/as_tree>. How much of such an object is |
|
125 |
copied depends on its type. For example, a dump of a customer object |
|
126 |
might also include the dumps of the shipping address and contact |
|
127 |
objects belonging to the customer. |
|
128 |
|
|
129 |
Each clipped object is stored in the user's session along with the |
|
130 |
timestamp of the copy operation. A controller can then query the |
|
131 |
clipboard for the latest clipped object of a certain type (or more |
|
132 |
types if the situation allows insertion of different types). If such a |
|
133 |
clipped object is available it can be turned into a shiny new |
|
134 |
Rose::DB::Object instance that can be saved to the database. |
|
135 |
|
|
136 |
Primary key columns will always be reset as will other columns |
|
137 |
depending on the type. For example, a copied requirement spec item |
|
138 |
will have its C<requirement_spec_id> column cleared. The controller is |
|
139 |
responsible for setting the columns before saving the object. |
|
140 |
|
|
141 |
Not every Rose::DB::Object instance can be copied. For each supported |
|
142 |
type C<Type> there must be a specialized clipboard support class |
|
143 |
C<SL::Clipboard::Type>. The type's name is derived from the Rose class |
|
144 |
name: by stripping the leading C<SL::DB::>. So the clipboard support |
|
145 |
class for a requirement spec item Rose class |
|
146 |
C<SL::DB::RequirementSpecItem> would be |
|
147 |
C<SL::Clipboard::RequirementSpecItem>. These support classes must |
|
148 |
inherit from L<SL::Clipboard::Base> which offers almost a full set of |
|
149 |
support functions so that the actual specialized class has to do very |
|
150 |
litte. |
|
151 |
|
|
152 |
As the clipboard is session-based its contents will be lost when the |
|
153 |
session expires (either due to timeouts or to the user logging off). |
|
154 |
|
|
155 |
=head1 FUNCTIONS |
|
156 |
|
|
157 |
=over 4 |
|
158 |
|
|
159 |
=item C<clear> |
|
160 |
|
|
161 |
Clears the clipboard (removes all entries). |
|
162 |
|
|
163 |
=item C<copy $object> |
|
164 |
|
|
165 |
Creates a dumped copy of C<$object> and stores that copy in the |
|
166 |
session. An unlimited number of copies of differeing types can be |
|
167 |
made. |
|
168 |
|
|
169 |
Returns the instance of the copied object, a sub-class of |
|
170 |
L<SL::Clipboard::Base>. |
|
171 |
|
|
172 |
=item C<get_entries [$type]> |
|
173 |
|
|
174 |
Returns an array of clipped objects whose type matches the regular |
|
175 |
expression C<$type>. If C<$type> is not given then all elements are |
|
176 |
returned. |
|
177 |
|
|
178 |
The array is sorted by the copy timestamp: the first element in the |
|
179 |
array is the one most recently copied. |
|
180 |
|
|
181 |
=item C<get_entry [$type]> |
|
182 |
|
|
183 |
Returns the most recently clipped object whose type matches the |
|
184 |
regular expression C<$type>. If C<$type> is not given then then then |
|
185 |
most recently copied object is returned. |
|
186 |
|
|
187 |
If no such object exists C<undef> is returned instead. |
|
188 |
|
|
189 |
=back |
|
190 |
|
|
191 |
=head1 BUGS |
|
192 |
|
|
193 |
Nothing here yet. |
|
194 |
|
|
195 |
=head1 AUTHOR |
|
196 |
|
|
197 |
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt> |
|
198 |
|
|
199 |
=cut |
SL/Clipboard/Base.pm | ||
---|---|---|
1 |
package SL::Clipboard::Base; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use parent qw(Rose::Object); |
|
6 |
|
|
7 |
use Rose::Object::MakeMethods::Generic ( |
|
8 |
'scalar --get_set_init' => [ qw(content timestamp) ], |
|
9 |
); |
|
10 |
|
|
11 |
use Rose::DB::Object::Helpers (); |
|
12 |
|
|
13 |
sub init_timestamp { die "'timestamp' property not set"; } |
|
14 |
sub init_content { die "'content' property not set"; } |
|
15 |
|
|
16 |
sub type { |
|
17 |
my ($self_or_class) = @_; |
|
18 |
return (split m/::/, ref($self_or_class) ? ref($self_or_class) : $self_or_class)[-1]; |
|
19 |
} |
|
20 |
|
|
21 |
sub reload_object { |
|
22 |
my ($self, $object) = @_; |
|
23 |
|
|
24 |
return ref($object)->new(map { $_ => $object->$_ } $object->meta->primary_key)->load; |
|
25 |
} |
|
26 |
|
|
27 |
sub as_tree { |
|
28 |
my ($self, $object, %params) = @_; |
|
29 |
|
|
30 |
my $tree = Rose::DB::Object::Helpers::as_tree($object, %params); |
|
31 |
$self->_fix_tree($tree, $object); |
|
32 |
return $tree; |
|
33 |
} |
|
34 |
|
|
35 |
sub to_object { |
|
36 |
my ($self) = @_; |
|
37 |
my $object = Rose::DB::Object::Helpers::new_from_tree("SL::DB::" . $self->type, $self->content); |
|
38 |
|
|
39 |
# Reset primary key columns and itime/mtime if the class supports it. |
|
40 |
foreach ($object->meta->primary_key, 'itime', 'mtime') { |
|
41 |
$object->$_(undef) if $object->can($_); |
|
42 |
} |
|
43 |
|
|
44 |
# Let sub classes fix the objects further. |
|
45 |
$self->_fix_object($object); |
|
46 |
return $object; |
|
47 |
} |
|
48 |
|
|
49 |
sub dump { |
|
50 |
my ($self, $object) = @_; |
|
51 |
return $self->as_tree($self->reload_object($object), max_depth => 1); |
|
52 |
} |
|
53 |
|
|
54 |
sub describe { |
|
55 |
die "'describe' method not overwritten by derived class"; |
|
56 |
} |
|
57 |
|
|
58 |
sub _fix_object { |
|
59 |
my ($self, $object) = @_; |
|
60 |
# To be overwritten by child classes. |
|
61 |
} |
|
62 |
|
|
63 |
sub _fix_tree { |
|
64 |
my ($self, $tree, $object) = @_; |
|
65 |
|
|
66 |
# Delete primary key columns and itime/mtime if the class supports it. |
|
67 |
foreach ($object->meta->primary_key, 'itime', 'mtime') { |
|
68 |
delete $tree->{$_} if $object->can($_); |
|
69 |
} |
|
70 |
} |
|
71 |
|
|
72 |
1; |
|
73 |
__END__ |
|
74 |
|
|
75 |
=pod |
|
76 |
|
|
77 |
=encoding utf8 |
|
78 |
|
|
79 |
=head1 NAME |
|
80 |
|
|
81 |
SL::Clipboard::Base - Base class for clipboard specialization classes |
|
82 |
|
|
83 |
=head1 SYNOPSIS |
|
84 |
|
|
85 |
See the synopsis of L<SL::Clipboard>. |
|
86 |
|
|
87 |
=head1 OVERVIEW |
|
88 |
|
|
89 |
This is a base class providing a lot of utility and |
|
90 |
defaults. Sub-classes must overwrite at least the function |
|
91 |
L</describe> but can overwrite others as well. |
|
92 |
|
|
93 |
Writing a specialized sub-class for a database type involves |
|
94 |
overwriting one or more functions. These are: |
|
95 |
|
|
96 |
=over 4 |
|
97 |
|
|
98 |
=item * C<describe> |
|
99 |
|
|
100 |
Must be overwritten. Returns a human-readable description of the |
|
101 |
content. Should only be one line. |
|
102 |
|
|
103 |
=item * C<dump> |
|
104 |
|
|
105 |
Optional. Overwrite if sub-class needs to dump more/less than the |
|
106 |
implementation in this class dumps. |
|
107 |
|
|
108 |
=item * C<_fix_object> |
|
109 |
|
|
110 |
Optional. Overwrite if re-created Rose::DB::Object instances must be |
|
111 |
cleaned further before they're returned to the caller. |
|
112 |
|
|
113 |
=item * C<_fix_tree> |
|
114 |
|
|
115 |
Optional. Overwrite if the tree created during a copy operation of a |
|
116 |
Rose::DB::Object instance must be cleaned further before it's stored. |
|
117 |
|
|
118 |
=back |
|
119 |
|
|
120 |
You don't have to or should not overwrite the other functions: |
|
121 |
|
|
122 |
=over 4 |
|
123 |
|
|
124 |
=item * C<as_tree> |
|
125 |
|
|
126 |
=item * C<reload_object> |
|
127 |
|
|
128 |
=item * C<to_object> |
|
129 |
|
|
130 |
=item * C<type> |
|
131 |
|
|
132 |
=back |
|
133 |
|
|
134 |
Don't forget to C<use> the specialized module here in Base! |
|
135 |
|
|
136 |
=head1 FUNCTIONS |
|
137 |
|
|
138 |
=over 4 |
|
139 |
|
|
140 |
=item C<as_tree $object, %params> |
|
141 |
|
|
142 |
A convenience function calling L<Rose::DB::Object::Helpers/as_tree> |
|
143 |
with C<$object> and C<%params> as parameters. Returns a hash/array |
|
144 |
reference tree of the function. |
|
145 |
|
|
146 |
Don't overwrite this function in sub-classes. Overwrite L</dump> |
|
147 |
instead. |
|
148 |
|
|
149 |
=item C<describe> |
|
150 |
|
|
151 |
Returns a human-readable description of the content. This should only |
|
152 |
be a single line without any markup. |
|
153 |
|
|
154 |
Sub-classes must overwrite this function. |
|
155 |
|
|
156 |
=item C<dump $object> |
|
157 |
|
|
158 |
Dumps the object as a hash/array tree and returns it by calling |
|
159 |
L<Rose::DB::Object::Helpers/as_tree>. The default implementation |
|
160 |
reloads the object first by calling L</reload_object>. It also only |
|
161 |
dumps the object itself, not any of the relationships, by calling |
|
162 |
C<as_tree> with the parameter C<max_depth =E<gt> 1>. |
|
163 |
|
|
164 |
Overwrite this in a sub-class if you need to dump more or differently |
|
165 |
(see L<SL::Clipboard::RequirementSpecItem> for an example). |
|
166 |
|
|
167 |
=item C<reload_object $object> |
|
168 |
|
|
169 |
Reloads C<$object> from the database and returns a new instance. Can |
|
170 |
be useful for sanitizing the object given to L</dump> before |
|
171 |
converting into a tree. It is used by the default implementation of |
|
172 |
L</dump>. |
|
173 |
|
|
174 |
=item C<to_object> |
|
175 |
|
|
176 |
Converts the dumped representation back to an Rose::DB::Object |
|
177 |
instance. Several columns of the newly created object are cleared by |
|
178 |
C<to_object> itself: the primary key columns (if any) and the columns |
|
179 |
C<itime> and C<mtime> (if the object has such columns). |
|
180 |
|
|
181 |
This function should not be overwritten by sub-classes. Instead, |
|
182 |
functions can overwrite C<_fix_object> which can be used for sanitzing |
|
183 |
the newly created object before handing it back to the caller. |
|
184 |
|
|
185 |
=item C<type> |
|
186 |
|
|
187 |
Returns the actual clipped type (e.g. C<RequirementSpecItem>). This is |
|
188 |
derived from the actual class name of C<$self>. |
|
189 |
|
|
190 |
=item C<_fix_object $object> |
|
191 |
|
|
192 |
This function is called by L</to_object> before the object is passed |
|
193 |
back to the caller. It does not do anything in the default |
|
194 |
implementation, but sub-classes are free to overwrite it if they need |
|
195 |
to sanitize the object. See L<SL::Clipboard::RequirementSpecItem> for |
|
196 |
an example. |
|
197 |
|
|
198 |
Its return value is ignored. |
|
199 |
|
|
200 |
=item C<_fix_tree $tree, $object> |
|
201 |
|
|
202 |
This function is called by L</as_tree> after dumping and before the |
|
203 |
object is stored during a copy operation. In the default |
|
204 |
implementation all primary key columns and the columns C<itime> and |
|
205 |
C<mtime> (if the object has such columns) are removed from the tree. |
|
206 |
Sub-classes are free to overwrite it if they need to sanitize the |
|
207 |
tree. See L<SL::Clipboard::RequirementSpecItem> for an example. |
|
208 |
|
|
209 |
C<$object> is just passed in for reference and should not be modified. |
|
210 |
|
|
211 |
Its return value is ignored. |
|
212 |
|
|
213 |
=back |
|
214 |
|
|
215 |
=head1 BUGS |
|
216 |
|
|
217 |
Nothing here yet. |
|
218 |
|
|
219 |
=head1 AUTHOR |
|
220 |
|
|
221 |
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt> |
|
222 |
|
|
223 |
=cut |
SL/Clipboard/RequirementSpecItem.pm | ||
---|---|---|
1 |
package SL::Clipboard::RequirementSpecItem; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use parent qw(SL::Clipboard::Base); |
|
6 |
|
|
7 |
use List::Util qw(sum); |
|
8 |
|
|
9 |
use SL::Common; |
|
10 |
use SL::Locale::String; |
|
11 |
|
|
12 |
sub dump { |
|
13 |
my ($self, $object) = @_; |
|
14 |
|
|
15 |
return $self->as_tree(_load_children($self->reload_object($object)), exclude => sub { ref($_[0]) !~ m/::RequirementSpecItem$/ }); |
|
16 |
} |
|
17 |
|
|
18 |
sub describe { |
|
19 |
my ($self) = @_; |
|
20 |
|
|
21 |
my $item = $self->content; |
|
22 |
my $num_children = @{ $item->{children} || [] }; |
|
23 |
my $num_grandchildren = sum map { scalar(@{ $_->{children} || [] }) } @{ $item->{children} || [] }; |
|
24 |
|
|
25 |
if ($item->{item_type} eq 'section') { |
|
26 |
return t8('Requirement spec section #1 "#2" with #3 function blocks and a total of #4 sub function blocks; preamble: "#5"', |
|
27 |
$item->{fb_number}, $item->{title}, $num_children, $num_grandchildren, Common::truncate($item->{description}, strip => 'full')); |
|
28 |
} elsif ($item->{item_type} eq 'function-block') { |
|
29 |
return t8('Requirement spec function block #1 with #2 sub function blocks; description: "#3"', |
|
30 |
$item->{fb_number}, $num_children, Common::truncate($item->{description}, strip => 'full')); |
|
31 |
} else { |
|
32 |
return t8('Requirement spec sub function block #1; description: "#2"', |
|
33 |
$item->{fb_number}, Common::truncate($item->{description}, strip => 'full')); |
|
34 |
} |
|
35 |
} |
|
36 |
|
|
37 |
sub _load_children { |
|
38 |
my ($object) = @_; |
|
39 |
|
|
40 |
_load_children($_) for @{ $object->children }; |
|
41 |
|
|
42 |
return $object; |
|
43 |
} |
|
44 |
|
|
45 |
sub _fix_object { |
|
46 |
my ($self, $object) = @_; |
|
47 |
|
|
48 |
$object->$_(undef) for qw(fb_number); |
|
49 |
$self->_fix_object($_) for @{ $object->children || [] }; |
|
50 |
} |
|
51 |
|
|
52 |
sub _fix_tree { |
|
53 |
my ($self, $tree, $object) = @_; |
|
54 |
|
|
55 |
delete @{ $tree }{ qw(id itime mtime parent_id position requirement_spec_id) }; |
|
56 |
$self->_fix_tree($_) for @{ $tree->{children} || [] }; |
|
57 |
} |
|
58 |
|
|
59 |
1; |
|
60 |
__END__ |
|
61 |
|
|
62 |
=pod |
|
63 |
|
|
64 |
=encoding utf8 |
|
65 |
|
|
66 |
=head1 NAME |
|
67 |
|
|
68 |
SL::Clipboard::RequirementSpecItem - Clipboard specialization for |
|
69 |
SL::DB::RequirementSpecItem |
|
70 |
|
|
71 |
=head1 FUNCTIONS |
|
72 |
|
|
73 |
=over 4 |
|
74 |
|
|
75 |
=item C<describe> |
|
76 |
|
|
77 |
Returns a human-readable description depending on the copied type |
|
78 |
(section, function block or sub function block). |
|
79 |
|
|
80 |
=item C<dump $object> |
|
81 |
|
|
82 |
This specialization reloads C<$object> from the database, loads all of |
|
83 |
its children (but only the other requirement spec items, no other |
|
84 |
relationships) and dumps it. |
|
85 |
|
|
86 |
=item C<_fix_object $object> |
|
87 |
|
|
88 |
Fixes C<$object> and all of its children by clearing certain columns |
|
89 |
like the position or function block numbers. |
|
90 |
|
|
91 |
=back |
|
92 |
|
|
93 |
=head1 BUGS |
|
94 |
|
|
95 |
Nothing here yet. |
|
96 |
|
|
97 |
=head1 AUTHOR |
|
98 |
|
|
99 |
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt> |
|
100 |
|
|
101 |
=cut |
SL/Clipboard/RequirementSpecTextBlock.pm | ||
---|---|---|
1 |
package SL::Clipboard::RequirementSpecTextBlock; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use parent qw(SL::Clipboard::Base); |
|
6 |
|
|
7 |
use SL::Common; |
|
8 |
use SL::Locale::String; |
|
9 |
|
|
10 |
sub describe { |
|
11 |
my ($self) = @_; |
|
12 |
|
|
13 |
return t8('Requirement spec text block "#1"; content: "#2"', $self->content->{title}, Common::truncate($self->content->{text}, strip => 'full')); |
|
14 |
} |
|
15 |
|
|
16 |
sub _fix_object { |
|
17 |
my ($self, $object) = @_; |
|
18 |
|
|
19 |
$object->$_(undef) for qw(output_position position requirement_spec_id); |
|
20 |
|
|
21 |
return $object; |
|
22 |
} |
|
23 |
|
|
24 |
1; |
|
25 |
__END__ |
|
26 |
|
|
27 |
=pod |
|
28 |
|
|
29 |
=encoding utf8 |
|
30 |
|
|
31 |
=head1 NAME |
|
32 |
|
|
33 |
SL::Clipboard::RequirementSpecTextBlock - Clipboard specialization for |
|
34 |
SL::DB::RequirementSpecTextBlock |
|
35 |
|
|
36 |
=head1 FUNCTIONS |
|
37 |
|
|
38 |
=over 4 |
|
39 |
|
|
40 |
=item C<describe> |
|
41 |
|
|
42 |
Returns a human-readable description including the title and an |
|
43 |
excerpt of its content. |
|
44 |
|
|
45 |
=item C<_fix_object $object> |
|
46 |
|
|
47 |
Fixes C<$object> by clearing certain columns like the position. |
|
48 |
|
|
49 |
=back |
|
50 |
|
|
51 |
=head1 BUGS |
|
52 |
|
|
53 |
Nothing here yet. |
|
54 |
|
|
55 |
=head1 AUTHOR |
|
56 |
|
|
57 |
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt> |
|
58 |
|
|
59 |
=cut |
Auch abrufbar als: Unified diff
Clipboard: eine erste allgemeine Implementation eines Clipboard-Mechanismus-Backends
Enthält bereits Spezialisierungen für Pflichtenhefttextblöcke und
-items (Abschnitte, Funktionsblöcke, Unterfunktionsblöcke).