allow in-place editing of product option names
[bse.git] / site / cgi-bin / modules / BSE / Edit / Product.pm
CommitLineData
ca9aa2bf
TC
1package BSE::Edit::Product;
2use strict;
3use base 'BSE::Edit::Article';
4use Products;
5use HTML::Entities;
aefcabcb 6use BSE::Template;
0ec4ac8a 7use BSE::Util::Iterate;
ab3c22ff 8use DevHelp::HTML;
2076966c 9use BSE::CfgInfo 'product_options';
58baa27b
TC
10use BSE::Util::Tags qw(tag_hash);
11
12=head1 NAME
13
14BSE::Edit::Product - tags and actions for editing BSE products
15
16=head1 SYNOPSIS
17
18 http://www.example.com/cgi-bin/admin/add.pl ...
19
20=head1 DESCRIPTION
21
22Article editor subclass for editing Products.
23
24=cut
ca9aa2bf
TC
25
26my %money_fields =
27 (
28 retailPrice => "Retail price",
29 wholesalePrice => "Wholesale price",
30 gst => "GST",
31 );
32
33sub generator { 'Generate::Product' }
34
35sub base_template_dirs {
36 return ( "products" );
37}
38
39sub extra_templates {
40 my ($self, $article) = @_;
41
42 my @extras = $self->SUPER::extra_templates($article);
aefcabcb
TC
43 push @extras, 'shopitem.tmpl'
44 if grep -f "$_/shopitem.tmpl",
45 BSE::Template->template_dirs($self->{cfg});
ca9aa2bf 46
d64413ee
TC
47 my $extras = $self->{cfg}->entry('products', 'extra_templates');
48 push @extras, grep /\.(tmpl|html)$/i, split /,/, $extras
49 if $extras;
50
ca9aa2bf
TC
51 return @extras;
52}
53
54sub hash_tag {
55 my ($article, $arg) = @_;
56
57 my $value = $article->{$arg};
58 defined $value or $value = '';
7b81711b
TC
59 if ($value =~ /\cJ/ && $value =~ /\cM/) {
60 $value =~ tr/\cM//d;
61 }
ca9aa2bf
TC
62
63 return encode_entities($value);
64}
65
d7538448
TC
66sub iter_subs {
67 require BSE::TB::Subscriptions;
68 BSE::TB::Subscriptions->all;
69}
0ec4ac8a 70
58baa27b
TC
71sub iter_option_values {
72 my ($self, $rcurrent_option, $args) = @_;
73
74 $$rcurrent_option
75 or return;
76
77 return $$rcurrent_option->values;
78}
79
ab3c22ff
TC
80sub tag_hash_mbcs {
81 my ($object, $args) = @_;
82
83 my $value = $object->{$args};
84 defined $value or $value = '';
85 if ($value =~ /\cJ/ && $value =~ /\cM/) {
86 $value =~ tr/\cM//d;
87 }
88 escape_html($value, '<>&"');
89}
90
58baa27b
TC
91sub tag_dboptionvalue_move {
92 my ($self, $req, $article, $rvalues, $rindex, $args) = @_;
93
94 $$rindex >= 0 && $$rindex < @$rvalues
95 or return "** dboptionvalue_move only in dboption_values iterator **";
96
97 my $my_id = $rvalues->[$$rindex]{id};
98 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&value_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option_value") . "&";
99
100 my $t = $req->cgi->param('_t');
101 $t && $t =~ /^\w+$/
102 and $base_url .= "_t=$t&";
103
104 my $up_url = '';
105 if ($$rindex > 0) {
106 $up_url = $base_url . "a_option_value_moveup=1";
107 }
108 my $down_url = '';
109 if ($$rindex < $#$rvalues) {
110 $down_url = $base_url . "a_option_value_movedown=1";
111 }
112
113 my $refresh = $self->refresh_url($article, $req->cgi);
114
115 require BSE::Arrows;
116 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptvaluemove");
117}
118
119sub tag_dboption_move {
120 my ($self, $req, $article, $roptions, $rindex, $args) = @_;
121
122 $$rindex >= 0 && $$rindex < @$roptions
123 or return "** dboption_move only in dboptions iterator **";
124
125 my $my_id = $roptions->[$$rindex]{id};
126 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&option_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option") . "&";
127
128 my $t = $req->cgi->param('_t');
129 $t && $t =~ /^\w+$/
130 and $base_url .= "_t=$t&";
131
132 my $up_url = '';
133 if ($$rindex > 0) {
134 $up_url = $base_url . "a_option_moveup=1";
135 }
136 my $down_url = '';
137 if ($$rindex < $#$roptions) {
138 $down_url = $base_url . "a_option_movedown=1";
139 }
140
141 my $refresh = $self->refresh_url($article, $req->cgi);
142
143 require BSE::Arrows;
144 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptmove");
145}
146
147=head1 Edit tags
148
149These a tags available on admin/edit_* pages specific to products.
150
151=over
152
153=item *
154
155product I<field> - display the given field from the product being edited.
156
157=item *
158
159iterator begin dboptions ... dboption I<field> ... iterator end dboptions
160
161- iterate over the existing database stored options for the product
162
163=item *
164
165dboption_move - display arrows to move the current dboption. The span
166for the arrows is given an id of "prodoptmoveI<option-id>" by default.
167
168=item *
169
170iterator begin dboptionvalues ... dboptionvalue I<field> ... iterator end dboptionvalues
171
172- iterate over the values for the current dboption
173
174=item *
175
176dboptionvalue_move - display arrows to move the current dboption. The
177span for the arrows is given an id of "prodoptvaluemoveI<value-id>"
178by default.
179
180=item *
181
182dboptionsjson - returns the product options as JSON.
183
184=back
185
186=cut
187
ca9aa2bf
TC
188sub low_edit_tags {
189 my ($self, $acts, $req, $article, $articles, $msg, $errors) = @_;
ab3c22ff 190
2076966c
TC
191 my $product_opts = product_options($req->cfg);
192
ab3c22ff
TC
193 my $cfg = $req->cfg;
194 my $mbcs = $cfg->entry('html', 'mbcs', 0);
195 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&hash_tag;
58baa27b
TC
196 my $current_option;
197 my @dboptions;
198 my $dboption_index;
199 my @dboption_values;
200 my $dboption_value_index;
201 my $current_option_value;
0ec4ac8a 202 my $it = BSE::Util::Iterate->new;
ca9aa2bf
TC
203 return
204 (
ab3c22ff 205 product => [ $tag_hash, $article ],
ca9aa2bf
TC
206 $self->SUPER::low_edit_tags($acts, $req, $article, $articles, $msg,
207 $errors),
2076966c 208 alloptions => join(",", sort keys %$product_opts),
d7538448
TC
209 $it->make_iterator
210 ([ \&iter_subs, $req ], 'subscription', 'subscriptions'),
58baa27b
TC
211 $it->make
212 (
213 single => "dboption",
214 plural => "dboptions",
215 store => \$current_option,
216 data => \@dboptions,
217 index => \$dboption_index,
218 code => [ db_options => $article ],
219 ),
220 dboption_move =>
221 [
222 tag_dboption_move =>
223 $self, $req, $article, \@dboptions, \$dboption_index
224 ],
225 $it->make
226 (
227 single => "dboptionvalue",
228 plural => "dboptionvalues",
229 data => \@dboption_values,
230 index => \$dboption_value_index,
231 store => \$current_option_value,
232 code => [ iter_option_values => $self, \$current_option ],
233 nocache => 1,
234 ),
235 dboptionsjson => [ tag_dboptionsjson => $self, $article ],
236 dboptionvalue_move =>
237 [
238 tag_dboptionvalue_move =>
239 $self, $req, $article, \@dboption_values, \$dboption_value_index
240 ],
ca9aa2bf
TC
241 );
242}
243
244sub edit_template {
245 my ($self, $article, $cgi) = @_;
246
247 my $base = 'product';
248 my $t = $cgi->param('_t');
249 if ($t && $t =~ /^\w+$/) {
250 $base = $t;
251 }
252 return $self->{cfg}->entry('admin templates', $base,
253 "admin/edit_$base");
254}
255
256sub add_template {
257 my ($self, $article, $cgi) = @_;
258
259 return $self->{cfg}->entry('admin templates', 'add_product',
918735d1 260 'admin/edit_product');
ca9aa2bf
TC
261}
262
263sub validate_parent {
264 my ($self, $data, $articles, $parent, $rmsg) = @_;
265
266 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
267 unless ($parent &&
268 $parent->{generator} eq 'Generate::Catalog') {
269 $$rmsg = "Products must be in a catalog (not $parent->{generator})";
270 return;
271 }
272
273 return $self->SUPER::validate_parent($data, $articles, $parent, $rmsg);
274}
275
276sub _validate_common {
277 my ($self, $data, $articles, $errors) = @_;
278
279 for my $col (keys %money_fields) {
280 my $value = $data->{$col};
0ec4ac8a 281 defined $value or next;
ca9aa2bf
TC
282 unless ($value =~ /^\d+(\.\d{1,2})?\s*/) {
283 $errors->{$col} = "$money_fields{$col} invalid";
284 }
285 }
2076966c 286
0ec4ac8a 287 if (defined $data->{options}) {
2076966c
TC
288 my $avail_options = product_options($self->{cfg});
289
290 my @bad_opts = grep !$avail_options->{$_},
0ec4ac8a
TC
291 split /,/, $data->{options};
292 if (@bad_opts) {
293 $errors->{options} = "Bad product options '". join(",", @bad_opts)."' entered";
294 }
295 }
ca9aa2bf 296
0ec4ac8a
TC
297 my @subs;
298 for my $sub_field (qw(subscription_id subscription_required)) {
299 my $value = $data->{$sub_field};
300 defined $value or next;
301 if ($value ne '-1') {
d7538448
TC
302 require BSE::TB::Subscriptions;
303 @subs = BSE::TB::Subscriptions->all unless @subs;
304 unless (grep $_->{subscription_id} == $value, @subs) {
0ec4ac8a 305 $errors->{$sub_field} = "Invalid $sub_field value";
d7538448 306 }
0ec4ac8a
TC
307 }
308 }
309 if (defined $data->{subscription_period}) {
ab2cd916
TC
310 my $sub = $data->{subscription_id};
311 if ($data->{subscription_period} !~ /^\d+$/) {
0ec4ac8a
TC
312 $errors->{subscription_period} = "Invalid subscription period, it must be the number of months to subscribe";
313 }
ab2cd916
TC
314 elsif ($sub != -1 && $data->{subscription_period} < 1) {
315 $errors->{subscription_period} = "Subscription period must be 1 or more when a subscription is selected";
316 }
0ec4ac8a
TC
317 }
318 if (defined $data->{subscription_usage}) {
319 unless ($data->{subscription_usage} =~ /^[123]$/) {
320 $errors->{subscription_usage} = "Invalid subscription usage";
321 }
ca9aa2bf
TC
322 }
323
324 return !keys %$errors;
325}
326
327sub validate {
918735d1 328 my ($self, $data, $articles, $errors) = @_;
ca9aa2bf 329
918735d1 330 my $ok = $self->SUPER::validate($data, $articles, $errors);
ca9aa2bf
TC
331 $self->_validate_common($data, $articles, $errors);
332
7fa9d326 333 for my $field (qw(title description body)) {
ca9aa2bf
TC
334 unless ($data->{$field} =~ /\S/) {
335 $errors->{$field} = "No $field entered";
336 }
337 }
338
339 return $ok && !keys %$errors;
340}
341
342sub validate_old {
918735d1 343 my ($self, $article, $data, $articles, $errors) = @_;
ca9aa2bf 344
918735d1 345 $self->SUPER::validate($data, $articles, $errors)
ca9aa2bf
TC
346 or return;
347
348 return !keys %$errors;
349}
350
351sub possible_parents {
352 my ($self, $article, $articles) = @_;
353
354 my %labels;
355 my @values;
356
357 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
358 # the parents of a catalog can be other catalogs or the shop
359 my $shop = $articles->getByPkey($shopid);
360 my @work = [ $shopid, $shop->{title} ];
361 while (@work) {
362 my ($id, $title) = @{pop @work};
363 push(@values, $id);
364 $labels{$id} = $title;
365 push @work, map [ $_->{id}, $title.' / '.$_->{title} ],
366 sort { $b->{displayOrder} <=> $a->{displayOrder} }
367 grep $_->{generator} eq 'Generate::Catalog',
368 $articles->getBy(parentid=>$id);
369 }
a5e3fc4b
TC
370 unless ($shop->{generator} eq 'Generate::Catalog') {
371 shift @values;
372 delete $labels{$shopid};
373 }
ca9aa2bf
TC
374 return (\@values, \%labels);
375}
376
377sub table_object {
378 my ($self, $articles) = @_;
379
380 'Products';
381}
382
383sub get_article {
384 my ($self, $articles, $article) = @_;
385
386 return Products->getByPkey($article->{id});
387}
388
95989433
TC
389sub default_link_path {
390 my ($self, $article) = @_;
391
392 $self->{cfg}->entry('uri', 'shop', '/shop');
393}
394
ca9aa2bf
TC
395sub make_link {
396 my ($self, $article) = @_;
397
57d988af
TC
398# Modified by adrian
399 my $urlbase = '';
400 if ($self->{cfg}->entry('shop', 'secureurl_articles', 1)) {
401 $urlbase = $self->{cfg}->entryVar('site', 'secureurl');
402 }
403# end adrian
efcc5a30
TC
404
405 if ($article->is_dynamic) {
b873a8fa 406 return "$urlbase/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($article->{title});
efcc5a30
TC
407 }
408
409 my $shop_uri = $self->link_path($article);
ca9aa2bf
TC
410 return $urlbase.$shop_uri."/shop$article->{id}.html";
411}
412
413sub _fill_product_data {
414 my ($self, $req, $data, $src) = @_;
415
416 for my $money_col (qw(retailPrice wholesalePrice gst)) {
417 if (exists $src->{$money_col}) {
418 if ($src->{$money_col} =~ /^\d+(\.\d\d)?\s*/) {
419 $data->{$money_col} = 100 * $src->{$money_col};
420 }
421 else {
422 $data->{$money_col} = 0;
423 }
424 }
425 }
426 if (exists $src->{leadTime}) {
427 $src->{leadTime} =~ /^\d+\s*$/
428 or $src->{leadTime} = 0;
429 $data->{leadTime} = $src->{leadTime};
430 }
74b21f6d 431 if (exists $src->{description} && length $src->{description}) {
4010d92e 432 if ($data->{id}) {
74b21f6d
TC
433 if ($req->user_can('edit_field_edit_description', $data)) {
434 $data->{description} = $src->{description};
435 }
436 }
437 }
438 if (exists $src->{product_code} && length $src->{product_code}) {
439 if ($data->{id}) {
440 if ($req->user_can('edit_field_edit_product_code', $data)) {
441 $data->{product_code} = $src->{product_code};
4010d92e
TC
442 }
443 }
444 }
0ec4ac8a
TC
445 for my $field (qw(options subscription_id subscription_period
446 subscription_usage subscription_required)) {
447 if (exists $src->{$field}) {
448 $data->{$field} = $src->{$field};
449 }
450 elsif ($data == $src) {
451 # use the default
452 $data->{$field} = $self->default_value($req, $data, $field);
453 }
918735d1 454 }
ca9aa2bf
TC
455}
456
457sub fill_new_data {
458 my ($self, $req, $data, $articles) = @_;
459
460 $self->_fill_product_data($req, $data, $data);
461
462 return $self->SUPER::fill_new_data($req, $data, $articles);
463}
464
465sub fill_old_data {
466 my ($self, $req, $article, $src) = @_;
467
468 $self->_fill_product_data($req, $article, $src);
469
470 return $self->SUPER::fill_old_data($req, $article, $src);
471}
472
caa7299c
TC
473sub default_template {
474 my ($self, $article, $cfg, $templates) = @_;
475
d64413ee 476 my $template = $cfg->entry('products', 'template');
caa7299c
TC
477 return $template
478 if $template && grep $_ eq $template, @$templates;
479
480 return $self->SUPER::default_template($article, $cfg, $templates);
481}
482
6473c56f
TC
483sub can_remove {
484 my ($self, $req, $article, $articles, $rmsg) = @_;
485
4175638b
TC
486 require BSE::TB::OrderItems;
487 my @items = BSE::TB::OrderItems->getBy(productId=>$article->{id});
6473c56f
TC
488 if (@items) {
489 $$rmsg = "There are orders for this product. It cannot be deleted.";
490 return;
491 }
492
493 return $self->SUPER::can_remove($req, $article, $articles, $rmsg);
494}
495
918735d1
TC
496sub flag_sections {
497 my ($self) = @_;
498
499 return ( 'product flags', $self->SUPER::flag_sections );
500}
501
0ec4ac8a
TC
502my %defaults =
503 (
504 options => '',
505 subscription_id => -1,
506 subscription_required => -1,
507 subscription_period => 1,
508 subscription_usage => 3,
509 retailPrice => 0,
510 );
511
512sub default_value {
513 my ($self, $req, $article, $col) = @_;
514
515 my $value = $self->SUPER::default_value($req, $article, $col);
516 defined $value and return $value;
517
518 exists $defaults{$col} and return $defaults{$col};
519
520 return;
521}
522
deae2a52
TC
523sub type_default_value {
524 my ($self, $req, $col) = @_;
525
526 my $value = $req->cfg->entry('product defaults', $col);
527 defined $value and return $value;
528
529 return $self->SUPER::type_default_value($req, $col);
530}
531
58baa27b
TC
532my %option_fields =
533 (
534 name =>
535 {
536 description => "Option name",
537 required => 1,
538 rules => "dh_one_line",
539 },
540 value1 =>
541 {
542 description => "Value 1",
543 rules => "dh_one_line",
544 },
545 value2 =>
546 {
547 description => "Value 2",
548 rules => "dh_one_line",
549 },
550 value3 =>
551 {
552 description => "Value 3",
553 rules => "dh_one_line",
554 },
555 value4 =>
556 {
557 description => "Value 4",
558 rules => "dh_one_line",
559 },
560 value5 =>
561 {
562 description => "Value 5",
563 rules => "dh_one_line",
564 },
565 );
566
567=head1 Targets
568
569Actions you can request from add.pl for products.
570
571=over
572
573=item a_add_option
574
575Add a new product option.
576
577On failure perform a service error.
578
579Requires _csrfp for admin_add_option
580
581For Ajax requests (or with a _ parameter) returns JSON like:
582
583 {
584 success: 1,
585 option: { <option data> },
586 values: [ { value data }, { value data }, ... ]
587 }
588
589Parameters:
590
591=over
592
593=item *
594
595id - Article id
596
597=item *
598
599name - Name of the option (required)
600
601=item *
602
603value1 .. value5 - if any of these are non-blank they are added to the
604option as values.
605
606=back
607
608=cut
609
610sub req_add_option {
611 my ($self, $req, $article, $articles, $msg, $errors) = @_;
612
613 $req->check_csrf('admin_add_option')
614 or return $self->csrf_error($req, $article, "admin_add_option", "Add Product Option");
615
616 my %errors;
617 $req->validate(fields => \%option_fields,
618 errors => \%errors);
619 keys %errors
620 and return $self->_service_error($req, $article, $articles, undef,
621 \%errors);
622
623 my $cgi = $req->cgi;
624 require BSE::TB::ProductOptions;
625 require BSE::TB::ProductOptionValues;
626 my $option = BSE::TB::ProductOptions->make
627 (
628 product_id => $article->{id},
629 name => scalar($cgi->param('name')),
630 display_order => time,
631 );
632
633 my $order = time;
634 my @values;
635 for my $value_key (sort grep /^value/, keys %option_fields) {
636 print STDERR "fetching $value_key\n";
637 my ($value) = $cgi->param($value_key);
638 if (defined $value && $value =~ /\S/) {
639 my $entry = BSE::TB::ProductOptionValues->make
640 (
641 product_option_id => $option->{id},
642 value => $value,
643 display_order => $order,
644 );
645 push @values, $entry;
646 ++$order;
647 }
648 }
649
650 $req->is_ajax
651 and return $req->json_content
652 (
653 success => 1,
654 option => $option->data_only,
655 values => [ map $_->data_only, @values ]
656 );
657
658 return $self->refresh($article, $cgi, undef, "Option added");
659}
660
661my %option_id =
662 (
663 option_id =>
664 {
665 rules => "required;positiveint",
666 },
667 );
668
669sub _get_option {
670 my ($self, $req, $article, $errors) = @_;
671
672 my $option;
673 my $cgi = $req->cgi;
674 $req->validate(fields => \%option_id,
675 errors => $errors);
4de47893
TC
676 my @option_ids = $cgi->param("option_id");
677 unless ($errors->{option_id}) {
678 @option_ids == 1
679 or $errors->{option_id} = "This request accepts only one option_id";
680 }
58baa27b
TC
681 unless ($errors->{option_id}) {
682 require BSE::TB::ProductOptions;
683 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
684 $option
685 or $errors->{option_id} = "Unknown option id";
686 }
687 unless ($errors->{option_id}) {
688 $option->{product_id} = $article->{id}
689 or $errors->{option_id} = "Option doesn't belong to this product";
690 }
691 $errors->{option_id}
692 and return;
693
694 return $option;
695}
696
697sub _common_option {
698 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
699
700 my %errors;
701 my $option = $self->_get_option($req, $article, \%errors);
702 keys %errors
703 and return $self->_service_error($req, $article, $articles, undef, \%errors);
704
705 my $it = BSE::Util::Iterate->new;
706 my %acts;
707 %acts =
708 (
709 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
710 option => [ \&tag_hash, $option ],
711 $it->make
712 (
713 single => "dboptionvalue",
714 plural => "dboptionvalues",
715 code => [ iter_option_values => $self, \$option ],
716 ),
717 );
718
719 return $req->dyn_response($template, \%acts);
720}
721
722=item a_edit_option
723
724Produce a form to edit the given option.
725
726Parameters:
727
728=over
729
730=item *
731
732id - article id
733
734=item *
735
736option_id - option id. This must belong to the product identified by
737id.
738
739=back
740
741Template: admin/prodopt_edit
742
743=cut
744
745sub req_edit_option {
746 my ($self, $req, $article, $articles, $msg, $errors) = @_;
747
748 return $self->_common_option('admin/prodopt_edit', $req, $article,
749 $articles, $msg, $errors);
750}
751
752my %option_name =
753 (
754 name =>
755 {
756 description => "Option name",
757 rules => "required;dh_one_line"
758 },
759 default_value =>
760 {
761 description => "Default Value",
762 rules => "integer"
763 }
764 );
765
766my %option_value =
767 (
768 description => "Value",
769 rules => "required;dh_one_line"
770 );
771
772=item a_save_option
773
774Saves changes to an option.
775
776On failure perform a service error.
777
778Requires _csrfp for admin_save_option
779
780For Ajax requests (or with a _ parameter), returns JSON like:
781
782 {
783 success: 1,
784 option: { <option data> },
785 values: [ { value data, value data, ... } ]
786 }
787
788Parameters:
789
790=over
791
792=item *
793
794id - article id
795
796=item *
797
798option_id - id of the option to save, must belong to the product
799identified by id.
800
801=item *
802
803name - new value for the name field
804
805=item *
806
807default_value - id of the default value
808
809=item *
810
811save_enabled - if supplied and true, set enabled from the enabled
812parameter.
813
814=item *
815
816enabled - If supplied and true, enable the option, otherwise disable
817it. Ignored unless save_enabled is true.
818
819=item *
820
821valueI<value-id> - set the displayed value for the value record
5708b3ac
TC
822identified by I<value-id>. If these aren't supplied the values aren't
823changed.
58baa27b
TC
824
825=back
826
827=cut
828
829sub req_save_option {
830 my ($self, $req, $article, $articles) = @_;
831
832 my $cgi = $req->cgi;
833
834 $req->check_csrf("admin_save_option")
835 or return $self->csrf_error($req, $article, "admin_save_option", "Save Product Option");
836
837 my %errors;
838 my $option = $self->_get_option($req, $article, \%errors);
839 keys %errors
840 and return $self->_service_error($req, $article, $articles, undef, \%errors);
841 $req->validate(rules => \%option_name,
842 errors => \%errors);
843 my @values = $option->values;
844 my %fields = map {; "value$_->{id}" => \%option_value } @values;
845 $req->validate(rules => \%fields,
846 errors => \%errors);
847 my $default_value = $cgi->param('default_value');
848 if (!$errors{default_value} && $default_value) {
849 grep $_->{id} == $default_value, @values
850 or $errors{default_value} = "Unknown value selected as default";
851 }
852 keys %errors
853 and return $self->req_edit_option($req, $article, $articles, undef, \%errors);
854
855 my $name = $cgi->param("name");
856 defined $name
857 and $option->set_name($name);
858 defined $default_value
859 and $option->set_default_value($default_value);
860 if ($cgi->param("save_enabled")) {
861 my $enabled = $cgi->param("enabled") ? 1 : 0;
862 $option->set_enabled($enabled);
863 }
864 $option->save;
865 for my $value (@values) {
866 my $new_value = $cgi->param("value$value->{id}");
5708b3ac 867 if (defined $new_value && $new_value ne $value->value) {
58baa27b
TC
868 $value->set_value($new_value);
869 $value->save;
870 }
871 }
872
873 $req->is_ajax
874 and return $req->json_content
875 (
876 success => 1,
877 option => $option->data_only,
878 values => [ map $_->data_only, @values ],
879 );
880
881 return $self->refresh($article, $req->cgi, undef, "Option saved");
882}
883
884=item a_delconf_option
885
886Produce a form to confirm deletion of the given option.
887
888Parameters:
889
890=over
891
892=item *
893
894id - article id
895
896=item *
897
898option_id - option id. This must belong to the product identified by
899id.
900
901=back
902
903Template: admin/prodopt_delete
904
905=cut
906
907sub req_delconf_option {
908 my ($self, $req, $article, $articles, $msg, $errors) = @_;
909
910 return $self->_common_option('admin/prodopt_delete', $req, $article,
911 $articles, $msg, $errors);
912}
913
914=item a_delete_option
915
916Delete the given option.
917
918On failure perform a service error.
919
920Requires _csrfp for admin_delete_option
921
922For Ajax requests (or with a _ parameter), returns JSON like:
923
924 {
925 success: 1,
926 }
927
928=cut
929
930sub req_delete_option {
931 my ($self, $req, $article, $articles) = @_;
932
933 $req->check_csrf("admin_delete_option")
934 or return $self->csrf_error($req, $article, "admin_delete_option", "Delete Product Option");
935
936 my %errors;
937 my $option = $self->_get_option($req, $article, \%errors);
938 keys %errors
939 and return $self->_service_error($req, $article, $articles, undef, \%errors);
940 my @values = $option->values;
941
942 for my $value (@values) {
943 $value->remove;
944 }
945 $option->remove;
946
947 $req->is_ajax
948 and return $req->json_content
949 (
950 success => 1
951 );
952
953 return $self->refresh($article, $req->cgi, undef, "Option deleted");
954}
955
956
957my %add_option_value_fields =
958 (
959 option_id =>
960 {
961 description => "Option id",
962 rules => "required;positiveint",
963 },
964 value =>
965 {
966 description => "Value",
967 rules => "required",
968 },
969 );
970
971=item a_add_option_value
972
973Add a value to a product option.
974
975On failure perform a service error, see BSE::Edit::Article::_service_error.
976
977Requires _csrfp for admin_add_option_value
978
979For Ajax requests returns JSON like
980
981 { success: 1, value: (valueobject) }
982
983Standard redirect on success otherwise.
984
985Parameters:
986
987=over
988
989=item *
990
991id - article id
992
993=item *
994
995option_id - id of the option to add the value to
996
997=item *
998
999value - text of the value to add.
1000
1001=back
1002
1003=cut
1004
1005sub req_add_option_value {
1006 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1007
1008 $req->check_csrf("admin_add_option_value")
1009 or return $self->csrf_error($req, $article, "admin_add_option_value", "Add Product Option Value");
1010
1011 my %errors;
1012 $req->validate(fields => \%add_option_value_fields,
1013 errors => \%errors);
1014 my $option;
1015 my $cgi = $req->cgi;
1016 unless ($errors{option_id}) {
1017 require BSE::TB::ProductOptions;
1018 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
1019 defined $option && $option->{product_id}
1020 or $errors{option_id} = "Bad option id - either unknown or for a different product";
1021 }
1022 keys %errors
1023 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1024
1025 my $value = $cgi->param("value");
1026 require BSE::TB::ProductOptionValues;
1027 my $entry = BSE::TB::ProductOptionValues->make
1028 (
1029 product_option_id => $option->{id},
1030 value => $value,
1031 display_order => time,
1032 );
1033
1034 $req->is_ajax
1035 and return $self->json_content
1036 (
1037 success => 1,
1038 value => $entry->data_only
1039 );
1040
1041 return $self->refresh($article, $cgi, undef, "Value added");
1042}
1043
1044
1045my %option_value_id =
1046 (
1047 value_id =>
1048 {
1049 rules => "required;positiveint",
1050 },
1051 );
1052
1053sub _get_option_value {
1054 my ($self, $req, $article, $errors) = @_;
1055
1056 my $option_value;
1057 my $cgi = $req->cgi;
1058 $req->validate(fields => \%option_value_id,
1059 errors => $errors);
1060 unless ($errors->{value_id}) {
1061 require BSE::TB::ProductOptionValues;
1062 $option_value = BSE::TB::ProductOptionValues->getByPkey($cgi->param("value_id"));
1063 $option_value
1064 or $errors->{value_id} = "Unknown option value id";
1065 }
1066 my $option;
1067 unless ($errors->{value_id}) {
1068 $option = $option_value->option;
1069 defined $option && $option->{product_id} == $article->{id}
1070 or $errors->{value_id} = "Value has no option or doesn't belong to the product";
1071 }
1072
1073 $errors->{value_id}
1074 and return;
1075
1076 return wantarray ? ( $option_value, $option ) : $option_value ;
1077}
1078
1079sub _common_option_value {
1080 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
1081
1082 my %errors;
1083 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1084 keys %errors
1085 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1086
1087 my %acts;
1088 %acts =
1089 (
1090 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
1091 option_value => [ \&tag_hash, $option_value ],
1092 option => [ \&tag_hash, $option ],
1093 );
1094
1095 return $req->dyn_response($template, \%acts);
1096}
1097
1098=item a_edit_option_value
1099
1100Displays a form to edit the value for a given option.
1101
1102Parameters:
1103
1104=over
1105
1106=item *
1107
1108id - id of the product
1109
1110=item *
1111
1112value_id - id of he product option value to edit, must belong to the
1113given product.
1114
1115=back
1116
1117Template: admin/prodopt_value_edit
1118
1119=cut
1120
1121sub req_edit_option_value {
1122 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1123
1124 return $self->_common_option_value('admin/prodopt_value_edit', $req,
1125 $article, $articles, $msg, $errors);
1126}
1127
1128my %save_option_value_fields =
1129 (
1130 value =>
1131 {
1132 rules => "required;dh_one_line",
1133 },
1134 );
1135
1136=item a_save_option_value
1137
1138Saves changes to an option.
1139
1140On failure perform a service error.
1141
1142Requires _csrfp for admin_save_option_value
1143
1144For Ajax requests (or with a _ parameter), returns JSON like:
1145
1146 {
1147 success: 1,
1148 value: { value data }
1149 }
1150
1151Parameters:
1152
1153=over
1154
1155=item *
1156
1157id - article id
1158
1159=item *
1160
1161value_id - id of the value to save, must belong to the product
1162identified by id.
1163
1164=item *
1165
1166value - new displayed value for the option value.
1167
1168=back
1169
1170=cut
1171
1172sub req_save_option_value {
1173 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1174
1175 $req->check_csrf("admin_save_option_value")
1176 or return $self->csrf_error($req, $article, "admin_save_option_value", "Save Product Option Value");
1177
1178 my %errors;
1179 $req->validate(fields => \%save_option_value_fields,
1180 errors => \%errors);
1181 my $option_value = $self->_get_option_value($req, $article, \%errors);
1182 keys %errors
1183 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1184
1185 my $cgi = $req->cgi;
1186 $option_value->{value} = $cgi->param("value");
1187 $option_value->save;
1188
1189 $req->is_ajax
1190 and return $self->json_content
1191 (
1192 success => 1,
1193 value => $option_value->data_only
1194 );
1195
1196 return $self->refresh($article, $cgi, undef, "Value saved");
1197}
1198
1199=item a_confdel_option_value
1200
1201Displays a page confirming deletion of a product option value.
1202
1203Parameters:
1204
1205=over
1206
1207=item *
1208
1209id - article id
1210
1211=item *
1212
1213value_id - option value id
1214
1215=back
1216
1217Template: admin/prodopt_value_delete
1218
1219=cut
1220
1221sub req_confdel_option_value {
1222 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1223
1224 return $self->_common_option_value('admin/prodopt_value_delete', $req,
1225 $article, $articles, $msg, $errors);
1226}
1227
1228=item a_delete_option_value
1229
1230Deletes a product option.
1231
1232On failure perform a service error.
1233
1234Requires _csrfp for admin_delete_option_value
1235
1236For Ajax requests (or with a _ parameter), returns JSON like:
1237
1238 {
1239 success: 1,
1240 }
1241
1242Parameters:
1243
1244=over
1245
1246=item *
1247
1248id - article id
1249
1250=item *
1251
1252value_id - id of the value to delete, must belong to the product
1253identified by id.
1254
1255=back
1256
1257=cut
1258
1259sub req_delete_option_value {
1260 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1261
1262 $req->check_csrf("admin_delete_option_value")
1263 or return $self->csrf_error($req, $article, "admin_delete_option_value", "Delete Product Option Value");
1264
1265 my %errors;
1266 my $option_value = $self->_get_option_value($req, $article, \%errors);
1267 keys %errors
1268 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1269
1270 $option_value->remove;
1271
1272 $req->is_ajax
1273 and return $self->json_content
1274 (
1275 success => 1
1276 );
1277
1278 return $self->refresh($article, $req->cgi, undef, "Value removed");
1279}
1280
1281sub tag_dboptionsjson {
1282 my ($self, $article) = @_;
1283
1284 my @result;
1285 my @options = $article->db_options;
1286 my @opt_cols = BSE::TB::ProductOption->columns;
1287 for my $option (@options) {
1288 my $entry = $option->data_only;
1289 $entry->{values} = [ map $_->data_only, $option->values ];
1290 push @result, $entry;
1291 }
1292
1293 require JSON;
1294 my $json = JSON->new;
1295 return $json->encode(\@result);
1296}
1297
1298sub _option_move {
1299 my ($self, $req, $article, $articles, $direction) = @_;
1300
1301 $req->check_csrf("admin_move_option")
1302 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1303
1304 my %errors;
1305 my $option = $self->_get_option($req, $article, \%errors);
1306 keys %errors
1307 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1308 my @options = $article->db_options;
1309 my ($index) = grep $options[$_]{id} == $option->{id}, 0 .. $#options
1310 or return $self->_service_error($req, $article, $articles, "Unknown option id");
1311
1312 $options[$index] = $option;
1313
1314 my $other_index = $index + $direction;
1315 $other_index >= 0 && $other_index < @options
1316 or return $self->_service_error($req, $article, $articles, "Can't move option beyond end");
1317
1318 my $other = $options[$other_index];
1319
1320 ($option->{display_order}, $other->{display_order}) =
1321 ($other->{display_order}, $option->{display_order});
1322 $option->save;
1323 $other->save;
1324
4de47893
TC
1325 if ($req->is_ajax) {
1326 @options = sort { $a->{display_order} <=> $b->{display_order} } @options;
1327 return $req->json_content
58baa27b
TC
1328 (
1329 success => 1,
1330 order => [ map $_->{id}, @options ]
1331 );
4de47893 1332 }
58baa27b
TC
1333
1334 return $self->refresh($article, $req->cgi, undef, "Option moved");
1335}
1336
4de47893 1337=item a_option_moveup
58baa27b 1338
4de47893 1339=item a_option_movedown
58baa27b
TC
1340
1341Move a product option up/down through the options for a product.
1342
1343On failure perform a service error.
1344
1345Requires _csrfp for admin_move_option
1346
1347For Ajax requests (or with a _ parameter), returns JSON like:
1348
1349 {
1350 success: 1,
1351 order: [ list of option ids ]
1352 }
1353
1354Parameters:
1355
1356=over
1357
1358=item *
1359
1360id - article id
1361
1362=item *
1363
1364option_id - option id. This must belong to the product identified by
1365id.
1366
1367=back
1368
1369=cut
1370
1371sub req_option_moveup {
1372 my ($self, $req, $article, $articles) = @_;
1373
1374 return $self->_option_move($req, $article, $articles, -1);
1375}
1376
1377sub req_option_movedown {
1378 my ($self, $req, $article, $articles) = @_;
1379
1380 return $self->_option_move($req, $article, $articles, 1);
1381}
1382
1383=item a_option_reorder
1384
1385Move a product option up/down through the options for a product.
1386
1387On failure perform a service error.
1388
1389Requires _csrfp for admin_move_option
1390
1391For Ajax requests (or with a _ parameter), returns JSON like:
1392
1393 {
1394 success: 1,
1395 order: [ list of option ids ]
1396 }
1397
1398Parameters:
1399
1400=over
1401
1402=item *
1403
1404id - article id
1405
1406=item *
1407
1408option_ids - option ids separated by commas. These must belong to the
1409product identified by id.
1410
1411=back
1412
1413=cut
1414
1415sub req_option_reorder {
1416 my ($self, $req, $article, $articles) = @_;
1417
1418 $req->check_csrf("admin_move_option")
1419 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1420
1421 my @options = $article->db_options;
1422 my @order = map { split ',' } $req->cgi->param('option_ids');
1423 my %options = map { $_->{id} => $_ } @options;
1424 my @new_options;
1425 for my $id (@order) {
1426 my $option = delete $options{$id}
1427 or next;
1428 push @new_options, $option;
1429 }
1430 push @new_options, sort { $a->{display_order} <=> $b->{display_order} } values %options;
1431 my @display_order = map $_->{display_order}, @options;
1432 for my $index (0 .. $#new_options) {
1433 $new_options[$index]{display_order} = $display_order[$index];
1434 $new_options[$index]->save;
1435 }
1436
1437 $req->is_ajax
1438 and return $req->json_content
1439 (
1440 success => 1,
1441 order => [ map $_->{id}, @new_options ]
1442 );
1443
1444 return $self->refresh($article, $req->cgi, undef, "Options reordered");
1445}
1446
1447sub _option_value_move {
1448 my ($self, $req, $article, $articles, $direction) = @_;
1449
1450 $req->check_csrf("admin_move_option_value")
1451 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1452
1453 my %errors;
1454 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1455 keys %errors
1456 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1457 my @values = $option->values;
1458 my ($index) = grep $values[$_]{id} == $option_value->{id}, 0 .. $#values
1459 or return $self->_service_error($req, $article, $articles, "Unknown option value id");
1460
1461 $values[$index] = $option_value;
1462
1463 my $other_index = $index + $direction;
1464 $other_index >= 0 && $other_index < @values
1465 or return $self->_service_error($req, $article, $articles, "Can't move option value beyond end");
1466
1467 my $other = $values[$other_index];
1468
1469 ($option_value->{display_order}, $other->{display_order}) =
1470 ($other->{display_order}, $option_value->{display_order});
1471 $option_value->save;
1472 $other->save;
1473
1474 $req->is_ajax
1475 and $req->json_content
1476 (
1477 success => 1,
1478 order => [ map $_->{id}, @values ]
1479 );
1480
1481 return $self->refresh($article, $req->cgi, undef, "Value moved");
1482}
1483
1484=item a_option_value_moveup
1485
1486=item a_option_value_movedown
1487
1488Move a product option value up/down through the values for a product
1489option.
1490
1491On failure perform a service error.
1492
1493Requires _csrfp for admin_move_option_value
1494
1495For Ajax requests (or with a _ parameter), returns JSON like:
1496
1497 {
1498 success: 1,
1499 order: [ list of value ids ]
1500 }
1501
1502Parameters:
1503
1504=over
1505
1506=item *
1507
1508id - article id
1509
1510=item *
1511
1512value_id - option id. This must belong to the product identified by
1513id.
1514
1515=back
1516
1517=cut
1518
1519sub req_option_value_moveup {
1520 my ($self, $req, $article, $articles) = @_;
1521
1522 return $self->_option_value_move($req, $article, $articles, -1);
1523}
1524
1525sub req_option_value_movedown {
1526 my ($self, $req, $article, $articles) = @_;
1527
1528 return $self->_option_value_move($req, $article, $articles, 1);
1529}
1530
1531=item a_option_value_reorder
1532
1533Specify a new order for the values belonging to a product option.
1534
1535On failure perform a service error.
1536
1537Requires _csrfp for admin_move_option_value
1538
1539For Ajax requests (or with a _ parameter), returns JSON like:
1540
1541 {
1542 success: 1,
1543 order: [ list of value ids ]
1544 }
1545
1546Parameters:
1547
1548=over
1549
1550=item *
1551
1552id - article id
1553
1554=item *
1555
4de47893
TC
1556option_id - the option to reorder values for
1557
1558=item *
1559
58baa27b
TC
1560value_ids - new order for values specified as value ids separated by
1561commas.
1562
1563=back
1564
1565=cut
1566
1567sub req_option_value_reorder {
1568 my ($self, $req, $article, $articles) = @_;
1569
1570 $req->check_csrf("admin_move_option_value")
1571 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1572
1573 my %errors;
1574 my $option = $self->_get_option($req, $article, \%errors);
1575 keys %errors
1576 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1577 my @order = map { split ',' } $req->cgi->param('value_ids');
1578 my @values = $option->values;
1579 my %values = map { $_->{id} => $_ } @values;
1580 my @new_values;
1581 for my $id (@order) {
1582 my $value = delete $values{$id}
1583 or next;
1584 push @new_values, $value;
1585 }
1586 push @new_values, sort { $a->{display_order} <=> $b->{display_order} } values %values;
1587 my @display_order = map $_->{display_order}, @values;
1588 for my $index (0 .. $#new_values) {
1589 $new_values[$index]{display_order} = $display_order[$index];
1590 $new_values[$index]->save;
1591 }
1592
1593 $req->is_ajax
1594 and return $req->json_content
1595 (
1596 success => 1,
1597 option => $option->data_only,
1598 order => [ map $_->{id}, @new_values ]
1599 );
1600
1601 return $self->refresh($article, $req->cgi, undef, "Values reordered");
1602}
1603
1604sub article_actions {
1605 my $self = shift;
1606
1607 return
1608 (
1609 $self->SUPER::article_actions,
1610 a_add_option => 'req_add_option',
1611 a_confdel_option => 'req_confdel_option',
1612 a_del_option => 'req_del_option',
1613 a_edit_option => 'req_edit_option',
1614 a_save_option => 'req_save_option',
1615 a_delconf_option => 'req_delconf_option',
1616 a_delete_option => 'req_delete_option',
1617 a_get_option => 'req_get_option',
1618 a_edit_option_value => 'req_edit_option_value',
1619 a_save_option_value => 'req_save_option_value',
1620 a_confdel_option_value => 'req_confdel_option_value',
1621 a_delete_option_value => 'req_delete_option_value',
1622 a_add_option_value => 'req_add_option_value',
1623 a_option_value_moveup => 'req_option_value_moveup',
1624 a_option_value_movedown => 'req_option_value_movedown',
1625 a_option_value_reorder => 'req_option_value_reorder',
1626 a_option_moveup => 'req_option_moveup',
1627 a_option_movedown => 'req_option_movedown',
1628 a_option_reorder => 'req_option_reorder',
1629 );
1630}
1631
ca9aa2bf 16321;