Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 9cddaf37

Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt

  • ID 9cddaf376822b4229457212a27d5d98958f11368
  • Vorgänger 15fa34d9
  • Nachfolger d41162bc

Pflichtenhefte: Unterstützung für an Textblöcke angehängte Bilder

Unterschiede anzeigen:

SL/Controller/RequirementSpec.pm
5 5

  
6 6
use parent qw(SL::Controller::Base);
7 7

  
8
use File::Spec ();
9

  
8 10
use SL::ClientJS;
11
use SL::Common ();
9 12
use SL::Controller::Helper::GetModels;
10 13
use SL::Controller::Helper::Filtered;
11 14
use SL::Controller::Helper::Paginated;
......
208 211
  my ($self, %params) = @_;
209 212

  
210 213
  my $base_name       = $self->requirement_spec->type->template_file_name || 'requirement_spec';
214
  my @pictures        = $self->prepare_pictures_for_printing;
211 215
  my %result          = SL::Template::LaTeX->parse_and_create_pdf("${base_name}.tex", SELF => $self, rspec => $self->requirement_spec);
212 216

  
217
  unlink @pictures unless ($::lx_office_conf{debug} || {})->{keep_temp_files};
218

  
213 219
  $::form->error(t8('Conversion to PDF failed: #1', $result{error})) if $result{error};
214 220

  
215 221
  my $attachment_name  =  $self->requirement_spec->type->description . ' ' . ($self->requirement_spec->working_copy_id || $self->requirement_spec->id);
......
255 261

  
256 262
  $::auth->assert('sales_quotation_edit');
257 263
  $::request->{layout}->use_stylesheet("${_}.css") for qw(jquery.contextMenu requirement_spec);
258
  $::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu client_js requirement_spec);
264
  $::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu requirement_spec);
259 265
  $self->init_visible_section;
260 266

  
261 267
  return 1;
......
469 475
    ->jstree->select_node('#tree', '#fb-' . $section->id);
470 476
}
471 477

  
478
sub prepare_pictures_for_printing {
479
  my ($self) = @_;
480

  
481
  my @files;
482
  my $userspath = File::Spec->rel2abs($::lx_office_conf{paths}->{userspath});
483
  my $target    =  "${userspath}/kivitendo-print-requirement-spec-picture-" . Common::unique_id() . '-';
484

  
485
  foreach my $picture (map { @{ $_->pictures } } @{ $self->requirement_spec->text_blocks }) {
486
    my $output_file_name        = $target . $picture->id . '.' . $picture->get_default_file_name_extension;
487
    $picture->{print_file_name} = File::Spec->abs2rel($output_file_name, $userspath);
488
    my $out                     = IO::File->new($output_file_name, 'w') || die("Could not create file " . $output_file_name);
489
    $out->binmode;
490
    $out->print($picture->picture_content);
491
    $out->close;
492

  
493
    push @files, $output_file_name;
494
  }
495

  
496
  return @files;
497
}
498

  
472 499
1;
SL/Controller/RequirementSpecTextBlock.pm
12 12
use SL::Clipboard;
13 13
use SL::Controller::Helper::RequirementSpec;
14 14
use SL::DB::RequirementSpec;
15
use SL::DB::RequirementSpecPicture;
15 16
use SL::DB::RequirementSpecPredefinedText;
16 17
use SL::DB::RequirementSpecTextBlock;
17 18
use SL::Helper::Flash;
......
19 20

  
20 21
use Rose::Object::MakeMethods::Generic
21 22
(
22
  scalar                  => [ qw(text_block) ],
23
  scalar                  => [ qw(text_block picture) ],
23 24
  'scalar --get_set_init' => [ qw(predefined_texts js) ],
24 25
);
25 26

  
26
__PACKAGE__->run_before('load_requirement_spec_text_block', only => [qw(ajax_edit ajax_update ajax_delete ajax_flag dragged_and_dropped ajax_copy)]);
27
__PACKAGE__->run_before('load_requirement_spec_text_block', only => [qw(ajax_edit ajax_update ajax_delete ajax_flag dragged_and_dropped ajax_copy ajax_add_picture)]);
27 28

  
28 29
#
29 30
# actions
......
259 260
    ->render($self);
260 261
}
261 262

  
263
#
264
# actions for pictures
265
#
266

  
267
sub action_ajax_add_picture {
268
  my ($self) = @_;
269

  
270
  $self->picture(SL::DB::RequirementSpecPicture->new);
271
  $self->render('requirement_spec_text_block/_picture_form', { layout => 0 });
272
}
273

  
274
sub action_ajax_edit_picture {
275
  my ($self) = @_;
276

  
277
  $self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{picture_id})->load);
278
  $self->text_block($self->picture->text_block);
279
  $self->render('requirement_spec_text_block/_picture_form', { layout => 0 });
280
}
281

  
282
sub action_ajax_create_picture {
283
  my ($self, %params)              = @_;
284

  
285
  my $attributes                   = $::form->{ $::form->{form_prefix} } || die "Missing attributes";
286
  $attributes->{picture_file_name} = ((($::form->{ATTACHMENTS} || {})->{ $::form->{form_prefix} } || {})->{picture_content} || {})->{filename};
287
  my @errors                       = $self->picture(SL::DB::RequirementSpecPicture->new(%{ $attributes }))->validate;
288

  
289
  return $self->js->error(@errors)->render($self) if @errors;
290

  
291
  $self->picture->save;
292

  
293
  $self->text_block($self->picture->text_block);
294
  my $html = $self->render('requirement_spec_text_block/_text_block_picture', { output => 0 }, picture => $self->picture);
295

  
296
  $self->invalidate_version
297
    ->dialog->close('#jqueryui_popup_dialog')
298
    ->append('#text-block-' . $self->text_block->id . '-pictures', $html)
299
    ->show('#text-block-' . $self->text_block->id . '-pictures')
300
    ->render($self);
301
}
302

  
303
sub action_ajax_update_picture {
304
  my ($self)     = @_;
305

  
306
  my $attributes = $::form->{ $::form->{form_prefix} } || die "Missing attributes";
307
  $self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{id})->load);
308

  
309
  if (!$attributes->{picture_content}) {
310
    delete $attributes->{picture_content};
311
  } else {
312
    $attributes->{picture_file_name} = ((($::form->{ATTACHMENTS} || {})->{ $::form->{form_prefix} } || {})->{picture_content} || {})->{filename};
313
  }
314

  
315
  $self->picture->assign_attributes(%{ $attributes });
316
  my @errors = $self->picture->validate;
317

  
318
  return $self->js->error(@errors)->render($self) if @errors;
319

  
320
  $self->picture->save;
321

  
322
  $self->text_block($self->picture->text_block);
323
  my $html = $self->render('requirement_spec_text_block/_text_block_picture', { output => 0 }, picture => $self->picture);
324

  
325
  $self->invalidate_version
326
    ->dialog->close('#jqueryui_popup_dialog')
327
    ->replaceWith('#text-block-picture-' . $self->picture->id, $html)
328
    ->show('#text-block-' . $self->text_block->id . '-pictures')
329
    ->render($self);
330
}
331

  
332
sub action_ajax_delete_picture {
333
  my ($self) = @_;
334

  
335
  $self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{id})->load);
336
  $self->picture->delete;
337
  $self->text_block(SL::DB::RequirementSpecTextBlock->new(id => $self->picture->text_block_id)->load);
338

  
339
  $self->invalidate_version
340
    ->remove('#text-block-picture-' . $self->picture->id)
341
    ->action_if(!@{ $self->text_block->pictures }, 'hide', '#text-block-' . $self->text_block->id . '-pictures')
342
    ->render($self);
343
}
344

  
345
sub action_ajax_download_picture {
346
  my ($self) = @_;
347

  
348
  $self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{id})->load);
349
  $self->send_file(\$self->picture->{picture_content}, type => $self->picture->picture_content_type, name => $self->picture->picture_file_name);
350
}
351

  
262 352
#
263 353
# filters
264 354
#
SL/DB/Helper/ALL.pm
82 82
use SL::DB::RequirementSpecDependency;
83 83
use SL::DB::RequirementSpecItem;
84 84
use SL::DB::RequirementSpecOrder;
85
use SL::DB::RequirementSpecPicture;
85 86
use SL::DB::RequirementSpecPredefinedText;
86 87
use SL::DB::RequirementSpecRisk;
87 88
use SL::DB::RequirementSpecStatus;
SL/DB/Helper/Mappings.pm
162 162
  requirement_spec_item_dependencies   => 'RequirementSpecDependency',
163 163
  requirement_spec_items               => 'RequirementSpecItem',
164 164
  requirement_spec_orders              => 'RequirementSpecOrder',
165
  requirement_spec_pictures            => 'RequirementSpecPicture',
165 166
  requirement_spec_predefined_texts    => 'RequirementSpecPredefinedText',
166 167
  requirement_spec_risks               => 'RequirementSpecRisk',
167 168
  requirement_spec_statuses            => 'RequirementSpecStatus',
SL/DB/Manager/RequirementSpecPicture.pm
1
# This file has been auto-generated only because it didn't exist.
2
# Feel free to modify it at will; it will not be overwritten automatically.
3

  
4
package SL::DB::Manager::RequirementSpecPicture;
5

  
6
use strict;
7

  
8
use SL::DB::Helper::Manager;
9
use base qw(SL::DB::Helper::Manager);
10

  
11
sub object_class { 'SL::DB::RequirementSpecPicture' }
12

  
13
__PACKAGE__->make_manager_methods;
14

  
15
1;
SL/DB/MetaSetup/RequirementSpec.pm
16 16
  itime                   => { type => 'timestamp', default => 'now()' },
17 17
  mtime                   => { type => 'timestamp' },
18 18
  previous_fb_number      => { type => 'integer', not_null => 1 },
19
  previous_picture_number => { type => 'integer', default => '0', not_null => 1 },
19 20
  previous_section_number => { type => 'integer', not_null => 1 },
20 21
  project_id              => { type => 'integer' },
21 22
  status_id               => { type => 'integer' },
SL/DB/MetaSetup/RequirementSpecPicture.pm
1
# This file has been auto-generated. Do not modify it; it will be overwritten
2
# by rose_auto_create_model.pl automatically.
3
package SL::DB::RequirementSpecPicture;
4

  
5
use strict;
6

  
7
use base qw(SL::DB::Object);
8

  
9
__PACKAGE__->meta->table('requirement_spec_pictures');
10

  
11
__PACKAGE__->meta->columns(
12
  description            => { type => 'text' },
13
  id                     => { type => 'serial', not_null => 1 },
14
  itime                  => { type => 'timestamp', default => 'now()', not_null => 1 },
15
  mtime                  => { type => 'timestamp' },
16
  number                 => { type => 'text', not_null => 1 },
17
  picture_content        => { type => 'bytea', not_null => 1 },
18
  picture_content_type   => { type => 'text', not_null => 1 },
19
  picture_file_name      => { type => 'text', not_null => 1 },
20
  picture_height         => { type => 'integer', not_null => 1 },
21
  picture_mtime          => { type => 'timestamp', default => 'now()', not_null => 1 },
22
  picture_width          => { type => 'integer', not_null => 1 },
23
  position               => { type => 'integer', not_null => 1 },
24
  requirement_spec_id    => { type => 'integer', not_null => 1 },
25
  text_block_id          => { type => 'integer', not_null => 1 },
26
  thumbnail_content      => { type => 'bytea', not_null => 1 },
27
  thumbnail_content_type => { type => 'text', not_null => 1 },
28
  thumbnail_height       => { type => 'integer', not_null => 1 },
29
  thumbnail_width        => { type => 'integer', not_null => 1 },
30
);
31

  
32
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
33

  
34
__PACKAGE__->meta->allow_inline_column_values(1);
35

  
36
__PACKAGE__->meta->foreign_keys(
37
  requirement_spec => {
38
    class       => 'SL::DB::RequirementSpec',
39
    key_columns => { requirement_spec_id => 'id' },
40
  },
41

  
42
  text_block => {
43
    class       => 'SL::DB::RequirementSpecTextBlock',
44
    key_columns => { text_block_id => 'id' },
45
  },
46
);
47

  
48
1;
49
;
SL/DB/RequirementSpec.pm
60 60
sub _before_save_initialize_not_null_columns {
61 61
  my ($self) = @_;
62 62

  
63
  $self->previous_section_number(0) if !defined $self->previous_section_number;
64
  $self->previous_fb_number(0)      if !defined $self->previous_fb_number;
63
  for (qw(previous_section_number previous_fb_number previous_picture_number)) {
64
    $self->$_(0) if !defined $self->$_;
65
  }
65 66

  
66 67
  return 1;
67 68
}
......
149 150

  
150 151
  # Copy attributes.
151 152
  if (!$params->{paste_template}) {
152
    $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 is_template)),
153
    $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)),
153 154
                             %attributes);
154 155
  }
155 156

  
156 157
  my %paste_template_result;
157 158

  
158
  # Clone text blocks.
159
  # Clone text blocks and pictures.
160
  my $clone_picture = sub {
161
    my ($picture) = @_;
162
    my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($picture);
163
    $cloned->position(undef);
164
    return $cloned;
165
  };
166

  
159 167
  my $clone_text_block = sub {
160 168
    my ($text_block) = @_;
161 169
    my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
162 170
    $cloned->position(undef);
171
    $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
163 172
    return $cloned;
164 173
  };
165 174

  
SL/DB/RequirementSpecPicture.pm
1
package SL::DB::RequirementSpecPicture;
2

  
3
use strict;
4

  
5
use Carp;
6
use GD;
7
use Image::Info;
8
use List::MoreUtils qw(apply);
9
use List::Util qw(max);
10
use Rose::DB::Object::Util;
11
use SL::DB::MetaSetup::RequirementSpecPicture;
12
use SL::DB::Manager::RequirementSpecPicture;
13
use SL::DB::Helper::ActsAsList;
14
use SL::Locale::String;
15

  
16
__PACKAGE__->meta->initialize;
17

  
18
__PACKAGE__->configure_acts_as_list(group_by => [qw(requirement_spec_id text_block_id)]);
19

  
20
__PACKAGE__->before_save(\&_create_picture_number);
21
__PACKAGE__->before_save(\&_update_thumbnail);
22

  
23
our %supported_mime_types = (
24
  'image/gif'  => { extension => 'gif', convert_to_png => 1, },
25
  'image/png'  => { extension => 'png' },
26
  'image/jpeg' => { extension => 'jpg' },
27
);
28

  
29
sub _create_picture_number {
30
  my ($self) = @_;
31

  
32
  return 1 if  $self->number;
33
  return 0 if !$self->requirement_spec_id;
34

  
35
  my $next_number = $self->requirement_spec->previous_picture_number + 1;
36

  
37
  $self->requirement_spec->update_attributes(previous_picture_number => $next_number) || return 0;
38

  
39
  $self->number($next_number);
40

  
41
  return 1;
42
}
43

  
44
sub _update_thumbnail {
45
  my ($self) = @_;
46

  
47
  return 1 if !$self->picture_content || !$self->picture_content_type || !Rose::DB::Object::Util::get_column_value_modified($self, 'picture_content');
48
  $self->create_thumbnail;
49
  return 1;
50
}
51

  
52
sub create_thumbnail {
53
  my ($self) = @_;
54

  
55
  croak "No picture set yet" if !$self->picture_content;
56

  
57
  my $image            = GD::Image->new($self->picture_content);
58
  my ($width, $height) = $image->getBounds;
59
  my $max_dim          = 64;
60
  my $curr_max         = max $width, $height, 1;
61
  my $factor           = $curr_max <= $max_dim ? 1 : $curr_max / $max_dim;
62
  my $new_width        = int($width  / $factor + 0.5);
63
  my $new_height       = int($height / $factor + 0.5);
64
  my $thumbnail        = GD::Image->new($new_width, $new_height);
65

  
66
  $thumbnail->copyResized($image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
67

  
68
  $self->thumbnail_content($thumbnail->png);
69
  $self->thumbnail_content_type('image/png');
70
  $self->thumbnail_width($new_width);
71
  $self->thumbnail_height($new_height);
72

  
73
  return 1;
74
}
75

  
76
sub update_type_and_dimensions {
77
  my ($self) = @_;
78

  
79
  return () if !$self->picture_content;
80
  return () if $self->picture_content_type && $self->picture_width && $self->picture_height && !Rose::DB::Object::Util::get_column_value_modified($self, 'picture_content');
81

  
82
  my @errors = $self->probe_type;
83
  return @errors if @errors;
84

  
85
  my $info = $supported_mime_types{ $self->picture_content_type };
86
  if ($info->{convert_to_png}) {
87
    $self->picture_content(GD::Image->new($self->picture_content)->png);
88
    $self->picture_content_type('image/png');
89
    $self->picture_file_name(apply { s/\.[^\.]+$//;  $_ .= '.png'; } $self->picture_file_name);
90
  }
91

  
92
  return ();
93
}
94

  
95
sub probe_type {
96
  my ($self) = @_;
97

  
98
  return (t8("No picture uploaded yet")) if !$self->picture_content;
99

  
100
  my $info = Image::Info::image_info(\$self->{picture_content});
101
  if (!$info || $info->{error} || !$info->{file_media_type} || !$supported_mime_types{ $info->{file_media_type} }) {
102
    $::lxdebug->warn("Image::Info error: " . $info->{error}) if $info && $info->{error};
103
    return (t8('Unsupported image type (supported types: #1)', join(' ', sort keys %supported_mime_types)));
104
  }
105

  
106
  $self->picture_content_type($info->{file_media_type});
107
  $self->picture_width($info->{width});
108
  $self->picture_height($info->{height});
109
  $self->picture_mtime(DateTime->now_local);
110

  
111
  $self->create_thumbnail;
112

  
113
  return ();
114
}
115

  
116
sub get_default_file_name_extension {
117
  my ($self) = @_;
118

  
119
  my $info = $supported_mime_types{ $self->picture_content_type } || croak("Unsupported content type " . $self->picture_content_type);
120
  return $info->{extension};
121
}
122

  
123
sub validate {
124
  my ($self) = @_;
125

  
126
  my @errors;
127

  
128
  push @errors, t8('The file name is missing') if !$self->picture_file_name;
129

  
130
  if (!length($self->picture_content // '')) {
131
    push @errors, t8('No picture has been uploaded');
132

  
133
  } else {
134
    push @errors, $self->update_type_and_dimensions;
135
  }
136

  
137
  return @errors;
138
}
139

  
140
1;
SL/DB/RequirementSpecTextBlock.pm
2 2

  
3 3
use strict;
4 4

  
5
use Carp;
5 6
use List::MoreUtils qw(any);
6 7
use Rose::DB::Object::Helpers;
7 8
use Rose::DB::Object::Util;
......
11 12
use SL::DB::Helper::ActsAsList;
12 13
use SL::Locale::String;
13 14

  
15
__PACKAGE__->meta->add_relationship(
16
  pictures => {
17
    type         => 'one to many',
18
    class        => 'SL::DB::RequirementSpecPicture',
19
    column_map   => { id => 'text_block_id' },
20
  },
21
);
22

  
14 23
__PACKAGE__->meta->initialize;
15 24

  
16 25
__PACKAGE__->configure_acts_as_list(group_by => [qw(requirement_spec_id output_position)]);
......
48 57
  return 1;
49 58
}
50 59

  
60
sub pictures_sorted {
61
  my ($self, @args) = @_;
62

  
63
  croak "Not a writer" if @args;
64

  
65
  return [ sort { $a->position <=> $b->position } $self->pictures ];
66
}
67

  
51 68
1;
SL/InstallationCheck.pm
27 27
  { name => "Email::MIME",                         url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-mime-perl' },
28 28
  { name => "FCGI",            version => '0.72',  url => "http://search.cpan.org/~mstrout/",   debian => 'libfcgi-perl' },
29 29
  { name => "File::Copy::Recursive",               url => "http://search.cpan.org/~dmuey/",     debian => 'libfile-copy-recursive-perl' },
30
  { name => "GD",                                  url => "http://search.cpan.org/~lds/",       debian => 'libgd-gd2-perl', },
31
  { name => "Image::Info",                         url => "http://search.cpan.org/~srezic/",    debian => 'libimage-info-perl' },
30 32
  { name => "JSON",                                url => "http://search.cpan.org/~makamaka",   debian => 'libjson-perl' },
31 33
  { name => "List::MoreUtils", version => '0.21',  url => "http://search.cpan.org/~vparseval/", debian => 'liblist-moreutils-perl' },
32 34
  { name => "Params::Validate",                    url => "http://search.cpan.org/~drolsky/",   debian => 'libparams-validate-perl' },
SL/Template/Plugin/Base64.pm
1
package SL::Template::Plugin::Base64;
2

  
3
use strict;
4
use vars qw($VERSION);
5

  
6
$VERSION = 0.01;
7

  
8
use base qw(Template::Plugin);
9
use Template::Plugin;
10
use Template::Stash;
11
use MIME::Base64 ();
12

  
13
$Template::Stash::SCALAR_OPS->{'encode_base64'} = \&_encode_base64;
14
$Template::Stash::SCALAR_OPS->{'decode_base64'} = \&_decode_base64;
15

  
16
sub new {
17
  my ($class, $context, $options) = @_;
18

  
19
  $context->define_filter('encode_base64', \&_encode_base64);
20
  $context->define_filter('decode_base64', \&_decode_base64);
21
  return bless {}, $class;
22
}
23

  
24
sub _encode_base64 {
25
  return MIME::Base64::encode_base64(shift, '');
26
}
27

  
28
sub _decode_base64 {
29
  my ($self, $var) = @_;
30
  return MIME::Base64::decode_base64(shift);
31
}
32

  
33
1;
34

  
35
__END__
36

  
37
=head1 NAME
38

  
39
SL::Template::Plugin::Base64 - TT2 interface to base64 encoding/decoding
40

  
41
=head1 SYNOPSIS
42

  
43
  [% USE Base64 -%]
44
  [% SELF.some_object.binary_stuff.encode_base64 -%]
45
  [% SELF.some_object.binary_stuff FILTER encode_base64 -%]
46

  
47
=head1 DESCRIPTION
48

  
49
The I<Base64> Template Toolkit plugin provides access to the Base64
50
routines from L<MIME::Base64>.
51

  
52
The following filters (and vmethods of the same name) are installed
53
into the current context:
54

  
55
=over 4
56

  
57
=item C<encode_base64>
58

  
59
Returns the string encoded as Base64.
60

  
61
=item C<decode_base64>
62

  
63
Returns the Base64 string decoded back to binary.
64

  
65
=back
66

  
67
As the filters are also available as vmethods the following are all
68
equivalent:
69

  
70
    FILTER encode_base64; content; END;
71
    content FILTER encode_base64;
72
    content.encode_base64;
73

  
74
=head1 AUTHOR
75

  
76
Moritz Bunkus E<lt>m.bunkus@linet-services.de<gt>
77

  
78
=cut
css/requirement_spec.css
48 48
.context-menu-item.icon-save   { background-image: url("../image/document-save.png"); }
49 49
.context-menu-item.icon-revert { background-image: url("../image/edit-undo.png"); }
50 50
.context-menu-item.icon-pdf    { background-image: url("../image/application-pdf.png"); }
51
.context-menu-item.icon-add-picture { background-image: url("../image/add-picture.png"); }
52
.context-menu-item.icon-download    { background-image: url("../image/download.png"); }
51 53

  
52 54
/* ------------------------------------------------------------ */
53 55
/* Sections & function blocks */
......
158 160
  margin-left: 0;
159 161
}
160 162

  
163
.requirement-spec-text-block-picture-thumbnail {
164
  border-radius: 5px;
165
  border: 2px solid #ebebeb;
166
  float: left;
167
  margin-right: 20px;
168
  padding: 5px;
169
  text-align: center;
170
  width: 130px;
171
}
172

  
173
.requirement-spec-text-block-picture-thumbnail-img-container {
174
  height: 64px;
175
  margin: auto;
176
  padding: auto;
177
  width: 64px;
178
}
179

  
180
.requirement-spec-text-block-picture-thumbnail.selected {
181
  border: 2px solid #cbb120;
182
}
183

  
161 184
/* ------------------------------------------------------------ */
162 185
/* Time/cost estimation */
163 186
/* ------------------------------------------------------------ */
js/locale/de.js
1 1
namespace("kivi").setupLocale({
2 2
"Add function block":"Funktionsblock hinzufügen",
3 3
"Add linked record":"Verknüpften Beleg hinzufügen",
4
"Add picture":"Bild hinzufügen",
5
"Add picture to text block":"Bild dem Textblock hinzufügen",
4 6
"Add section":"Abschnitt hinzufügen",
5 7
"Add sub function block":"Unterfunktionsblock hinzufügen",
6 8
"Add text block":"Textblock erfassen",
......
18 20
"Create new version":"Neue Version anlegen",
19 21
"Database Connection Test":"Test der Datenbankverbindung",
20 22
"Delete":"Löschen",
23
"Delete picture":"Bild löschen",
21 24
"Delete quotation/order":"Angebot/Auftrag löschen",
22 25
"Delete requirement spec":"Pflichtenheft löschen",
23 26
"Delete template":"Vorlage löschen",
......
25 28
"Do you really want to cancel?":"Wollen Sie wirklich abbrechen?",
26 29
"Do you really want to revert to this version?":"Wollen Sie wirklich auf diese Version zurücksetzen?",
27 30
"Do you want to set the account number \"#1\" to \"#2\" and the name \"#3\" to \"#4\"?":"Soll die Kontonummer \"#1\" zu \"#2\" und den Name \"#3\" zu \"#4\" geändert werden?",
31
"Download picture":"Bild herunterladen",
28 32
"Edit":"Bearbeiten",
29 33
"Edit article/section assignments":"Zuweisung Artikel/Abschnitte bearbeiten",
34
"Edit picture":"Bild bearbeiten",
30 35
"Edit text block":"Textblock bearbeiten",
31 36
"Enter longdescription":"Langtext eingeben",
32 37
"Function block actions":"Funktionsblockaktionen",
33 38
"If you switch to a different tab without saving you will lose the data you've entered in the current tab.":"Wenn Sie auf einen anderen Tab wechseln, ohne vorher zu speichern, so gehen die im aktuellen Tab eingegebenen Daten verloren.",
34 39
"Map":"Karte",
35
"Orders/Quotations actions":"",
36 40
"Part picker":"Artikelauswahl",
37 41
"Paste":"Einfügen",
38 42
"Paste template":"Vorlage einfügen",
......
44 48
"Section/Function block actions":"Abschnitts-/Funktionsblockaktionen",
45 49
"Select template to paste":"Einzufügende Vorlage auswählen",
46 50
"Text block actions":"Textblockaktionen",
51
"Text block picture actions":"Aktionen für Textblockbilder",
47 52
"The description is missing.":"Die Beschreibung fehlt.",
48 53
"The name is missing.":"Der Name fehlt.",
49 54
"The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.",
js/requirement_spec.js
108 108

  
109 109
ns.find_text_block_id = function(clicked_elt) {
110 110
  var id = $(clicked_elt).attr('id');
111

  
112
  if (/^text-block-picture-\d+$/.test(id))
113
    id = $(clicked_elt).closest('.text-block-context-menu').attr('id');
114

  
111 115
  // console.log("id: " + id);
112 116
  if (/^text-block-\d+$/.test(id)) {
113 117
    // console.log("find_text_block_id: case 1: " + id.substr(11));
......
183 187
  }
184 188
};
185 189

  
190
ns.find_text_block_picture_id = function(clicked_elt) {
191
  var id    = $(clicked_elt).attr('id');
192
  var match = id.match(/^text-block-picture-(\d+)$/);
193
  if (match)
194
    return match[1] * 1;
195

  
196
  return undefined;
197
};
198

  
199
ns.add_edit_text_block_picture_ajax_call = function(key, opt) {
200
  var title = key == 'add_picture' ? kivi.t8('Add picture to text block') : kivi.t8('Edit picture');
201

  
202
  kivi.popup_dialog({ url:    'controller.pl',
203
                      data:   { action:     'RequirementSpecTextBlock/ajax_' + key,
204
                                id:         ns.find_text_block_id(opt.$trigger),
205
                                picture_id: ns.find_text_block_picture_id(opt.$trigger) },
206
                      dialog: { title:      title }});
207

  
208
  return true;
209
};
210

  
211
ns.standard_text_block_picture_ajax_call = function(key, opt) {
212
  var data = {
213
      action: "RequirementSpecTextBlock/ajax_" + key
214
    , id:     ns.find_text_block_picture_id(opt.$trigger)
215
  };
216

  
217
  if (key == 'download_picture')
218
    $.download("controller.pl", data);
219
  else
220
    $.post("controller.pl", data, kivi.eval_json_result);
221

  
222
  return true;
223
};
224

  
225
ns.ask_delete_text_block_picture = function(key, opt) {
226
  if (confirm(kivi.t8("Are you sure?")))
227
    ns.standard_text_block_picture_ajax_call(key, opt);
228
  return true;
229
};
230

  
231
ns.handle_text_block_picture_popup_menu_markings = function(opt, add) {
232
  var id = ns.find_text_block_picture_id(opt.$trigger);
233
  if (id)
234
    $('#text-block-picture-' + id ).toggleClass('selected', add);
235
  return true;
236
};
237

  
238
ns.text_block_picture_popup_menu_shown = function(opt) {
239
  return ns.handle_text_block_picture_popup_menu_markings(opt, true);
240
};
241

  
242
ns.text_block_picture_popup_menu_hidden = function(opt) {
243
  return ns.handle_text_block_picture_popup_menu_markings(opt, false);
244
};
245

  
186 246
// --------------------------------------------------------------------------------
187 247
// ------------------------------ sections and items ------------------------------
188 248
// --------------------------------------------------------------------------------
......
568 628
    };
569 629
  }                             // if (is_template) ... else ...
570 630

  
571
  var events = {
572
      show: kivi.requirement_spec.text_block_popup_menu_shown
573
    , hide: kivi.requirement_spec.text_block_popup_menu_hidden
574
  };
575

  
576 631
  $.contextMenu({
577 632
    selector: '.text-block-context-menu',
578 633
    events:   {
......
584 639
      , add:     { name: kivi.t8('Add text block'),        icon: "add",    callback: kivi.requirement_spec.standard_text_block_ajax_call }
585 640
      , edit:    { name: kivi.t8('Edit text block'),       icon: "edit",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
586 641
      , delete:  { name: kivi.t8('Delete text block'),     icon: "delete", callback: kivi.requirement_spec.ask_delete_text_block,         disabled: kivi.requirement_spec.disable_edit_text_block_commands }
642
      , add_picture: { name: kivi.t8('Add picture to text block'), icon: "add-picture", callback: kivi.requirement_spec.add_edit_text_block_picture_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
587 643
      , sep1:    "---------"
588 644
      , flag:    { name: kivi.t8('Toggle marker'),         icon: "flag",   callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands }
589 645
      , sep2:    "---------"
......
592 648
    }, general_actions)
593 649
  });
594 650

  
651
  $.contextMenu({
652
    selector: '.text-block-picture-context-menu',
653
    events:   {
654
        show: kivi.requirement_spec.text_block_picture_popup_menu_shown
655
      , hide: kivi.requirement_spec.text_block_picture_popup_menu_hidden
656
    },
657
    items:    $.extend({
658
        heading:          { name: kivi.t8('Text block picture actions'), className: 'context-menu-heading'                                                 }
659
      , add_picture:      { name: kivi.t8('Add picture'),      icon: "add-picture", callback: kivi.requirement_spec.add_edit_text_block_picture_ajax_call  }
660
      , edit_picture:     { name: kivi.t8('Edit picture'),     icon: "edit",        callback: kivi.requirement_spec.add_edit_text_block_picture_ajax_call  }
661
      , delete_picture:   { name: kivi.t8('Delete picture'),   icon: "delete",      callback: kivi.requirement_spec.ask_delete_text_block_picture          }
662
      , download_picture: { name: kivi.t8('Download picture'), icon: "download",    callback: kivi.requirement_spec.standard_text_block_picture_ajax_call  }
663
    }, general_actions)
664
  });
665

  
595 666
  $.contextMenu({
596 667
    selector: '.basic-settings-context-menu',
597 668
    items:    $.extend({
......
671 742
    items:    general_actions
672 743
  });
673 744

  
674
  events = {
745
  var events = {
675 746
      show: kivi.requirement_spec.item_popup_menu_shown
676 747
    , hide: kivi.requirement_spec.item_popup_menu_hidden
677 748
  };
locale/de/all
177 177
  'Add new currency'            => 'Neue Währung hinzufügen',
178 178
  'Add new custom variable'     => 'Neue benutzerdefinierte Variable erfassen',
179 179
  'Add note'                    => 'Notiz erfassen',
180
  'Add printer'                 => 'Drucker hinzufügen',
180
  'Add picture'                 => 'Bild hinzufügen',
181
  'Add picture to text block'   => 'Bild dem Textblock hinzufügen',
181 182
  'Add section'                 => 'Abschnitt hinzufügen',
182 183
  'Add sub function block'      => 'Unterfunktionsblock hinzufügen',
183 184
  'Add text block'              => 'Textblock erfassen',
......
631 632
  'Current Earnings'            => 'Gewinn',
632 633
  'Current assets account'      => 'Konto für Umlaufvermögen',
633 634
  'Current filter'              => 'Aktueller Filter',
635
  'Current picture'             => 'Aktuelles Bild',
634 636
  'Current profile'             => 'Aktuelles Profil',
635 637
  'Current status'              => 'Aktueller Status',
636 638
  'Current value:'              => 'Aktueller Wert:',
......
742 744
  'Delete Shipto'               => 'Lieferadresse löschen',
743 745
  'Delete drafts'               => 'Entwürfe löschen',
744 746
  'Delete links'                => 'Verknüpfungen löschen',
747
  'Delete picture'              => 'Bild löschen',
745 748
  'Delete profile'              => 'Profil löschen',
746 749
  'Delete quotation/order'      => 'Angebot/Auftrag löschen',
747 750
  'Delete requirement spec'     => 'Pflichtenheft löschen',
......
789 792
  'Detail view'                 => 'Detailanzeige',
790 793
  'Details (one letter abbreviation)' => 'D',
791 794
  'Difference'                  => 'Differenz',
795
  'Dimensions'                  => 'Abmessungen',
792 796
  'Directory'                   => 'Verzeichnis',
793 797
  'Discard duplicate entries in CSV file' => 'Doppelte Einträge in CSV-Datei verwerfen',
794 798
  'Discard entries with duplicates in database or CSV file' => 'Einträge aus CSV-Datei verwerfen, die es bereits in der Datenbank oder der CSV-Datei gibt',
......
828 832
  'Done'                        => 'Fertig',
829 833
  'Double partnumbers'          => 'Doppelte Artikelnummern',
830 834
  'Download SEPA XML export file' => 'SEPA-XML-Exportdatei herunterladen',
835
  'Download picture'            => 'Bild herunterladen',
831 836
  'Download sample file'        => 'Beispieldatei herunterladen',
832 837
  'Draft saved.'                => 'Entwurf gespeichert.',
833 838
  'Drawing'                     => 'Zeichnung',
......
923 928
  'Edit greetings'              => 'Anreden bearbeiten',
924 929
  'Edit note'                   => 'Notiz bearbeiten',
925 930
  'Edit payment term'           => 'Zahlungsbedingungen bearbeiten',
931
  'Edit picture'                => 'Bild bearbeiten',
926 932
  'Edit predefined text'        => 'Vordefinierten Textblock bearbeiten',
927 933
  'Edit prices and discount (if not used, textfield is ONLY set readonly)' => 'Preise und Rabatt in Formularen frei anpassen (falls deaktiviert, wird allerdings NUR das textfield auf READONLY gesetzt / kann je nach Browserversion und technischen Fähigkeiten des Anwenders noch umgangen werden)',
928 934
  'Edit project'                => 'Projekt bearbeiten',
......
937 943
  'Edit templates'              => 'Vorlagen bearbeiten',
938 944
  'Edit text block'             => 'Textblock bearbeiten',
939 945
  'Edit text block \'#1\''      => 'Textblock \'#1\' bearbeiten',
946
  'Edit text block picture #1'  => 'Textblockbild #1 bearbeiten',
940 947
  'Edit the Delivery Order'     => 'Lieferschein bearbeiten',
941 948
  'Edit the configuration for periodic invoices' => 'Konfiguration für wiederkehrende Rechnungen bearbeiten',
942 949
  'Edit the currency names in order to rename them.' => 'Bearbeiten Sie den Namen, um eine Währung umzubennen.',
......
1372 1379
  'Long Description'            => 'Langtext',
1373 1380
  'MAILED'                      => 'Gesendet',
1374 1381
  'MD'                          => 'PT',
1382
  'MIME type'                   => 'MIME-Typ',
1375 1383
  'Machine'                     => 'Maschine',
1376 1384
  'Main Preferences'            => 'Grundeinstellungen',
1377 1385
  'Main sorting'                => 'Hauptsortierung',
......
1495 1503
  'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.',
1496 1504
  'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.',
1497 1505
  'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.',
1506
  'No picture has been uploaded' => 'Es wurde kein Bild hochgeladen',
1507
  'No picture uploaded yet'     => 'Noch kein Bild hochgeladen',
1498 1508
  'No predefined texts has been created yet.' => 'Es wurden noch keine vordefinierten Textblöcken angelegt.',
1499 1509
  'No prices will be updated because no prices have been entered.' => 'Es werden keine Preise aktualisiert, weil keine gültigen Preisänderungen eingegeben wurden.',
1500 1510
  'No print templates have been created for this client yet. Please do so in the client configuration.' => 'Für diesen Mandanten wurden noch keine Druckvorlagen angelegt. Bitte holen Sie dies in der Mandantenkonfiguration nach.',
......
1685 1695
  'Phone1'                      => 'Telefon 1 ',
1686 1696
  'Phone2'                      => 'Telefon 2',
1687 1697
  'Pick List'                   => 'Sammelliste',
1698
  'Picture #1: #2'              => 'Abbildung #1: #2',
1688 1699
  'Pictures for parts'          => 'Bilder für Waren',
1689 1700
  'Pictures for search parts'   => 'Bilder für Warensuche',
1690 1701
  'Please Check the bank information for each customer:' => 'Bitte überprüfen Sie die Bankinformationen der Kunden:',
......
2017 2028
  'Select a vendor'             => 'Einen Lieferanten ausw&auml;hlen',
2018 2029
  'Select all'                  => 'Alle auswählen',
2019 2030
  'Select federal state...'     => 'Bundesland auswählen...',
2031
  'Select file to upload'       => 'Datei zum Hochladen auswählen',
2020 2032
  'Select from one of the items below' => 'Wählen Sie einen der untenstehenden Einträge',
2021 2033
  'Select from one of the names below' => 'Wählen Sie einen der untenstehenden Namen',
2022 2034
  'Select from one of the projects below' => 'Wählen Sie eines der untenstehenden Projekte',
......
2236 2248
  'Test and preview'            => 'Test und Vorschau',
2237 2249
  'Test database connectivity'  => 'Datenbankverbindung testen',
2238 2250
  'Text block actions'          => 'Textblockaktionen',
2251
  'Text block picture actions'  => 'Aktionen für Textblockbilder',
2239 2252
  'Text blocks back'            => 'Textblöcke hinten',
2240 2253
  'Text blocks front'           => 'Textblöcke vorne',
2241 2254
  'Text field'                  => 'Textfeld',
......
2347 2360
  'The existing record has been created from the link target to add.' => 'Der bestehende Beleg wurde aus dem auszuwählenden Verknüpfungsziel erstellt.',
2348 2361
  'The factor is missing in row %d.' => 'Der Faktor fehlt in Zeile %d.',
2349 2362
  'The factor is missing.'      => 'Der Faktor fehlt.',
2363
  'The file name is missing'    => 'Der Dateiname fehlt',
2350 2364
  'The first reason is that kivitendo contained a bug which resulted in the wrong taxkeys being recorded for transactions in which two entries are posted for the same chart with different taxkeys.' => 'Der erste Grund war ein Fehler in kivitendo, der dazu führte, dass bei einer Transaktion, bei der zwei Buchungen mit unterschiedlichen Steuerschlüsseln auf dasselbe Konto durchgeführt wurden, die falschen Steuerschlüssel gespeichert wurden.',
2351 2365
  'The follow-up date is missing.' => 'Das Wiedervorlagedatum fehlt.',
2352 2366
  'The following currencies have been used, but they are not defined:' => 'Die folgenden Währungen wurden benutzt, sind aber nicht ordnungsgemäß in der Datenbank eingetragen:',
......
2634 2648
  'Unknown dependency \'%s\'.'  => 'Unbekannte Abh&auml;ngigkeit \'%s\'.',
2635 2649
  'Unknown problem type.'       => 'Unbekannter Problem-Typ',
2636 2650
  'Unlock System'               => 'System entsperren',
2651
  'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)',
2637 2652
  'Until'                       => 'Bis',
2638 2653
  'Update'                      => 'Erneuern',
2639 2654
  'Update Prices'               => 'Preise aktualisieren',
......
2649 2664
  'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren',
2650 2665
  'Updating prices of existing entry in database' => 'Preis des Eintrags in der Datenbank wird aktualisiert',
2651 2666
  'Updating the client fields in the database "#1" on host "#2:#3" failed.' => 'Die Aktualisierung der Mandantenfelder in der Datenbank "#1" auf Host "#2:#3" schlug fehl.',
2667
  'Uploaded at'                 => 'Hochgeladen um',
2652 2668
  'Uploaded on #1, size #2 kB'  => 'Am #1 hochgeladen, Größe #2 kB',
2653 2669
  'Use As New'                  => 'Als neu verwenden',
2654 2670
  'Use WebDAV Repository'       => 'WebDAV-Ablage verwenden',
......
2690 2706
  'Vendor missing!'             => 'Lieferant fehlt!',
2691 2707
  'Vendor not on file or locked!' => 'Dieser Lieferant existiert nicht oder ist gesperrt.',
2692 2708
  'Vendor not on file!'         => 'Lieferant ist nicht in der Datenbank!',
2693
  'Vendor saved!'               => 'Lieferant gespeichert!',
2694 2709
  'Vendor saved'                => 'Lieferant gespeichert',
2710
  'Vendor saved!'               => 'Lieferant gespeichert!',
2695 2711
  'Vendor type'                 => 'Lieferantentyp',
2696 2712
  'Vendors'                     => 'Lieferanten',
2697 2713
  'Verrechnungseinheit'         => 'Verrechnungseinheit',
2714
  'Version'                     => 'Version',
2698 2715
  'Version actions'             => 'Aktionen für Versionen',
2699 2716
  'Version number'              => 'Versionsnummer',
2700
  'Version'                     => 'Version',
2701 2717
  'Versions'                    => 'Versionen',
2702 2718
  'View SEPA export'            => 'SEPA-Export-Details ansehen',
2703 2719
  'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen',
sql/Pg-upgrade2/requirement_spec_pictures.sql
1
-- @tag: requirement_spec_pictures
2
-- @description: Pflichtenhefte: Support für Bilder
3
-- @depends: requirement_specs
4

  
5
CREATE TABLE requirement_spec_pictures (
6
  id                     SERIAL    NOT NULL,
7
  requirement_spec_id    INTEGER   NOT NULL,
8
  text_block_id          INTEGER   NOT NULL,
9
  position               INTEGER   NOT NULL,
10
  number                 TEXT      NOT NULL,
11
  description            TEXT,
12
  picture_file_name      TEXT      NOT NULL,
13
  picture_content_type   TEXT      NOT NULL,
14
  picture_mtime          TIMESTAMP NOT NULL DEFAULT now(),
15
  picture_content        BYTEA     NOT NULL,
16
  picture_width          INTEGER   NOT NULL,
17
  picture_height         INTEGER   NOT NULL,
18
  thumbnail_content_type TEXT      NOT NULL,
19
  thumbnail_content      BYTEA     NOT NULL,
20
  thumbnail_width        INTEGER   NOT NULL,
21
  thumbnail_height       INTEGER   NOT NULL,
22
  itime                  TIMESTAMP NOT NULL DEFAULT now(),
23
  mtime                  TIMESTAMP,
24

  
25
  PRIMARY KEY (id),
26
  FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs            (id) ON DELETE CASCADE,
27
  FOREIGN KEY (text_block_id)       REFERENCES requirement_spec_text_blocks (id) ON DELETE CASCADE
28
);
29

  
30
CREATE TRIGGER mtime_requirement_spec_pictures BEFORE UPDATE ON requirement_spec_pictures FOR EACH ROW EXECUTE PROCEDURE set_mtime();
31

  
32
ALTER TABLE requirement_specs ADD COLUMN previous_picture_number INTEGER;
33
UPDATE requirement_specs SET previous_picture_number = 0;
34
ALTER TABLE requirement_specs ALTER COLUMN previous_picture_number SET NOT NULL;
35
ALTER TABLE requirement_specs ALTER COLUMN previous_picture_number SET DEFAULT 0;
templates/print/Standard/requirement_spec.tex
85 85
\end{longtable}
86 86
%$( END )$
87 87

  
88
%$( BLOCK picture_outputter )$
89
%  $( SET width_cm = (picture.picture_width / 150.0) * 2.54 )$
90
%  $( SET width_cm = width_cm < 16.4 ? width_cm : 16.4 )$
91
\begin{figure}[h!]
92
  \centering
93
  \includegraphics[width=$( width_cm )$cm,keepaspectratio]{$( picture.print_file_name )$}
94

  
95
\mbox{Abbildung $( picture.number )$: $( LxLatex.filter(picture.description ? picture.description : picture.picture_file_name) )$}
96
\end{figure}
97
%$( END )$
98

  
88 99
%$( BLOCK text_block_outputter )$
89 100
%  $( SET text_blocks = rspec.text_blocks_sorted(output_position=output_position) )$
90 101
%  $( IF text_blocks.size )$
......
99 110

  
100 111
$( LxLatex.filter(text_block.text) )$
101 112

  
113
%      $( FOREACH picture = text_block.pictures_sorted.as_list )$
114
$( PROCESS picture_outputter picture=picture )$
115
%      $( END )$
116

  
102 117
%    $( END )$
103 118
%  $( END )$
104 119
%$( END )$
templates/webpages/requirement_spec_text_block/_picture_form.html
1
[%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE JavaScript -%][% USE Base64 %][% SET style="width: 500px" %]
2
[% SET id_base = 'edit_text_block_picture_' _ (SELF.picture.id ? SELF.picture.id : 'new') %]
3
<form method="post" id="[% id_base %]_form" method="POST" enctype="multipart/form-data">
4
 [% L.hidden_tag('form_prefix',                    id_base,         id=id_base _ '_form_prefix') %]
5
 [% L.hidden_tag('id',                             SELF.picture.id, no_id=1) %]
6
 [% L.hidden_tag(id_base _ '.text_block_id',       SELF.text_block.id) %]
7
 [% L.hidden_tag(id_base _ '.requirement_spec_id', SELF.text_block.requirement_spec_id) %]
8

  
9
 <h2>
10
  [%- IF SELF.picture.id %]
11
   [%- LxERP.t8("Edit text block picture #1", SELF.picture.number) %]
12
  [%- ELSE %]
13
   [%- LxERP.t8("Add picture to text block") %]
14
  [%- END %]
15
 </h2>
16

  
17
 <table>
18
[% IF SELF.picture.number %]
19
  <tr>
20
   <th align="right">[%- LxERP.t8("Number") %]:</th>
21
   <td>[% HTML.escape(SELF.picture.number) %]</td>
22
  </tr>
23
[% END %]
24

  
25
  <tr>
26
   <th align="right">[%- LxERP.t8("Description") %]:</th>
27
   <td>[% L.input_tag(id_base _ '.description', SELF.picture.description, style=style) %]</td>
28
  </tr>
29

  
30
[% IF SELF.picture.picture_content %]
31
  <tr>
32
   <th align="right">[%- LxERP.t8("File name") %]:</th>
33
   <td>[% HTML.escape(SELF.picture.picture_file_name) %]</td>
34
  </tr>
35

  
36
  <tr>
37
   <th align="right">[%- LxERP.t8("MIME type") %]:</th>
38
   <td>[% HTML.escape(SELF.picture.picture_content_type) %]</td>
39
  </tr>
40

  
41
  <tr>
42
   <th align="right">[%- LxERP.t8("Dimensions") %]:</th>
43
   <td>[% HTML.escape(SELF.picture.picture_width) %]x[% HTML.escape(SELF.picture.picture_height) %]</td>
44
  </tr>
45

  
46
  <tr>
47
   <th align="right">[%- LxERP.t8("Uploaded at") %]:</th>
48
   <td>[% HTML.escape(SELF.picture.picture_mtime.to_kivitendo(precision='second')) %]</td>
49
  </tr>
50
[% END %]
51

  
52
  <tr>
53
   <th align="right">[%- LxERP.t8("Select file to upload") %]:</th>
54
   <td>[% L.input_tag(id_base _ '.picture_content', '', type='file') %]</td>
55
  </tr>
56
 </table>
57

  
58
 <p>
59
  [%- L.ajax_submit_tag('controller.pl?action=RequirementSpecTextBlock/ajax_' _ (SELF.picture.id ? 'update' : 'create') _ '_picture', '#' _ id_base _ '_form', LxERP.t8('Save'), no_id=1) %]
60
  <a href="#" onclick="$('#jqueryui_popup_dialog').dialog('close');">[%- LxERP.t8("Cancel") %]</a>
61
 </p>
62

  
63
</form>
64

  
65
[% IF SELF.picture.id %]
66
<h2>[% LxERP.t8("Current picture") %]</h2>
67

  
68
<div>
69
 <img src="data:[% HTML.escape(SELF.picture.picture_content_type) %];base64,[% SELF.picture.picture_content.encode_base64 %]">
70
</div>
71
[% END %]
templates/webpages/requirement_spec_text_block/_text_block.html
11 11
   [% L.simple_format(text_block.text) %]
12 12
  </div>
13 13
 [% END %]
14

  
15
 <div class="clearfix" id="text-block-[% HTML.escape(text_block.id) %]-pictures"[% UNLESS text_block.pictures.as_list.size %] style="display: none"[% END %]>
16
  [% FOREACH picture = text_block.pictures_sorted %]
17
   [% INCLUDE 'requirement_spec_text_block/_text_block_picture.html' picture=picture %]
18
  [% END %]
19
 </div>
14 20
</div>
templates/webpages/requirement_spec_text_block/_text_block_picture.html
1
[%- USE HTML -%][%- USE LxERP -%][% USE Base64 %]
2
[% SET title=LxERP.t8('Picture #1: #2', picture.number, picture.description ? picture.description _ ' (' _ picture.picture_file_name _ ')' : picture.picture_file_name) %]
3
<div id="text-block-picture-[% HTML.escape(picture.id) %]" class="requirement-spec-text-block-picture-thumbnail text-block-picture-context-menu">
4
 <div class="requirement-spec-text-block-picture-thumbnail-img-container">
5
  <img src="data:[% HTML.escape(picture.thumbnail_content_type) %];base64,[% picture.thumbnail_content.encode_base64 %]" alt="[% title %]">
6
 </div>
7
 <div>
8
  [% LxERP.t8('Picture #1: #2', picture.number, picture.description ? picture.description : picture.picture_file_name) %]
9
 </div>
10
</div>

Auch abrufbar als: Unified diff