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