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);
11 use constant PRODUCT_CUSTOM_FIELDS_CFG => "product custom fields";
13 our $VERSION = "1.015";
17 BSE::Edit::Product - tags and actions for editing BSE products
21 http://www.example.com/cgi-bin/admin/add.pl ...
25 Article editor subclass for editing Products.
31 retailPrice => "Retail price",
32 wholesalePrice => "Wholesale price",
36 sub generator { 'BSE::Generate::Product' }
38 sub base_template_dirs {
39 return ( "products" );
43 my ($self, $article) = @_;
45 my @extras = $self->SUPER::extra_templates($article);
46 push @extras, 'shopitem.tmpl'
47 if grep -f "$_/shopitem.tmpl",
48 BSE::Template->template_dirs($self->{cfg});
50 my $extras = $self->{cfg}->entry('products', 'extra_templates');
51 push @extras, grep /\.(tmpl|html)$/i, split /,/, $extras
58 my ($article, $arg) = @_;
60 my $value = $article->{$arg};
61 defined $value or $value = '';
62 if ($value =~ /\cJ/ && $value =~ /\cM/) {
66 return encode_entities($value);
70 require BSE::TB::Subscriptions;
71 BSE::TB::Subscriptions->all;
74 sub iter_option_values {
75 my ($self, $rcurrent_option, $args) = @_;
80 return $$rcurrent_option->values;
84 my ($object, $args) = @_;
86 my $value = $object->{$args};
87 defined $value or $value = '';
88 if ($value =~ /\cJ/ && $value =~ /\cM/) {
91 escape_html($value, '<>&"');
94 sub tag_dboptionvalue_move {
95 my ($self, $req, $article, $rvalues, $rindex, $args) = @_;
97 $$rindex >= 0 && $$rindex < @$rvalues
98 or return "** dboptionvalue_move only in dboption_values iterator **";
100 my $my_id = $rvalues->[$$rindex]{id};
101 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&value_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option_value") . "&";
103 my $t = $req->cgi->param('_t');
105 and $base_url .= "_t=$t&";
109 $up_url = $base_url . "a_option_value_moveup=1";
112 if ($$rindex < $#$rvalues) {
113 $down_url = $base_url . "a_option_value_movedown=1";
116 my $refresh = $self->refresh_url($article, $req->cgi);
119 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptvaluemove");
122 sub tag_dboption_move {
123 my ($self, $req, $article, $roptions, $rindex, $args) = @_;
125 $$rindex >= 0 && $$rindex < @$roptions
126 or return "** dboption_move only in dboptions iterator **";
128 my $my_id = $roptions->[$$rindex]{id};
129 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&option_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option") . "&";
131 my $t = $req->cgi->param('_t');
133 and $base_url .= "_t=$t&";
137 $up_url = $base_url . "a_option_moveup=1";
140 if ($$rindex < $#$roptions) {
141 $down_url = $base_url . "a_option_movedown=1";
144 my $refresh = $self->refresh_url($article, $req->cgi);
147 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptmove");
151 my ($self, $rtier, $rprices, $product) = @_;
153 unless ($rprices->{loaded}) {
154 %$rprices = map { $_->tier_id => $_ } $product->prices
156 $rprices->{loaded} = 1;
159 $$rtier or return '** no current tier **';
161 exists $rprices->{$$rtier->id}
164 return $rprices->{$$rtier->id}->retailPrice;
168 my ($self, $req, $article, $data) = @_;
170 $self->_save_price_tiers($req, $article, $data);
171 $self->SUPER::save_more($req, $article, $data);
175 my ($self, $req, $article, $data) = @_;
177 $self->_save_price_tiers($req, $article, $data);
178 $self->SUPER::save_new_more($req, $article, $data);
181 sub _save_price_tiers {
182 my ($self, $req, $article, $data) = @_;
184 $data->{save_pricing_tiers}
187 $req->user_can('edit_field_edit_retailPrice', $article)
190 my @tiers = BSE::TB::Products->pricing_tiers;
192 for my $tier (@tiers) {
193 my $key = "tier_price_" . $tier->id;
194 if (exists $data->{$key} && $data->{$key} =~ /\S/) {
195 $prices{$tier->id} = $data->{$key} * 100;
198 $article->set_prices(\%prices);
202 my ($self, $table_object) = @_;
204 my @cols = $self->SUPER::save_columns($table_object);
205 my @tiers = BSE::TB::Products->pricing_tiers;
207 push @cols, "save_pricing_tiers";
208 push @cols, map { "tier_price_" . $_->id } @tiers;
215 my ($self, $article) = @_;
220 return $article->db_options;
225 These a tags available on admin/edit_* pages specific to products.
231 product I<field> - display the given field from the product being edited.
235 iterator begin dboptions ... dboption I<field> ... iterator end dboptions
237 - iterate over the existing database stored options for the product
241 dboption_move - display arrows to move the current dboption. The span
242 for the arrows is given an id of "prodoptmoveI<option-id>" by default.
246 iterator begin dboptionvalues ... dboptionvalue I<field> ... iterator end dboptionvalues
248 - iterate over the values for the current dboption
252 dboptionvalue_move - display arrows to move the current dboption. The
253 span for the arrows is given an id of "prodoptvaluemoveI<value-id>"
258 dboptionsjson - returns the product options as JSON.
262 iterator begin price_tiers ... price_tier I<field> ... iterator end price_tiers
264 Iterate over the configured price tiers.
270 Return the price at the current price_tier. Returns an empty string
271 if there's no price at this tier.
278 my ($self, $acts, $req, $article, $articles, $msg, $errors) = @_;
280 my $product_opts = product_options($req->cfg);
283 my $mbcs = $cfg->entry('html', 'mbcs', 0);
284 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&hash_tag;
289 my $dboption_value_index;
290 my $current_option_value;
291 my $it = BSE::Util::Iterate->new;
297 product => [ \&tag_article, $article, $cfg ],
298 $self->SUPER::low_edit_tags($acts, $req, $article, $articles, $msg,
300 alloptions => join(",", sort keys %$product_opts),
302 ([ \&iter_subs, $req ], 'subscription', 'subscriptions'),
305 single => "dboption",
306 plural => "dboptions",
307 store => \$current_option,
309 index => \$dboption_index,
310 code => [ iter_dboptions => $self, $article ],
315 $self, $req, $article, \@dboptions, \$dboption_index
319 single => "dboptionvalue",
320 plural => "dboptionvalues",
321 data => \@dboption_values,
322 index => \$dboption_value_index,
323 store => \$current_option_value,
324 code => [ iter_option_values => $self, \$current_option ],
327 dboptionsjson => [ tag_dboptionsjson => $self, $article ],
328 dboptionvalue_move =>
330 tag_dboptionvalue_move =>
331 $self, $req, $article, \@dboption_values, \$dboption_value_index
335 single => "price_tier",
336 plural => "price_tiers",
337 code => [ pricing_tiers => "BSE::TB::Products" ],
339 store => \$price_tier,
341 tier_price => [ tag_tier_price => $self, \$price_tier, \%prices, $article ],
346 my ($self, $article, $cgi) = @_;
348 my $base = 'product';
349 my $t = $cgi->param('_t');
350 if ($t && $t =~ /^\w+$/) {
353 return $self->{cfg}->entry('admin templates', $base,
358 my ($self, $article, $cgi) = @_;
360 return $self->{cfg}->entry('admin templates', 'add_product',
361 'admin/edit_product');
364 sub validate_parent {
365 my ($self, $data, $articles, $parent, $rmsg) = @_;
367 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
369 $parent->{generator} eq 'BSE::Generate::Catalog') {
370 $$rmsg = "Products must be in a catalog (not $parent->{generator})";
374 return $self->SUPER::validate_parent($data, $articles, $parent, $rmsg);
377 sub _validate_common {
378 my ($self, $data, $articles, $errors) = @_;
380 $self->SUPER::_validate_common($data, $articles, $errors);
382 for my $col (keys %money_fields) {
383 my $value = $data->{$col};
384 defined $value or next;
385 unless ($value =~ /^\d+(\.\d{1,2})?\s*/) {
386 $errors->{$col} = "$money_fields{$col} invalid";
390 if (defined $data->{options}) {
391 my $avail_options = product_options($self->{cfg});
393 my @bad_opts = grep !$avail_options->{$_},
394 split /,/, $data->{options};
396 $errors->{options} = "Bad product options '". join(",", @bad_opts)."' entered";
401 for my $sub_field (qw(subscription_id subscription_required)) {
402 my $value = $data->{$sub_field};
403 defined $value or next;
404 if ($value ne '-1') {
405 require BSE::TB::Subscriptions;
406 @subs = BSE::TB::Subscriptions->all unless @subs;
407 unless (grep $_->{subscription_id} == $value, @subs) {
408 $errors->{$sub_field} = "Invalid $sub_field value";
412 if (defined $data->{subscription_period}) {
413 my $sub = $data->{subscription_id};
414 if ($data->{subscription_period} !~ /^\d+$/) {
415 $errors->{subscription_period} = "Invalid subscription period, it must be the number of months to subscribe";
417 elsif ($sub != -1 && $data->{subscription_period} < 1) {
418 $errors->{subscription_period} = "Subscription period must be 1 or more when a subscription is selected";
421 if (defined $data->{subscription_usage}) {
422 unless ($data->{subscription_usage} =~ /^[123]$/) {
423 $errors->{subscription_usage} = "Invalid subscription usage";
427 if ($data->{save_pricing_tiers}) {
428 my @tiers = BSE::TB::Products->pricing_tiers;
429 for my $tier (@tiers) {
430 my $key = "tier_price_" . $tier->id;
431 my $value = $data->{$key};
432 defined $value or next;
433 if ($value =~ /\S/ && $value !~ /^\d+(\.\d{1,2})?\s*/) {
434 $errors->{$key} = 'Pricing tier "' . $tier->description . '" price invalid';
439 return !keys %$errors;
443 my ($self, $data, $articles, $errors) = @_;
445 my $ok = $self->SUPER::validate($data, $articles, $errors);
446 $self->_validate_common($data, $articles, $errors);
448 for my $field (qw(title)) {
449 unless ($data->{$field} =~ /\S/) {
450 $errors->{$field} = "No $field entered";
454 return $ok && !keys %$errors;
458 my ($self, $article, $data, $articles, $errors) = @_;
460 $self->SUPER::validate_old($article, $data, $articles, $errors)
463 return !keys %$errors;
466 sub possible_parents {
467 my ($self, $article, $articles) = @_;
472 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
473 # the parents of a catalog can be other catalogs or the shop
474 my $shop = $articles->getByPkey($shopid);
475 my @work = [ $shopid, $shop->{title} ];
477 my ($id, $title) = @{pop @work};
479 $labels{$id} = $title;
480 push @work, map [ $_->{id}, $title.' / '.$_->{title} ],
481 sort { $b->{displayOrder} <=> $a->{displayOrder} }
482 grep $_->{generator} eq 'BSE::Generate::Catalog',
483 $articles->getBy(parentid=>$id);
485 unless ($shop->{generator} eq 'BSE::Generate::Catalog') {
487 delete $labels{$shopid};
489 return (\@values, \%labels);
493 my ($self, $articles) = @_;
499 my ($self, $articles, $article) = @_;
501 return BSE::TB::Products->getByPkey($article->{id});
504 sub default_link_path {
505 my ($self, $article) = @_;
507 $self->{cfg}->entry('uri', 'shop', '/shop');
511 my ($self, $article) = @_;
518 if ($self->{cfg}->entry('shop', 'secureurl_articles', 1)) {
519 $urlbase = $self->{cfg}->entryVar('site', 'secureurl');
523 if ($article->is_dynamic) {
524 (my $extra = $article->title) =~ tr/A-Za-z0-9/-/sc;
525 return "$urlbase/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($extra);
528 my $shop_uri = $self->link_path($article);
529 return $urlbase.$shop_uri."/shop$article->{id}.html";
532 sub _fill_product_data {
533 my ($self, $req, $data, $src) = @_;
535 for my $money_col (qw(retailPrice wholesalePrice gst)) {
536 if (exists $src->{$money_col}) {
537 if ($src->{$money_col} =~ /^\d+(\.\d\d)?\s*/) {
538 $data->{$money_col} = 100 * $src->{$money_col};
541 $data->{$money_col} = 0;
545 if (exists $src->{leadTime}) {
546 $src->{leadTime} =~ /^\d+\s*$/
547 or $src->{leadTime} = 0;
548 $data->{leadTime} = $src->{leadTime};
550 if (exists $src->{description} && length $src->{description}) {
552 if ($req->user_can('edit_field_edit_description', $data)) {
553 $data->{description} = $src->{description};
557 if (exists $src->{product_code} && length $src->{product_code}) {
559 if ($req->user_can('edit_field_edit_product_code', $data)) {
560 $data->{product_code} = $src->{product_code};
564 for my $field (qw(options subscription_id subscription_period
565 subscription_usage subscription_required
566 weight length width height)) {
567 if (exists $src->{$field}) {
568 $data->{$field} = $src->{$field};
570 elsif ($data == $src) {
572 $data->{$field} = $self->default_value($req, $data, $field);
578 my ($self, $req, $data, $articles) = @_;
580 $self->_fill_product_data($req, $data, $data);
582 return $self->SUPER::fill_new_data($req, $data, $articles);
586 my ($self, $req, $article, $src) = @_;
588 $self->_fill_product_data($req, $article, $src);
590 return $self->SUPER::fill_old_data($req, $article, $src);
593 sub default_template {
594 my ($self, $article, $cfg, $templates) = @_;
596 my $template = $cfg->entry('products', 'template');
598 if $template && grep $_ eq $template, @$templates;
600 return $self->SUPER::default_template($article, $cfg, $templates);
606 return ( 'product flags', $self->SUPER::flag_sections );
609 sub shop_article { 1 }
615 subscription_id => -1,
616 subscription_required => -1,
617 subscription_period => 1,
618 subscription_usage => 3,
631 my ($self, $req, $article, $col) = @_;
633 my $value = $self->SUPER::default_value($req, $article, $col);
634 defined $value and return $value;
636 exists $defaults{$col} and return $defaults{$col};
641 sub type_default_value {
642 my ($self, $req, $col) = @_;
644 my $value = $req->cfg->entry('product defaults', $col);
645 defined $value and return $value;
647 return $self->SUPER::type_default_value($req, $col);
654 description => "Option name",
656 rules => "dh_one_line",
661 description => "Value 1",
662 rules => "dh_one_line",
667 description => "Value 2",
668 rules => "dh_one_line",
673 description => "Value 3",
674 rules => "dh_one_line",
679 description => "Value 4",
680 rules => "dh_one_line",
685 description => "Value 5",
686 rules => "dh_one_line",
693 Actions you can request from add.pl for products.
699 Add a new product option.
701 On failure perform a service error.
703 Requires _csrfp for admin_add_option
705 For Ajax requests (or with a _ parameter) returns JSON like:
709 option: { <option data> },
710 values: [ { value data }, { value data }, ... ]
723 name - Name of the option (required)
727 value1 .. value5 - if any of these are non-blank they are added to the
732 Permission required: bse_edit_prodopt_add
737 my ($self, $req, $article, $articles, $msg, $errors) = @_;
739 $req->check_csrf('admin_add_option')
740 or return $self->csrf_error($req, $article, "admin_add_option", "Add Product Option");
742 $req->user_can(bse_edit_prodopt_add => $article)
743 or return $self->_service_error($req, $article, $articles, "Insufficient product access to add options");
746 $req->validate(fields => \%option_fields,
749 and return $self->_service_error($req, $article, $articles, undef,
753 require BSE::TB::ProductOptions;
754 require BSE::TB::ProductOptionValues;
755 my $option = BSE::TB::ProductOptions->make
757 product_id => $article->{id},
758 name => scalar($cgi->param('name')),
759 display_order => time,
764 for my $value_key (sort grep /^value/, keys %option_fields) {
765 my ($value) = $cgi->param($value_key);
766 if (defined $value && $value =~ /\S/) {
767 my $entry = BSE::TB::ProductOptionValues->make
769 product_option_id => $option->{id},
771 display_order => $order,
773 push @values, $entry;
779 and return $req->json_content
782 option => $option->data_only,
783 values => [ map $_->data_only, @values ]
786 return $self->refresh($article, $cgi, undef, "Option added");
793 rules => "required;positiveint",
798 my ($self, $req, $article, $errors) = @_;
802 $req->validate(fields => \%option_id,
804 my @option_ids = $cgi->param("option_id");
805 unless ($errors->{option_id}) {
807 or $errors->{option_id} = "This request accepts only one option_id";
809 unless ($errors->{option_id}) {
810 require BSE::TB::ProductOptions;
811 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
813 or $errors->{option_id} = "Unknown option id";
815 unless ($errors->{option_id}) {
816 $option->{product_id} = $article->{id}
817 or $errors->{option_id} = "Option doesn't belong to this product";
826 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
829 my $option = $self->_get_option($req, $article, \%errors);
831 and return $self->_service_error($req, $article, $articles, undef, \%errors);
833 my $it = BSE::Util::Iterate->new;
837 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
838 option => [ \&tag_hash, $option ],
841 single => "dboptionvalue",
842 plural => "dboptionvalues",
843 code => [ iter_option_values => $self, \$option ],
847 return $req->dyn_response($template, \%acts);
852 Produce a form to edit the given option.
864 option_id - option id. This must belong to the product identified by
869 Template: admin/prodopt_edit
871 Permission required: bse_edit_prodopt_edit
875 sub req_edit_option {
876 my ($self, $req, $article, $articles, $msg, $errors) = @_;
878 $req->user_can(bse_edit_prodopt_edit => $article)
879 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
881 return $self->_common_option('admin/prodopt_edit', $req, $article,
882 $articles, $msg, $errors);
889 description => "Option name",
890 rules => "required;dh_one_line",
895 description => "Default Value",
896 rules => "positiveint"
902 description => "Value",
903 rules => "required;dh_one_line",
909 Saves changes to an option.
911 On failure perform a service error.
913 Requires _csrfp for admin_save_option
915 For Ajax requests (or with a _ parameter), returns JSON like:
919 option: { <option data> },
920 values: [ { value data, value data, ... } ]
933 option_id - id of the option to save, must belong to the product
938 name - new value for the name field
942 default_value - id of the default value
946 save_enabled - if supplied and true, set enabled from the enabled
951 enabled - If supplied and true, enable the option, otherwise disable
952 it. Ignored unless save_enabled is true.
956 valueI<value-id> - set the displayed value for the value record
957 identified by I<value-id>. If these aren't supplied the values aren't
962 Permission required: bse_edit_prodopt_save
966 sub req_save_option {
967 my ($self, $req, $article, $articles) = @_;
971 $req->check_csrf("admin_save_option")
972 or return $self->csrf_error($req, $article, "admin_save_option", "Save Product Option");
974 $req->user_can(bse_edit_prodopt_edit => $article)
975 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
978 my $option = $self->_get_option($req, $article, \%errors);
980 and return $self->_service_error($req, $article, $articles, undef, \%errors, 'FIELD', "req_edit_option");
981 $req->validate(fields => \%option_name,
983 my @values = $option->values;
984 my %fields = map {; "value$_->{id}" => \%option_value } @values;
985 $req->validate(fields => \%fields,
988 my $default_value = $cgi->param('default_value');
989 if (!$errors{default_value} && $default_value) {
990 grep $_->{id} == $default_value, @values
991 or $errors{default_value} = "Unknown value selected as default";
997 while ($index < 10 && defined $cgi->param("newvalue$index")) {
998 my $field = "newvalue$index";
999 my $value = $cgi->param($field);
1000 $req->validate(fields => { $field => \%option_value },
1001 errors => \%errors);
1002 push @new_values, $value;
1008 and return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD", "req_edit_option");
1010 my $name = $cgi->param("name");
1012 and $option->set_name($name);
1013 defined $default_value
1014 and $option->set_default_value($default_value);
1015 if ($cgi->param("save_enabled")) {
1016 my $enabled = $cgi->param("enabled") ? 1 : 0;
1017 $option->set_enabled($enabled);
1020 for my $value (@values) {
1021 my $new_value = $cgi->param("value$value->{id}");
1022 if (defined $new_value && $new_value ne $value->value) {
1023 $value->set_value($new_value);
1027 my $order = @values ? $values[-1]->display_order : time;
1028 for my $value (@new_values) {
1029 BSE::TB::ProductOptionValues->make
1031 product_option_id => $option->id,
1033 display_order => ++$order,
1038 and return $req->json_content
1041 option => $option->data_only,
1042 values => [ map $_->data_only, @values ],
1045 return $self->refresh($article, $req->cgi, undef,
1046 "Option '" . $option->name . "' saved");
1049 =item a_delconf_option
1051 Produce a form to confirm deletion of the given option.
1063 option_id - option id. This must belong to the product identified by
1068 Template: admin/prodopt_delete
1072 sub req_delconf_option {
1073 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1075 $req->user_can(bse_edit_prodopt_delete => $article)
1076 or return $self->_service_error($req, $article, $articles, "Insufficient product access to delete options");
1078 return $self->_common_option('admin/prodopt_delete', $req, $article,
1079 $articles, $msg, $errors);
1082 =item a_delete_option
1084 Delete the given option.
1086 On failure perform a service error.
1088 Requires _csrfp for admin_delete_option
1090 For Ajax requests (or with a _ parameter), returns JSON like:
1096 Permission required: bse_edit_prodopt_delete
1100 sub req_delete_option {
1101 my ($self, $req, $article, $articles) = @_;
1103 $req->check_csrf("admin_delete_option")
1104 or return $self->csrf_error($req, $article, "admin_delete_option", "Delete Product Option");
1106 $req->user_can(bse_edit_prodopt_delete => $article)
1107 or return $self->_service_error($req, $article, $articles, "Insufficient product access to delete options");
1110 my $option = $self->_get_option($req, $article, \%errors);
1112 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1113 my @values = $option->values;
1115 for my $value (@values) {
1121 and return $req->json_content
1126 return $self->refresh($article, $req->cgi, undef, "Option deleted");
1130 my %add_option_value_fields =
1134 description => "Option id",
1135 rules => "required;positiveint",
1139 description => "Value",
1140 rules => "required;dh_one_line",
1145 =item a_add_option_value
1147 Add a value to a product option.
1149 On failure perform a service error, see BSE::Edit::Article::_service_error.
1151 Requires _csrfp for admin_add_option_value
1153 For Ajax requests returns JSON like
1155 { success: 1, value: (valueobject) }
1157 Standard redirect on success otherwise.
1169 option_id - id of the option to add the value to
1173 value - text of the value to add.
1177 Permission required: bse_edit_prodopt_edit
1181 sub req_add_option_value {
1182 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1184 $req->check_csrf("admin_add_option_value")
1185 or return $self->csrf_error($req, $article, "admin_add_option_value", "Add Product Option Value");
1187 $req->user_can(bse_edit_prodopt_edit => $article)
1188 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1191 $req->validate(fields => \%add_option_value_fields,
1192 errors => \%errors);
1194 my $cgi = $req->cgi;
1195 unless ($errors{option_id}) {
1196 require BSE::TB::ProductOptions;
1197 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
1198 defined $option && $option->{product_id}
1199 or $errors{option_id} = "Bad option id - either unknown or for a different product";
1202 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1204 my $value = $cgi->param("value");
1205 require BSE::TB::ProductOptionValues;
1206 my $entry = BSE::TB::ProductOptionValues->make
1208 product_option_id => $option->{id},
1210 display_order => time,
1214 and return $req->json_content
1217 value => $entry->data_only
1220 return $self->refresh($article, $cgi, undef, "Value added");
1224 my %option_value_id =
1228 rules => "required;positiveint",
1232 sub _get_option_value {
1233 my ($self, $req, $article, $errors) = @_;
1236 my $cgi = $req->cgi;
1237 $req->validate(fields => \%option_value_id,
1239 unless ($errors->{value_id}) {
1240 require BSE::TB::ProductOptionValues;
1241 $option_value = BSE::TB::ProductOptionValues->getByPkey($cgi->param("value_id"));
1243 or $errors->{value_id} = "Unknown option value id";
1246 unless ($errors->{value_id}) {
1247 $option = $option_value->option;
1248 defined $option && $option->{product_id} == $article->{id}
1249 or $errors->{value_id} = "Value has no option or doesn't belong to the product";
1255 return wantarray ? ( $option_value, $option ) : $option_value ;
1258 sub _common_option_value {
1259 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
1262 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1264 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1269 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
1270 option_value => [ \&tag_hash, $option_value ],
1271 option => [ \&tag_hash, $option ],
1274 return $req->dyn_response($template, \%acts);
1277 =item a_edit_option_value
1279 Displays a form to edit the value for a given option.
1287 id - id of the product
1291 value_id - id of he product option value to edit, must belong to the
1296 Template: admin/prodopt_value_edit
1298 Permission required: bse_edit_prodopt_edit
1302 sub req_edit_option_value {
1303 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1305 $req->user_can(bse_edit_prodopt_edit => $article)
1306 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1308 return $self->_common_option_value('admin/prodopt_value_edit', $req,
1309 $article, $articles, $msg, $errors);
1312 my %save_option_value_fields =
1316 rules => "required;dh_one_line",
1321 =item a_save_option_value
1323 Saves changes to an option.
1325 On failure perform a service error.
1327 Requires _csrfp for admin_save_option_value
1329 For Ajax requests (or with a _ parameter), returns JSON like:
1333 value: { value data }
1346 value_id - id of the value to save, must belong to the product
1351 value - new displayed value for the option value.
1355 Permission required: bse_edit_prodopt_edit
1359 sub req_save_option_value {
1360 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1362 $req->check_csrf("admin_save_option_value")
1363 or return $self->csrf_error($req, $article, "admin_save_option_value", "Save Product Option Value");
1365 $req->user_can(bse_edit_prodopt_edit => $article)
1366 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1369 $req->validate(fields => \%save_option_value_fields,
1370 errors => \%errors);
1371 my $option_value = $self->_get_option_value($req, $article, \%errors);
1373 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1375 my $cgi = $req->cgi;
1376 $option_value->{value} = $cgi->param("value");
1377 $option_value->save;
1380 and return $req->json_content
1383 value => $option_value->data_only
1386 return $self->refresh($article, $cgi, undef, "Value saved");
1389 =item a_confdel_option_value
1391 Displays a page confirming deletion of a product option value.
1403 value_id - option value id
1407 Template: admin/prodopt_value_delete
1409 Permission required: bse_edit_prodopt_edit
1413 sub req_confdel_option_value {
1414 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1416 $req->user_can(bse_edit_prodopt_edit => $article)
1417 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1419 return $self->_common_option_value('admin/prodopt_value_delete', $req,
1420 $article, $articles, $msg, $errors);
1423 =item a_delete_option_value
1425 Deletes a product option.
1427 On failure perform a service error.
1429 Requires _csrfp for admin_delete_option_value
1431 For Ajax requests (or with a _ parameter), returns JSON like:
1447 value_id - id of the value to delete, must belong to the product
1452 Permission required: bse_edit_prodopt_edit
1456 sub req_delete_option_value {
1457 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1459 $req->check_csrf("admin_delete_option_value")
1460 or return $self->csrf_error($req, $article, "admin_delete_option_value", "Delete Product Option Value");
1462 $req->user_can(bse_edit_prodopt_edit => $article)
1463 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1466 my $option_value = $self->_get_option_value($req, $article, \%errors);
1468 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1470 $option_value->remove;
1473 and return $req->json_content
1478 return $self->refresh($article, $req->cgi, undef, "Value removed");
1481 sub tag_dboptionsjson {
1482 my ($self, $article) = @_;
1485 my @options = $article->db_options;
1486 my @opt_cols = BSE::TB::ProductOption->columns;
1487 for my $option (@options) {
1488 my $entry = $option->data_only;
1489 $entry->{values} = [ map $_->data_only, $option->values ];
1490 push @result, $entry;
1494 my $json = JSON->new;
1495 return $json->encode(\@result);
1499 my ($self, $req, $article, $articles, $direction) = @_;
1501 $req->check_csrf("admin_move_option")
1502 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1504 $req->user_can(bse_edit_prodopt_move => $article)
1505 or return $self->_service_error($req, $article, $articles, "Insufficient product access to move options");
1508 my $option = $self->_get_option($req, $article, \%errors);
1510 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1511 my @options = $article->db_options;
1512 my ($index) = grep $options[$_]{id} == $option->{id}, 0 .. $#options
1513 or return $self->_service_error($req, $article, $articles, "Unknown option id");
1515 $options[$index] = $option;
1517 my $other_index = $index + $direction;
1518 $other_index >= 0 && $other_index < @options
1519 or return $self->_service_error($req, $article, $articles, "Can't move option beyond end");
1521 my $other = $options[$other_index];
1523 ($option->{display_order}, $other->{display_order}) =
1524 ($other->{display_order}, $option->{display_order});
1528 if ($req->is_ajax) {
1529 @options = sort { $a->{display_order} <=> $b->{display_order} } @options;
1530 return return $req->json_content
1533 order => [ map $_->{id}, @options ]
1537 return $self->refresh($article, $req->cgi, undef, "Option moved");
1540 =item a_option_moveup
1542 =item a_option_movedown
1544 Move a product option up/down through the options for a product.
1546 On failure perform a service error.
1548 Requires _csrfp for admin_move_option
1550 For Ajax requests (or with a _ parameter), returns JSON like:
1554 order: [ list of option ids ]
1567 option_id - option id. This must belong to the product identified by
1572 Permission required: bse_edit_prodopt_move
1576 sub req_option_moveup {
1577 my ($self, $req, $article, $articles) = @_;
1579 return $self->_option_move($req, $article, $articles, -1);
1582 sub req_option_movedown {
1583 my ($self, $req, $article, $articles) = @_;
1585 return $self->_option_move($req, $article, $articles, 1);
1588 =item a_option_reorder
1590 Move a product option up/down through the options for a product.
1592 On failure perform a service error.
1594 Requires _csrfp for admin_move_option
1596 For Ajax requests (or with a _ parameter), returns JSON like:
1600 order: [ list of option ids ]
1613 option_ids - option ids separated by commas. These must belong to the
1614 product identified by id.
1618 Permission required: bse_edit_prodopt_move
1622 sub req_option_reorder {
1623 my ($self, $req, $article, $articles) = @_;
1625 $req->check_csrf("admin_move_option")
1626 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1628 $req->user_can(bse_edit_prodopt_move => $article)
1629 or return $self->_service_error($req, $article, $articles, "Insufficient product access to move options");
1631 my @options = $article->db_options;
1632 my @order = map { split ',' } $req->cgi->param('option_ids');
1633 my %options = map { $_->{id} => $_ } @options;
1635 for my $id (@order) {
1636 my $option = delete $options{$id}
1638 push @new_options, $option;
1640 push @new_options, sort { $a->{display_order} <=> $b->{display_order} } values %options;
1641 my @display_order = map $_->{display_order}, @options;
1642 for my $index (0 .. $#new_options) {
1643 $new_options[$index]{display_order} = $display_order[$index];
1644 $new_options[$index]->save;
1648 and return $req->json_content
1651 order => [ map $_->{id}, @new_options ]
1654 return $self->refresh($article, $req->cgi, undef, "Options reordered");
1657 sub _option_value_move {
1658 my ($self, $req, $article, $articles, $direction) = @_;
1660 $req->check_csrf("admin_move_option_value")
1661 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1663 $req->user_can(bse_edit_prodopt_edit => $article)
1664 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1667 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1669 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1670 my @values = $option->values;
1671 my ($index) = grep $values[$_]{id} == $option_value->{id}, 0 .. $#values
1672 or return $self->_service_error($req, $article, $articles, "Unknown option value id");
1674 $values[$index] = $option_value;
1676 my $other_index = $index + $direction;
1677 $other_index >= 0 && $other_index < @values
1678 or return $self->_service_error($req, $article, $articles, "Can't move option value beyond end");
1680 my $other = $values[$other_index];
1682 ($option_value->{display_order}, $other->{display_order}) =
1683 ($other->{display_order}, $option_value->{display_order});
1684 $option_value->save;
1687 # make sure the json gets the new order
1688 @values[$index, $other_index] = @values[$other_index, $index];
1691 and return $req->json_content
1694 order => [ map $_->{id}, @values ]
1697 return $self->refresh($article, $req->cgi, undef, "Value moved");
1700 =item a_option_value_moveup
1702 =item a_option_value_movedown
1704 Move a product option value up/down through the values for a product
1707 On failure perform a service error.
1709 Requires _csrfp for admin_move_option_value
1711 For Ajax requests (or with a _ parameter), returns JSON like:
1715 order: [ list of value ids ]
1728 value_id - option id. This must belong to the product identified by
1733 Permission required: bse_edit_prodopt_edit
1737 sub req_option_value_moveup {
1738 my ($self, $req, $article, $articles) = @_;
1740 return $self->_option_value_move($req, $article, $articles, -1);
1743 sub req_option_value_movedown {
1744 my ($self, $req, $article, $articles) = @_;
1746 return $self->_option_value_move($req, $article, $articles, 1);
1749 =item a_option_value_reorder
1751 Specify a new order for the values belonging to a product option.
1753 On failure perform a service error.
1755 Requires _csrfp for admin_move_option_value
1757 For Ajax requests (or with a _ parameter), returns JSON like:
1761 order: [ list of value ids ]
1774 option_id - the option to reorder values for
1778 value_ids - new order for values specified as value ids separated by
1783 Permission required: bse_edit_prodopt_edit
1787 sub req_option_value_reorder {
1788 my ($self, $req, $article, $articles) = @_;
1790 $req->check_csrf("admin_move_option_value")
1791 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1793 $req->user_can(bse_edit_prodopt_edit => $article)
1794 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1797 my $option = $self->_get_option($req, $article, \%errors);
1799 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1800 my @order = map { split ',' } $req->cgi->param('value_ids');
1801 my @values = $option->values;
1802 my %values = map { $_->{id} => $_ } @values;
1804 for my $id (@order) {
1805 my $value = delete $values{$id}
1807 push @new_values, $value;
1809 push @new_values, sort { $a->{display_order} <=> $b->{display_order} } values %values;
1810 my @display_order = map $_->{display_order}, @values;
1811 for my $index (0 .. $#new_values) {
1812 $new_values[$index]{display_order} = $display_order[$index];
1813 $new_values[$index]->save;
1817 and return $req->json_content
1820 option => $option->data_only,
1821 order => [ map $_->{id}, @new_values ]
1824 return $self->refresh($article, $req->cgi, undef, "Values reordered");
1830 my $custom = $self->SUPER::custom_fields();
1832 require DevHelp::Validate;
1833 DevHelp::Validate->import;
1834 return DevHelp::Validate::dh_configure_fields
1838 PRODUCT_CUSTOM_FIELDS_CFG,
1839 BSE::DB->single->dbh,
1843 sub article_actions {
1848 $self->SUPER::article_actions,
1849 a_add_option => 'req_add_option',
1850 a_confdel_option => 'req_confdel_option',
1851 a_del_option => 'req_del_option',
1852 a_edit_option => 'req_edit_option',
1853 a_save_option => 'req_save_option',
1854 a_delconf_option => 'req_delconf_option',
1855 a_delete_option => 'req_delete_option',
1856 a_get_option => 'req_get_option',
1857 a_edit_option_value => 'req_edit_option_value',
1858 a_save_option_value => 'req_save_option_value',
1859 a_confdel_option_value => 'req_confdel_option_value',
1860 a_delete_option_value => 'req_delete_option_value',
1861 a_add_option_value => 'req_add_option_value',
1862 a_option_value_moveup => 'req_option_value_moveup',
1863 a_option_value_movedown => 'req_option_value_movedown',
1864 a_option_value_reorder => 'req_option_value_reorder',
1865 a_option_moveup => 'req_option_moveup',
1866 a_option_movedown => 'req_option_movedown',
1867 a_option_reorder => 'req_option_reorder',
1877 Tony Cook <tony@develop-help.com>