1 package BSE::Edit::Product;
3 use base 'BSE::Edit::Article';
7 use BSE::Util::Iterate;
9 use BSE::CfgInfo 'product_options';
10 use BSE::Util::Tags qw(tag_hash tag_article);
12 use constant PRODUCT_CUSTOM_FIELDS_CFG => "product custom fields";
14 our $VERSION = "1.019";
18 BSE::Edit::Product - tags and actions for editing BSE products
22 http://www.example.com/cgi-bin/admin/add.pl ...
26 Article editor subclass for editing Products.
32 retailPrice => "Retail price",
33 wholesalePrice => "Wholesale price",
37 sub generator { 'BSE::Generate::Product' }
39 sub _make_dummy_article {
40 my ($self, $article) = @_;
42 require BSE::DummyProduct;
43 return bless $article, "BSE::DummyProduct";
46 sub base_template_dirs {
47 return ( "products" );
51 my ($self, $article) = @_;
53 my @extras = $self->SUPER::extra_templates($article);
54 push @extras, 'shopitem.tmpl'
55 if grep -f "$_/shopitem.tmpl",
56 BSE::Template->template_dirs($self->{cfg});
58 my $extras = $self->{cfg}->entry('products', 'extra_templates');
59 push @extras, grep /\.(tmpl|html)$/i, split /,/, $extras
66 my ($article, $arg) = @_;
68 my $value = $article->{$arg};
69 defined $value or $value = '';
70 if ($value =~ /\cJ/ && $value =~ /\cM/) {
74 return encode_entities($value);
78 require BSE::TB::Subscriptions;
79 BSE::TB::Subscriptions->all;
82 sub iter_option_values {
83 my ($self, $rcurrent_option, $args) = @_;
88 return $$rcurrent_option->values;
92 my ($object, $args) = @_;
94 my $value = $object->{$args};
95 defined $value or $value = '';
96 if ($value =~ /\cJ/ && $value =~ /\cM/) {
99 escape_html($value, '<>&"');
102 sub tag_dboptionvalue_move {
103 my ($self, $req, $article, $rvalues, $rindex, $args) = @_;
105 $$rindex >= 0 && $$rindex < @$rvalues
106 or return "** dboptionvalue_move only in dboption_values iterator **";
108 my $my_id = $rvalues->[$$rindex]{id};
109 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&value_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option_value") . "&";
111 my $t = $req->cgi->param('_t');
113 and $base_url .= "_t=$t&";
117 $up_url = $base_url . "a_option_value_moveup=1";
120 if ($$rindex < $#$rvalues) {
121 $down_url = $base_url . "a_option_value_movedown=1";
124 my $refresh = $self->refresh_url($article, $req->cgi);
127 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptvaluemove");
130 sub tag_dboption_move {
131 my ($self, $req, $article, $roptions, $rindex, $args) = @_;
133 $$rindex >= 0 && $$rindex < @$roptions
134 or return "** dboption_move only in dboptions iterator **";
136 my $my_id = $roptions->[$$rindex]{id};
137 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&option_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option") . "&";
139 my $t = $req->cgi->param('_t');
141 and $base_url .= "_t=$t&";
145 $up_url = $base_url . "a_option_moveup=1";
148 if ($$rindex < $#$roptions) {
149 $down_url = $base_url . "a_option_movedown=1";
152 my $refresh = $self->refresh_url($article, $req->cgi);
155 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptmove");
159 my ($self, $rtier, $rprices, $product) = @_;
161 unless ($rprices->{loaded}) {
162 %$rprices = map { $_->tier_id => $_ } $product->prices
164 $rprices->{loaded} = 1;
167 $$rtier or return '** no current tier **';
169 exists $rprices->{$$rtier->id}
172 return $rprices->{$$rtier->id}->retailPrice;
176 my ($self, $req, $article, $data) = @_;
178 $self->_save_price_tiers($req, $article, $data);
179 $self->SUPER::save_more($req, $article, $data);
183 my ($self, $req, $article, $data) = @_;
185 $self->_save_price_tiers($req, $article, $data);
186 $self->SUPER::save_new_more($req, $article, $data);
189 sub _save_price_tiers {
190 my ($self, $req, $article, $data) = @_;
192 $data->{save_pricing_tiers}
195 $req->user_can('edit_field_edit_retailPrice', $article)
198 my @tiers = BSE::TB::Products->pricing_tiers;
200 for my $tier (@tiers) {
201 my $key = "tier_price_" . $tier->id;
202 if (exists $data->{$key} && $data->{$key} =~ /\S/) {
203 $prices{$tier->id} = $data->{$key} * 100;
206 $article->set_prices(\%prices);
210 my ($self, $table_object) = @_;
212 my @cols = $self->SUPER::save_columns($table_object);
213 my @tiers = BSE::TB::Products->pricing_tiers;
215 push @cols, "save_pricing_tiers";
216 push @cols, map { "tier_price_" . $_->id } @tiers;
223 my ($self, $article) = @_;
228 return $article->db_options;
233 These a tags available on admin/edit_* pages specific to products.
239 product I<field> - display the given field from the product being edited.
243 iterator begin dboptions ... dboption I<field> ... iterator end dboptions
245 - iterate over the existing database stored options for the product
249 dboption_move - display arrows to move the current dboption. The span
250 for the arrows is given an id of "prodoptmoveI<option-id>" by default.
254 iterator begin dboptionvalues ... dboptionvalue I<field> ... iterator end dboptionvalues
256 - iterate over the values for the current dboption
260 dboptionvalue_move - display arrows to move the current dboption. The
261 span for the arrows is given an id of "prodoptvaluemoveI<value-id>"
266 dboptionsjson - returns the product options as JSON.
270 iterator begin price_tiers ... price_tier I<field> ... iterator end price_tiers
272 Iterate over the configured price tiers.
278 Return the price at the current price_tier. Returns an empty string
279 if there's no price at this tier.
286 my ($self, $acts, $req, $article, $articles, $msg, $errors) = @_;
288 my $product_opts = product_options($req->cfg);
291 my $mbcs = $cfg->entry('html', 'mbcs', 0);
292 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&hash_tag;
297 my $dboption_value_index;
298 my $current_option_value;
299 my $it = BSE::Util::Iterate->new;
303 $req->set_variable(product => $article);
304 BSE::PubSub->customize(product_edit_variables => { req => $req, product => $article, errors => \$errors });
307 product => [ \&tag_article, $article, $cfg ],
308 $self->SUPER::low_edit_tags($acts, $req, $article, $articles, $msg,
310 alloptions => join(",", sort keys %$product_opts),
312 ([ \&iter_subs, $req ], 'subscription', 'subscriptions'),
315 single => "dboption",
316 plural => "dboptions",
317 store => \$current_option,
319 index => \$dboption_index,
320 code => [ iter_dboptions => $self, $article ],
325 $self, $req, $article, \@dboptions, \$dboption_index
329 single => "dboptionvalue",
330 plural => "dboptionvalues",
331 data => \@dboption_values,
332 index => \$dboption_value_index,
333 store => \$current_option_value,
334 code => [ iter_option_values => $self, \$current_option ],
337 dboptionsjson => [ tag_dboptionsjson => $self, $article ],
338 dboptionvalue_move =>
340 tag_dboptionvalue_move =>
341 $self, $req, $article, \@dboption_values, \$dboption_value_index
345 single => "price_tier",
346 plural => "price_tiers",
347 code => [ pricing_tiers => "BSE::TB::Products" ],
349 store => \$price_tier,
351 tier_price => [ tag_tier_price => $self, \$price_tier, \%prices, $article ],
356 my ($self, $article, $cgi) = @_;
358 my $base = 'product';
359 my $t = $cgi->param('_t');
360 if ($t && $t =~ /^\w+$/) {
363 return $self->{cfg}->entry('admin templates', $base,
368 my ($self, $article, $cgi) = @_;
370 return $self->{cfg}->entry('admin templates', 'add_product',
371 'admin/edit_product');
374 sub validate_parent {
375 my ($self, $data, $articles, $parent, $rmsg) = @_;
377 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
379 $parent->{generator} eq 'BSE::Generate::Catalog') {
380 $$rmsg = "Products must be in a catalog (not $parent->{generator})";
384 return $self->SUPER::validate_parent($data, $articles, $parent, $rmsg);
387 sub _validate_common {
388 my ($self, $data, $articles, $errors) = @_;
390 $self->SUPER::_validate_common($data, $articles, $errors);
392 for my $col (keys %money_fields) {
393 my $value = $data->{$col};
394 defined $value or next;
395 unless ($value =~ /^\d+(\.\d{1,2})?\s*/) {
396 $errors->{$col} = "$money_fields{$col} invalid";
400 if (defined $data->{options}) {
401 my $avail_options = product_options($self->{cfg});
403 my @bad_opts = grep !$avail_options->{$_},
404 split /,/, $data->{options};
406 $errors->{options} = "Bad product options '". join(",", @bad_opts)."' entered";
411 for my $sub_field (qw(subscription_id subscription_required)) {
412 my $value = $data->{$sub_field};
413 defined $value or next;
414 if ($value ne '-1') {
415 require BSE::TB::Subscriptions;
416 @subs = BSE::TB::Subscriptions->all unless @subs;
417 unless (grep $_->{subscription_id} == $value, @subs) {
418 $errors->{$sub_field} = "Invalid $sub_field value";
422 if (defined $data->{subscription_period}) {
423 my $sub = $data->{subscription_id};
424 if ($data->{subscription_period} !~ /^\d+$/) {
425 $errors->{subscription_period} = "Invalid subscription period, it must be the number of months to subscribe";
427 elsif ($sub != -1 && $data->{subscription_period} < 1) {
428 $errors->{subscription_period} = "Subscription period must be 1 or more when a subscription is selected";
431 if (defined $data->{subscription_usage}) {
432 unless ($data->{subscription_usage} =~ /^[123]$/) {
433 $errors->{subscription_usage} = "Invalid subscription usage";
437 if ($data->{save_pricing_tiers}) {
438 my @tiers = BSE::TB::Products->pricing_tiers;
439 for my $tier (@tiers) {
440 my $key = "tier_price_" . $tier->id;
441 my $value = $data->{$key};
442 defined $value or next;
443 if ($value =~ /\S/ && $value !~ /^\d+(\.\d{1,2})?\s*/) {
444 $errors->{$key} = 'Pricing tier "' . $tier->description . '" price invalid';
449 return !keys %$errors;
453 my ($self, $data, $articles, $errors) = @_;
455 my $ok = $self->SUPER::validate($data, $articles, $errors);
456 $self->_validate_common($data, $articles, $errors);
458 for my $field (qw(title)) {
459 unless ($data->{$field} =~ /\S/) {
460 $errors->{$field} = "No $field entered";
464 return $ok && !keys %$errors;
468 my ($self, $article, $data, $articles, $errors) = @_;
470 $self->SUPER::validate_old($article, $data, $articles, $errors)
473 return !keys %$errors;
476 sub possible_parents {
477 my ($self, $article, $articles) = @_;
482 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
483 # the parents of a catalog can be other catalogs or the shop
484 my $shop = $articles->getByPkey($shopid);
485 my @work = [ $shopid, $shop->{title} ];
487 my ($id, $title) = @{pop @work};
489 $labels{$id} = $title;
490 push @work, map [ $_->{id}, $title.' / '.$_->{title} ],
491 sort { $b->{displayOrder} <=> $a->{displayOrder} }
492 grep $_->{generator} eq 'BSE::Generate::Catalog',
493 $articles->getBy(parentid=>$id);
495 unless ($shop->{generator} eq 'BSE::Generate::Catalog') {
497 delete $labels{$shopid};
499 return (\@values, \%labels);
503 my ($self, $articles) = @_;
509 my ($self, $articles, $article) = @_;
511 return BSE::TB::Products->getByPkey($article->{id});
514 sub default_link_path {
515 my ($self, $article) = @_;
517 $self->{cfg}->entry('uri', 'shop', '/shop');
521 my ($self, $article) = @_;
528 if ($self->{cfg}->entry('shop', 'secureurl_articles', 1)) {
529 $urlbase = $self->{cfg}->entryVar('site', 'secureurl');
533 if ($article->is_dynamic) {
534 (my $extra = $article->title) =~ tr/A-Za-z0-9/-/sc;
535 return "$urlbase/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($extra);
538 my $shop_uri = $self->link_path($article);
539 return $urlbase.$shop_uri."/shop$article->{id}.html";
542 sub _fill_product_data {
543 my ($self, $req, $data, $src) = @_;
545 for my $money_col (qw(retailPrice wholesalePrice gst)) {
546 if (exists $src->{$money_col}) {
547 if ($src->{$money_col} =~ /^\d+(\.\d\d)?\s*/) {
548 $data->{$money_col} = 100 * $src->{$money_col};
551 $data->{$money_col} = 0;
555 if (exists $src->{leadTime}) {
556 $src->{leadTime} =~ /^\d+\s*$/
557 or $src->{leadTime} = 0;
558 $data->{leadTime} = $src->{leadTime};
560 if (exists $src->{description} && length $src->{description}) {
562 if ($req->user_can('edit_field_edit_description', $data)) {
563 $data->{description} = $src->{description};
567 if (exists $src->{product_code} && length $src->{product_code}) {
569 if ($req->user_can('edit_field_edit_product_code', $data)) {
570 $data->{product_code} = $src->{product_code};
574 for my $field (qw(options subscription_id subscription_period
575 subscription_usage subscription_required
576 weight length width height)) {
577 if (exists $src->{$field}) {
578 $data->{$field} = $src->{$field};
580 elsif ($data == $src) {
582 $data->{$field} = $self->default_value($req, $data, $field);
588 my ($self, $req, $data, $articles) = @_;
590 $self->_fill_product_data($req, $data, $data);
592 return $self->SUPER::fill_new_data($req, $data, $articles);
596 my ($self, $req, $article, $src) = @_;
598 $self->_fill_product_data($req, $article, $src);
600 return $self->SUPER::fill_old_data($req, $article, $src);
603 sub default_template {
604 my ($self, $article, $cfg, $templates) = @_;
606 my $template = $cfg->entry('products', 'template');
608 if $template && grep $_ eq $template, @$templates;
610 return $self->SUPER::default_template($article, $cfg, $templates);
616 return ( 'product flags', $self->SUPER::flag_sections );
619 sub shop_article { 1 }
625 subscription_id => -1,
626 subscription_required => -1,
627 subscription_period => 1,
628 subscription_usage => 3,
641 my ($self, $req, $article, $col) = @_;
643 my $value = $self->SUPER::default_value($req, $article, $col);
644 defined $value and return $value;
646 exists $defaults{$col} and return $defaults{$col};
651 sub type_default_value {
652 my ($self, $req, $col) = @_;
654 my $value = $req->cfg->entry('product defaults', $col);
655 defined $value and return $value;
657 return $self->SUPER::type_default_value($req, $col);
664 description => "Option name",
666 rules => "dh_one_line",
671 description => "Value 1",
672 rules => "dh_one_line",
679 Actions you can request from add.pl for products.
685 Add a new product option.
687 On failure perform a service error.
689 Requires _csrfp for admin_add_option
691 For Ajax requests (or with a _ parameter) returns JSON like:
695 option: { <option data> },
696 values: [ { value data }, { value data }, ... ]
709 name - Name of the option (required)
713 value1 .. value5 - if any of these are non-blank they are added to the
718 Permission required: bse_edit_prodopt_add
723 my ($self, $req, $article, $articles, $msg, $errors) = @_;
725 $req->check_csrf('admin_add_option')
726 or return $self->csrf_error($req, $article, "admin_add_option", "Add Product Option");
728 $req->user_can(bse_edit_prodopt_add => $article)
729 or return $self->_service_error($req, $article, $articles, "Insufficient product access to add options");
732 my %work_option_fields = %option_fields;
733 for my $field (grep /^value[1-9][0-9]*$/, $req->cgi->param) {
734 my ($index) = ( $field =~ /([1-9][0-9]*)$/);
735 $work_option_fields{$field} =
737 description => "Value $index",
738 rules => "dh_one_line",
742 $req->validate(fields => \%work_option_fields,
744 BSE::PubSub->customize(
745 product_option_add_validate =>
750 fields => \%work_option_fields,
753 and return $self->_service_error($req, $article, $articles, undef,
757 require BSE::TB::ProductOptions;
758 require BSE::TB::ProductOptionValues;
759 my $option = BSE::TB::ProductOptions->make
761 product_id => $article->{id},
762 name => scalar($cgi->param('name')),
763 display_order => time,
769 for my $value_key (sort grep /^value/, keys %work_option_fields) {
770 my ($value) = $cgi->param($value_key);
771 if (defined $value && $value =~ /\S/) {
772 my $entry = BSE::TB::ProductOptionValues->make
774 product_option_id => $option->{id},
776 display_order => $order,
778 push @values, $entry;
779 $value_keys{$value_key} = $entry;
783 my $def = $cgi->param("default_value");
784 if ($def && $value_keys{$def}) {
785 $option->set_default_value($value_keys{$def}->id);
789 BSE::PubSub->customize(
790 product_option_add =>
795 values => \%value_keys
799 and return $req->json_content
802 option => $option->data_only,
803 values => [ map $_->data_only, @values ]
806 return $self->refresh($article, $cgi, undef, "Option added");
813 rules => "required;positiveint",
818 my ($self, $req, $article, $errors) = @_;
822 $req->validate(fields => \%option_id,
824 my @option_ids = $cgi->param("option_id");
825 unless ($errors->{option_id}) {
827 or $errors->{option_id} = "This request accepts only one option_id";
829 unless ($errors->{option_id}) {
830 require BSE::TB::ProductOptions;
831 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
833 or $errors->{option_id} = "Unknown option id";
835 unless ($errors->{option_id}) {
836 $option->{product_id} = $article->{id}
837 or $errors->{option_id} = "Option doesn't belong to this product";
846 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
849 my $option = $self->_get_option($req, $article, \%errors);
851 and return $self->_service_error($req, $article, $articles, undef, \%errors);
853 if ($template =~ /edit/) {
854 BSE::PubSub->customize(
855 product_edit_option_edit => {
861 $req->set_variable(option => $option);
862 $req->messages($errors);
864 my $it = BSE::Util::Iterate->new;
868 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
869 option => [ \&tag_hash, $option ],
872 single => "dboptionvalue",
873 plural => "dboptionvalues",
874 code => [ iter_option_values => $self, \$option ],
878 return $req->dyn_response($template, \%acts);
883 Produce a form to edit the given option.
895 option_id - option id. This must belong to the product identified by
900 Template: admin/prodopt_edit
902 Permission required: bse_edit_prodopt_edit
906 sub req_edit_option {
907 my ($self, $req, $article, $articles, $msg, $errors) = @_;
909 $req->user_can(bse_edit_prodopt_edit => $article)
910 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
913 return $self->_common_option('admin/prodopt_edit', $req, $article,
914 $articles, $msg, $errors);
921 description => "Option name",
922 rules => "required;dh_one_line",
927 description => "Default Value",
928 rules => "positiveint"
934 description => "Value",
935 rules => "required;dh_one_line",
941 Saves changes to an option.
943 On failure perform a service error.
945 Requires _csrfp for admin_save_option
947 For Ajax requests (or with a _ parameter), returns JSON like:
951 option: { <option data> },
952 values: [ { value data, value data, ... } ]
965 option_id - id of the option to save, must belong to the product
970 name - new value for the name field
974 default_value - id of the default value
978 save_enabled - if supplied and true, set enabled from the enabled
983 enabled - If supplied and true, enable the option, otherwise disable
984 it. Ignored unless save_enabled is true.
988 valueI<value-id> - set the displayed value for the value record
989 identified by I<value-id>. If these aren't supplied the values aren't
994 Permission required: bse_edit_prodopt_save
998 sub req_save_option {
999 my ($self, $req, $article, $articles) = @_;
1001 my $cgi = $req->cgi;
1003 $req->check_csrf("admin_save_option")
1004 or return $self->csrf_error($req, $article, "admin_save_option", "Save Product Option");
1006 $req->user_can(bse_edit_prodopt_edit => $article)
1007 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1010 my $option = $self->_get_option($req, $article, \%errors);
1012 and return $self->_service_error($req, $article, $articles, undef, \%errors, 'FIELD', "req_edit_option");
1013 $req->validate(fields => \%option_name,
1014 errors => \%errors);
1015 BSE::PubSub->customize(
1016 product_option_edit_validate =>
1020 product => $article,
1023 my @values = $option->values;
1024 my %fields = map {; "value$_->{id}" => \%option_value } @values;
1025 $req->validate(fields => \%fields,
1028 my $default_value = $cgi->param('default_value');
1029 if (!$errors{default_value} && $default_value) {
1030 grep $_->{id} == $default_value, @values
1031 or $errors{default_value} = "Unknown value selected as default";
1036 my @newvalue_fields = grep /^newvalue[1-9][0-9]*$/, $cgi->param;
1037 for my $field (@newvalue_fields) {
1038 my $value = $cgi->param($field);
1039 $req->validate(fields => { $field => \%option_value },
1040 errors => \%errors);
1041 push @new_values, [ $field, $value ];
1047 and return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD", "req_edit_option");
1049 my $name = $cgi->param("name");
1051 and $option->set_name($name);
1052 defined $default_value
1053 and $option->set_default_value($default_value);
1054 if ($cgi->param("save_enabled")) {
1055 my $enabled = $cgi->param("enabled") ? 1 : 0;
1056 $option->set_enabled($enabled);
1059 for my $value (@values) {
1060 my $new_value = $cgi->param("value$value->{id}");
1061 if (defined $new_value && $new_value ne $value->value) {
1062 $value->set_value($new_value);
1066 my $order = @values ? $values[-1]->display_order : time;
1068 for my $new (@new_values) {
1069 my ($name, $value) = @$new;
1070 $newvalues{$name} = BSE::TB::ProductOptionValues->make
1072 product_option_id => $option->id,
1074 display_order => ++$order,
1077 BSE::PubSub->customize(
1078 product_option_edit_save =>
1081 product => $article,
1083 newvalues => \%newvalues,
1087 and return $req->json_content
1090 option => $option->data_only,
1091 values => [ map $_->data_only, @values ],
1094 return $self->refresh($article, $req->cgi, undef,
1095 "Option '" . $option->name . "' saved");
1098 =item a_delconf_option
1100 Produce a form to confirm deletion of the given option.
1112 option_id - option id. This must belong to the product identified by
1117 Template: admin/prodopt_delete
1121 sub req_delconf_option {
1122 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1124 $req->user_can(bse_edit_prodopt_delete => $article)
1125 or return $self->_service_error($req, $article, $articles, "Insufficient product access to delete options");
1127 return $self->_common_option('admin/prodopt_delete', $req, $article,
1128 $articles, $msg, $errors);
1131 =item a_delete_option
1133 Delete the given option.
1135 On failure perform a service error.
1137 Requires _csrfp for admin_delete_option
1139 For Ajax requests (or with a _ parameter), returns JSON like:
1145 Permission required: bse_edit_prodopt_delete
1149 sub req_delete_option {
1150 my ($self, $req, $article, $articles) = @_;
1152 $req->check_csrf("admin_delete_option")
1153 or return $self->csrf_error($req, $article, "admin_delete_option", "Delete Product Option");
1155 $req->user_can(bse_edit_prodopt_delete => $article)
1156 or return $self->_service_error($req, $article, $articles, "Insufficient product access to delete options");
1159 my $option = $self->_get_option($req, $article, \%errors);
1161 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1162 my @values = $option->values;
1164 for my $value (@values) {
1170 and return $req->json_content
1175 return $self->refresh($article, $req->cgi, undef, "Option deleted");
1179 my %add_option_value_fields =
1183 description => "Option id",
1184 rules => "required;positiveint",
1188 description => "Value",
1189 rules => "required;dh_one_line",
1194 =item a_add_option_value
1196 Add a value to a product option.
1198 On failure perform a service error, see BSE::Edit::Article::_service_error.
1200 Requires _csrfp for admin_add_option_value
1202 For Ajax requests returns JSON like
1204 { success: 1, value: (valueobject) }
1206 Standard redirect on success otherwise.
1218 option_id - id of the option to add the value to
1222 value - text of the value to add.
1226 Permission required: bse_edit_prodopt_edit
1230 sub req_add_option_value {
1231 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1233 $req->check_csrf("admin_add_option_value")
1234 or return $self->csrf_error($req, $article, "admin_add_option_value", "Add Product Option Value");
1236 $req->user_can(bse_edit_prodopt_edit => $article)
1237 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1240 $req->validate(fields => \%add_option_value_fields,
1241 errors => \%errors);
1243 my $cgi = $req->cgi;
1244 unless ($errors{option_id}) {
1245 require BSE::TB::ProductOptions;
1246 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
1247 defined $option && $option->{product_id}
1248 or $errors{option_id} = "Bad option id - either unknown or for a different product";
1251 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1253 my $value = $cgi->param("value");
1254 require BSE::TB::ProductOptionValues;
1255 my $entry = BSE::TB::ProductOptionValues->make
1257 product_option_id => $option->{id},
1259 display_order => time,
1263 and return $req->json_content
1266 value => $entry->data_only
1269 return $self->refresh($article, $cgi, undef, "Value added");
1273 my %option_value_id =
1277 rules => "required;positiveint",
1281 sub _get_option_value {
1282 my ($self, $req, $article, $errors) = @_;
1285 my $cgi = $req->cgi;
1286 $req->validate(fields => \%option_value_id,
1288 unless ($errors->{value_id}) {
1289 require BSE::TB::ProductOptionValues;
1290 $option_value = BSE::TB::ProductOptionValues->getByPkey($cgi->param("value_id"));
1292 or $errors->{value_id} = "Unknown option value id";
1295 unless ($errors->{value_id}) {
1296 $option = $option_value->option;
1297 defined $option && $option->{product_id} == $article->{id}
1298 or $errors->{value_id} = "Value has no option or doesn't belong to the product";
1304 return wantarray ? ( $option_value, $option ) : $option_value ;
1307 sub _common_option_value {
1308 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
1311 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1313 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1315 $req->set_variable(option => $option);
1316 $req->set_variable(option_value => $option_value);
1320 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
1321 option_value => [ \&tag_hash, $option_value ],
1322 option => [ \&tag_hash, $option ],
1325 return $req->dyn_response($template, \%acts);
1328 =item a_edit_option_value
1330 Displays a form to edit the value for a given option.
1338 id - id of the product
1342 value_id - id of he product option value to edit, must belong to the
1347 Template: admin/prodopt_value_edit
1349 Permission required: bse_edit_prodopt_edit
1353 sub req_edit_option_value {
1354 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1356 $req->user_can(bse_edit_prodopt_edit => $article)
1357 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1359 return $self->_common_option_value('admin/prodopt_value_edit', $req,
1360 $article, $articles, $msg, $errors);
1363 my %save_option_value_fields =
1367 rules => "required;dh_one_line",
1372 =item a_save_option_value
1374 Saves changes to an option.
1376 On failure perform a service error.
1378 Requires _csrfp for admin_save_option_value
1380 For Ajax requests (or with a _ parameter), returns JSON like:
1384 value: { value data }
1397 value_id - id of the value to save, must belong to the product
1402 value - new displayed value for the option value.
1406 Permission required: bse_edit_prodopt_edit
1410 sub req_save_option_value {
1411 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1413 $req->check_csrf("admin_save_option_value")
1414 or return $self->csrf_error($req, $article, "admin_save_option_value", "Save Product Option Value");
1416 $req->user_can(bse_edit_prodopt_edit => $article)
1417 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1420 $req->validate(fields => \%save_option_value_fields,
1421 errors => \%errors);
1422 my $option_value = $self->_get_option_value($req, $article, \%errors);
1424 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1426 my $cgi = $req->cgi;
1427 $option_value->{value} = $cgi->param("value");
1428 $option_value->save;
1431 and return $req->json_content
1434 value => $option_value->data_only
1437 return $self->refresh($article, $cgi, undef, "Value saved");
1440 =item a_confdel_option_value
1442 Displays a page confirming deletion of a product option value.
1454 value_id - option value id
1458 Template: admin/prodopt_value_delete
1460 Permission required: bse_edit_prodopt_edit
1464 sub req_confdel_option_value {
1465 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1467 $req->user_can(bse_edit_prodopt_edit => $article)
1468 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1470 return $self->_common_option_value('admin/prodopt_value_delete', $req,
1471 $article, $articles, $msg, $errors);
1474 =item a_delete_option_value
1476 Deletes a product option.
1478 On failure perform a service error.
1480 Requires _csrfp for admin_delete_option_value
1482 For Ajax requests (or with a _ parameter), returns JSON like:
1498 value_id - id of the value to delete, must belong to the product
1503 Permission required: bse_edit_prodopt_edit
1507 sub req_delete_option_value {
1508 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1510 $req->check_csrf("admin_delete_option_value")
1511 or return $self->csrf_error($req, $article, "admin_delete_option_value", "Delete Product Option Value");
1513 $req->user_can(bse_edit_prodopt_edit => $article)
1514 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1517 my $option_value = $self->_get_option_value($req, $article, \%errors);
1519 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1521 $option_value->remove;
1524 and return $req->json_content
1529 return $self->refresh($article, $req->cgi, undef, "Value removed");
1532 sub tag_dboptionsjson {
1533 my ($self, $article) = @_;
1536 my @options = $article->db_options;
1537 my @opt_cols = BSE::TB::ProductOption->columns;
1538 for my $option (@options) {
1539 my $entry = $option->data_only;
1540 $entry->{values} = [ map $_->data_only, $option->values ];
1541 push @result, $entry;
1545 my $json = JSON->new;
1546 return $json->encode(\@result);
1550 my ($self, $req, $article, $articles, $direction) = @_;
1552 $req->check_csrf("admin_move_option")
1553 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1555 $req->user_can(bse_edit_prodopt_move => $article)
1556 or return $self->_service_error($req, $article, $articles, "Insufficient product access to move options");
1559 my $option = $self->_get_option($req, $article, \%errors);
1561 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1562 my @options = $article->db_options;
1563 my ($index) = grep $options[$_]{id} == $option->{id}, 0 .. $#options
1564 or return $self->_service_error($req, $article, $articles, "Unknown option id");
1566 $options[$index] = $option;
1568 my $other_index = $index + $direction;
1569 $other_index >= 0 && $other_index < @options
1570 or return $self->_service_error($req, $article, $articles, "Can't move option beyond end");
1572 my $other = $options[$other_index];
1574 ($option->{display_order}, $other->{display_order}) =
1575 ($other->{display_order}, $option->{display_order});
1579 if ($req->is_ajax) {
1580 @options = sort { $a->{display_order} <=> $b->{display_order} } @options;
1581 return return $req->json_content
1584 order => [ map $_->{id}, @options ]
1588 return $self->refresh($article, $req->cgi, undef, "Option moved");
1591 =item a_option_moveup
1593 =item a_option_movedown
1595 Move a product option up/down through the options for a product.
1597 On failure perform a service error.
1599 Requires _csrfp for admin_move_option
1601 For Ajax requests (or with a _ parameter), returns JSON like:
1605 order: [ list of option ids ]
1618 option_id - option id. This must belong to the product identified by
1623 Permission required: bse_edit_prodopt_move
1627 sub req_option_moveup {
1628 my ($self, $req, $article, $articles) = @_;
1630 return $self->_option_move($req, $article, $articles, -1);
1633 sub req_option_movedown {
1634 my ($self, $req, $article, $articles) = @_;
1636 return $self->_option_move($req, $article, $articles, 1);
1639 =item a_option_reorder
1641 Move a product option up/down through the options for a product.
1643 On failure perform a service error.
1645 Requires _csrfp for admin_move_option
1647 For Ajax requests (or with a _ parameter), returns JSON like:
1651 order: [ list of option ids ]
1664 option_ids - option ids separated by commas. These must belong to the
1665 product identified by id.
1669 Permission required: bse_edit_prodopt_move
1673 sub req_option_reorder {
1674 my ($self, $req, $article, $articles) = @_;
1676 $req->check_csrf("admin_move_option")
1677 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1679 $req->user_can(bse_edit_prodopt_move => $article)
1680 or return $self->_service_error($req, $article, $articles, "Insufficient product access to move options");
1682 my @options = $article->db_options;
1683 my @order = map { split ',' } $req->cgi->param('option_ids');
1684 my %options = map { $_->{id} => $_ } @options;
1686 for my $id (@order) {
1687 my $option = delete $options{$id}
1689 push @new_options, $option;
1691 push @new_options, sort { $a->{display_order} <=> $b->{display_order} } values %options;
1692 my @display_order = map $_->{display_order}, @options;
1693 for my $index (0 .. $#new_options) {
1694 $new_options[$index]{display_order} = $display_order[$index];
1695 $new_options[$index]->save;
1699 and return $req->json_content
1702 order => [ map $_->{id}, @new_options ]
1705 return $self->refresh($article, $req->cgi, undef, "Options reordered");
1708 sub _option_value_move {
1709 my ($self, $req, $article, $articles, $direction) = @_;
1711 $req->check_csrf("admin_move_option_value")
1712 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1714 $req->user_can(bse_edit_prodopt_edit => $article)
1715 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1718 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1720 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1721 my @values = $option->values;
1722 my ($index) = grep $values[$_]{id} == $option_value->{id}, 0 .. $#values
1723 or return $self->_service_error($req, $article, $articles, "Unknown option value id");
1725 $values[$index] = $option_value;
1727 my $other_index = $index + $direction;
1728 $other_index >= 0 && $other_index < @values
1729 or return $self->_service_error($req, $article, $articles, "Can't move option value beyond end");
1731 my $other = $values[$other_index];
1733 ($option_value->{display_order}, $other->{display_order}) =
1734 ($other->{display_order}, $option_value->{display_order});
1735 $option_value->save;
1738 # make sure the json gets the new order
1739 @values[$index, $other_index] = @values[$other_index, $index];
1742 and return $req->json_content
1745 order => [ map $_->{id}, @values ]
1748 return $self->refresh($article, $req->cgi, undef, "Value moved");
1751 =item a_option_value_moveup
1753 =item a_option_value_movedown
1755 Move a product option value up/down through the values for a product
1758 On failure perform a service error.
1760 Requires _csrfp for admin_move_option_value
1762 For Ajax requests (or with a _ parameter), returns JSON like:
1766 order: [ list of value ids ]
1779 value_id - option id. This must belong to the product identified by
1784 Permission required: bse_edit_prodopt_edit
1788 sub req_option_value_moveup {
1789 my ($self, $req, $article, $articles) = @_;
1791 return $self->_option_value_move($req, $article, $articles, -1);
1794 sub req_option_value_movedown {
1795 my ($self, $req, $article, $articles) = @_;
1797 return $self->_option_value_move($req, $article, $articles, 1);
1800 =item a_option_value_reorder
1802 Specify a new order for the values belonging to a product option.
1804 On failure perform a service error.
1806 Requires _csrfp for admin_move_option_value
1808 For Ajax requests (or with a _ parameter), returns JSON like:
1812 order: [ list of value ids ]
1825 option_id - the option to reorder values for
1829 value_ids - new order for values specified as value ids separated by
1834 Permission required: bse_edit_prodopt_edit
1838 sub req_option_value_reorder {
1839 my ($self, $req, $article, $articles) = @_;
1841 $req->check_csrf("admin_move_option_value")
1842 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1844 $req->user_can(bse_edit_prodopt_edit => $article)
1845 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1848 my $option = $self->_get_option($req, $article, \%errors);
1850 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1851 my @order = map { split ',' } $req->cgi->param('value_ids');
1852 my @values = $option->values;
1853 my %values = map { $_->{id} => $_ } @values;
1855 for my $id (@order) {
1856 my $value = delete $values{$id}
1858 push @new_values, $value;
1860 push @new_values, sort { $a->{display_order} <=> $b->{display_order} } values %values;
1861 my @display_order = map $_->{display_order}, @values;
1862 for my $index (0 .. $#new_values) {
1863 $new_values[$index]{display_order} = $display_order[$index];
1864 $new_values[$index]->save;
1868 and return $req->json_content
1871 option => $option->data_only,
1872 order => [ map $_->{id}, @new_values ]
1875 return $self->refresh($article, $req->cgi, undef, "Values reordered");
1881 my $custom = $self->SUPER::custom_fields();
1883 require DevHelp::Validate;
1884 DevHelp::Validate->import;
1885 return DevHelp::Validate::dh_configure_fields
1889 PRODUCT_CUSTOM_FIELDS_CFG,
1890 BSE::DB->single->dbh,
1894 sub article_actions {
1899 $self->SUPER::article_actions,
1900 a_add_option => 'req_add_option',
1901 a_confdel_option => 'req_confdel_option',
1902 a_del_option => 'req_del_option',
1903 a_edit_option => 'req_edit_option',
1904 a_save_option => 'req_save_option',
1905 a_delconf_option => 'req_delconf_option',
1906 a_delete_option => 'req_delete_option',
1907 a_get_option => 'req_get_option',
1908 a_edit_option_value => 'req_edit_option_value',
1909 a_save_option_value => 'req_save_option_value',
1910 a_confdel_option_value => 'req_confdel_option_value',
1911 a_delete_option_value => 'req_delete_option_value',
1912 a_add_option_value => 'req_add_option_value',
1913 a_option_value_moveup => 'req_option_value_moveup',
1914 a_option_value_movedown => 'req_option_value_movedown',
1915 a_option_value_reorder => 'req_option_value_reorder',
1916 a_option_moveup => 'req_option_moveup',
1917 a_option_movedown => 'req_option_movedown',
1918 a_option_reorder => 'req_option_reorder',
1928 Tony Cook <tony@develop-help.com>