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.016";
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 _make_dummy_article {
39 my ($self, $article) = @_;
41 require BSE::DummyProduct;
42 return bless $article, "BSE::DummyProduct";
45 sub base_template_dirs {
46 return ( "products" );
50 my ($self, $article) = @_;
52 my @extras = $self->SUPER::extra_templates($article);
53 push @extras, 'shopitem.tmpl'
54 if grep -f "$_/shopitem.tmpl",
55 BSE::Template->template_dirs($self->{cfg});
57 my $extras = $self->{cfg}->entry('products', 'extra_templates');
58 push @extras, grep /\.(tmpl|html)$/i, split /,/, $extras
65 my ($article, $arg) = @_;
67 my $value = $article->{$arg};
68 defined $value or $value = '';
69 if ($value =~ /\cJ/ && $value =~ /\cM/) {
73 return encode_entities($value);
77 require BSE::TB::Subscriptions;
78 BSE::TB::Subscriptions->all;
81 sub iter_option_values {
82 my ($self, $rcurrent_option, $args) = @_;
87 return $$rcurrent_option->values;
91 my ($object, $args) = @_;
93 my $value = $object->{$args};
94 defined $value or $value = '';
95 if ($value =~ /\cJ/ && $value =~ /\cM/) {
98 escape_html($value, '<>&"');
101 sub tag_dboptionvalue_move {
102 my ($self, $req, $article, $rvalues, $rindex, $args) = @_;
104 $$rindex >= 0 && $$rindex < @$rvalues
105 or return "** dboptionvalue_move only in dboption_values iterator **";
107 my $my_id = $rvalues->[$$rindex]{id};
108 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&value_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option_value") . "&";
110 my $t = $req->cgi->param('_t');
112 and $base_url .= "_t=$t&";
116 $up_url = $base_url . "a_option_value_moveup=1";
119 if ($$rindex < $#$rvalues) {
120 $down_url = $base_url . "a_option_value_movedown=1";
123 my $refresh = $self->refresh_url($article, $req->cgi);
126 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptvaluemove");
129 sub tag_dboption_move {
130 my ($self, $req, $article, $roptions, $rindex, $args) = @_;
132 $$rindex >= 0 && $$rindex < @$roptions
133 or return "** dboption_move only in dboptions iterator **";
135 my $my_id = $roptions->[$$rindex]{id};
136 my $base_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&option_id=$my_id&_csrfp=".$req->get_csrf_token("admin_move_option") . "&";
138 my $t = $req->cgi->param('_t');
140 and $base_url .= "_t=$t&";
144 $up_url = $base_url . "a_option_moveup=1";
147 if ($$rindex < $#$roptions) {
148 $down_url = $base_url . "a_option_movedown=1";
151 my $refresh = $self->refresh_url($article, $req->cgi);
154 return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptmove");
158 my ($self, $rtier, $rprices, $product) = @_;
160 unless ($rprices->{loaded}) {
161 %$rprices = map { $_->tier_id => $_ } $product->prices
163 $rprices->{loaded} = 1;
166 $$rtier or return '** no current tier **';
168 exists $rprices->{$$rtier->id}
171 return $rprices->{$$rtier->id}->retailPrice;
175 my ($self, $req, $article, $data) = @_;
177 $self->_save_price_tiers($req, $article, $data);
178 $self->SUPER::save_more($req, $article, $data);
182 my ($self, $req, $article, $data) = @_;
184 $self->_save_price_tiers($req, $article, $data);
185 $self->SUPER::save_new_more($req, $article, $data);
188 sub _save_price_tiers {
189 my ($self, $req, $article, $data) = @_;
191 $data->{save_pricing_tiers}
194 $req->user_can('edit_field_edit_retailPrice', $article)
197 my @tiers = BSE::TB::Products->pricing_tiers;
199 for my $tier (@tiers) {
200 my $key = "tier_price_" . $tier->id;
201 if (exists $data->{$key} && $data->{$key} =~ /\S/) {
202 $prices{$tier->id} = $data->{$key} * 100;
205 $article->set_prices(\%prices);
209 my ($self, $table_object) = @_;
211 my @cols = $self->SUPER::save_columns($table_object);
212 my @tiers = BSE::TB::Products->pricing_tiers;
214 push @cols, "save_pricing_tiers";
215 push @cols, map { "tier_price_" . $_->id } @tiers;
222 my ($self, $article) = @_;
227 return $article->db_options;
232 These a tags available on admin/edit_* pages specific to products.
238 product I<field> - display the given field from the product being edited.
242 iterator begin dboptions ... dboption I<field> ... iterator end dboptions
244 - iterate over the existing database stored options for the product
248 dboption_move - display arrows to move the current dboption. The span
249 for the arrows is given an id of "prodoptmoveI<option-id>" by default.
253 iterator begin dboptionvalues ... dboptionvalue I<field> ... iterator end dboptionvalues
255 - iterate over the values for the current dboption
259 dboptionvalue_move - display arrows to move the current dboption. The
260 span for the arrows is given an id of "prodoptvaluemoveI<value-id>"
265 dboptionsjson - returns the product options as JSON.
269 iterator begin price_tiers ... price_tier I<field> ... iterator end price_tiers
271 Iterate over the configured price tiers.
277 Return the price at the current price_tier. Returns an empty string
278 if there's no price at this tier.
285 my ($self, $acts, $req, $article, $articles, $msg, $errors) = @_;
287 my $product_opts = product_options($req->cfg);
290 my $mbcs = $cfg->entry('html', 'mbcs', 0);
291 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&hash_tag;
296 my $dboption_value_index;
297 my $current_option_value;
298 my $it = BSE::Util::Iterate->new;
302 $req->set_variable(product => $article);
305 product => [ \&tag_article, $article, $cfg ],
306 $self->SUPER::low_edit_tags($acts, $req, $article, $articles, $msg,
308 alloptions => join(",", sort keys %$product_opts),
310 ([ \&iter_subs, $req ], 'subscription', 'subscriptions'),
313 single => "dboption",
314 plural => "dboptions",
315 store => \$current_option,
317 index => \$dboption_index,
318 code => [ iter_dboptions => $self, $article ],
323 $self, $req, $article, \@dboptions, \$dboption_index
327 single => "dboptionvalue",
328 plural => "dboptionvalues",
329 data => \@dboption_values,
330 index => \$dboption_value_index,
331 store => \$current_option_value,
332 code => [ iter_option_values => $self, \$current_option ],
335 dboptionsjson => [ tag_dboptionsjson => $self, $article ],
336 dboptionvalue_move =>
338 tag_dboptionvalue_move =>
339 $self, $req, $article, \@dboption_values, \$dboption_value_index
343 single => "price_tier",
344 plural => "price_tiers",
345 code => [ pricing_tiers => "BSE::TB::Products" ],
347 store => \$price_tier,
349 tier_price => [ tag_tier_price => $self, \$price_tier, \%prices, $article ],
354 my ($self, $article, $cgi) = @_;
356 my $base = 'product';
357 my $t = $cgi->param('_t');
358 if ($t && $t =~ /^\w+$/) {
361 return $self->{cfg}->entry('admin templates', $base,
366 my ($self, $article, $cgi) = @_;
368 return $self->{cfg}->entry('admin templates', 'add_product',
369 'admin/edit_product');
372 sub validate_parent {
373 my ($self, $data, $articles, $parent, $rmsg) = @_;
375 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
377 $parent->{generator} eq 'BSE::Generate::Catalog') {
378 $$rmsg = "Products must be in a catalog (not $parent->{generator})";
382 return $self->SUPER::validate_parent($data, $articles, $parent, $rmsg);
385 sub _validate_common {
386 my ($self, $data, $articles, $errors) = @_;
388 $self->SUPER::_validate_common($data, $articles, $errors);
390 for my $col (keys %money_fields) {
391 my $value = $data->{$col};
392 defined $value or next;
393 unless ($value =~ /^\d+(\.\d{1,2})?\s*/) {
394 $errors->{$col} = "$money_fields{$col} invalid";
398 if (defined $data->{options}) {
399 my $avail_options = product_options($self->{cfg});
401 my @bad_opts = grep !$avail_options->{$_},
402 split /,/, $data->{options};
404 $errors->{options} = "Bad product options '". join(",", @bad_opts)."' entered";
409 for my $sub_field (qw(subscription_id subscription_required)) {
410 my $value = $data->{$sub_field};
411 defined $value or next;
412 if ($value ne '-1') {
413 require BSE::TB::Subscriptions;
414 @subs = BSE::TB::Subscriptions->all unless @subs;
415 unless (grep $_->{subscription_id} == $value, @subs) {
416 $errors->{$sub_field} = "Invalid $sub_field value";
420 if (defined $data->{subscription_period}) {
421 my $sub = $data->{subscription_id};
422 if ($data->{subscription_period} !~ /^\d+$/) {
423 $errors->{subscription_period} = "Invalid subscription period, it must be the number of months to subscribe";
425 elsif ($sub != -1 && $data->{subscription_period} < 1) {
426 $errors->{subscription_period} = "Subscription period must be 1 or more when a subscription is selected";
429 if (defined $data->{subscription_usage}) {
430 unless ($data->{subscription_usage} =~ /^[123]$/) {
431 $errors->{subscription_usage} = "Invalid subscription usage";
435 if ($data->{save_pricing_tiers}) {
436 my @tiers = BSE::TB::Products->pricing_tiers;
437 for my $tier (@tiers) {
438 my $key = "tier_price_" . $tier->id;
439 my $value = $data->{$key};
440 defined $value or next;
441 if ($value =~ /\S/ && $value !~ /^\d+(\.\d{1,2})?\s*/) {
442 $errors->{$key} = 'Pricing tier "' . $tier->description . '" price invalid';
447 return !keys %$errors;
451 my ($self, $data, $articles, $errors) = @_;
453 my $ok = $self->SUPER::validate($data, $articles, $errors);
454 $self->_validate_common($data, $articles, $errors);
456 for my $field (qw(title)) {
457 unless ($data->{$field} =~ /\S/) {
458 $errors->{$field} = "No $field entered";
462 return $ok && !keys %$errors;
466 my ($self, $article, $data, $articles, $errors) = @_;
468 $self->SUPER::validate_old($article, $data, $articles, $errors)
471 return !keys %$errors;
474 sub possible_parents {
475 my ($self, $article, $articles) = @_;
480 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
481 # the parents of a catalog can be other catalogs or the shop
482 my $shop = $articles->getByPkey($shopid);
483 my @work = [ $shopid, $shop->{title} ];
485 my ($id, $title) = @{pop @work};
487 $labels{$id} = $title;
488 push @work, map [ $_->{id}, $title.' / '.$_->{title} ],
489 sort { $b->{displayOrder} <=> $a->{displayOrder} }
490 grep $_->{generator} eq 'BSE::Generate::Catalog',
491 $articles->getBy(parentid=>$id);
493 unless ($shop->{generator} eq 'BSE::Generate::Catalog') {
495 delete $labels{$shopid};
497 return (\@values, \%labels);
501 my ($self, $articles) = @_;
507 my ($self, $articles, $article) = @_;
509 return BSE::TB::Products->getByPkey($article->{id});
512 sub default_link_path {
513 my ($self, $article) = @_;
515 $self->{cfg}->entry('uri', 'shop', '/shop');
519 my ($self, $article) = @_;
526 if ($self->{cfg}->entry('shop', 'secureurl_articles', 1)) {
527 $urlbase = $self->{cfg}->entryVar('site', 'secureurl');
531 if ($article->is_dynamic) {
532 (my $extra = $article->title) =~ tr/A-Za-z0-9/-/sc;
533 return "$urlbase/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($extra);
536 my $shop_uri = $self->link_path($article);
537 return $urlbase.$shop_uri."/shop$article->{id}.html";
540 sub _fill_product_data {
541 my ($self, $req, $data, $src) = @_;
543 for my $money_col (qw(retailPrice wholesalePrice gst)) {
544 if (exists $src->{$money_col}) {
545 if ($src->{$money_col} =~ /^\d+(\.\d\d)?\s*/) {
546 $data->{$money_col} = 100 * $src->{$money_col};
549 $data->{$money_col} = 0;
553 if (exists $src->{leadTime}) {
554 $src->{leadTime} =~ /^\d+\s*$/
555 or $src->{leadTime} = 0;
556 $data->{leadTime} = $src->{leadTime};
558 if (exists $src->{description} && length $src->{description}) {
560 if ($req->user_can('edit_field_edit_description', $data)) {
561 $data->{description} = $src->{description};
565 if (exists $src->{product_code} && length $src->{product_code}) {
567 if ($req->user_can('edit_field_edit_product_code', $data)) {
568 $data->{product_code} = $src->{product_code};
572 for my $field (qw(options subscription_id subscription_period
573 subscription_usage subscription_required
574 weight length width height)) {
575 if (exists $src->{$field}) {
576 $data->{$field} = $src->{$field};
578 elsif ($data == $src) {
580 $data->{$field} = $self->default_value($req, $data, $field);
586 my ($self, $req, $data, $articles) = @_;
588 $self->_fill_product_data($req, $data, $data);
590 return $self->SUPER::fill_new_data($req, $data, $articles);
594 my ($self, $req, $article, $src) = @_;
596 $self->_fill_product_data($req, $article, $src);
598 return $self->SUPER::fill_old_data($req, $article, $src);
601 sub default_template {
602 my ($self, $article, $cfg, $templates) = @_;
604 my $template = $cfg->entry('products', 'template');
606 if $template && grep $_ eq $template, @$templates;
608 return $self->SUPER::default_template($article, $cfg, $templates);
614 return ( 'product flags', $self->SUPER::flag_sections );
617 sub shop_article { 1 }
623 subscription_id => -1,
624 subscription_required => -1,
625 subscription_period => 1,
626 subscription_usage => 3,
639 my ($self, $req, $article, $col) = @_;
641 my $value = $self->SUPER::default_value($req, $article, $col);
642 defined $value and return $value;
644 exists $defaults{$col} and return $defaults{$col};
649 sub type_default_value {
650 my ($self, $req, $col) = @_;
652 my $value = $req->cfg->entry('product defaults', $col);
653 defined $value and return $value;
655 return $self->SUPER::type_default_value($req, $col);
662 description => "Option name",
664 rules => "dh_one_line",
669 description => "Value 1",
670 rules => "dh_one_line",
675 description => "Value 2",
676 rules => "dh_one_line",
681 description => "Value 3",
682 rules => "dh_one_line",
687 description => "Value 4",
688 rules => "dh_one_line",
693 description => "Value 5",
694 rules => "dh_one_line",
701 Actions you can request from add.pl for products.
707 Add a new product option.
709 On failure perform a service error.
711 Requires _csrfp for admin_add_option
713 For Ajax requests (or with a _ parameter) returns JSON like:
717 option: { <option data> },
718 values: [ { value data }, { value data }, ... ]
731 name - Name of the option (required)
735 value1 .. value5 - if any of these are non-blank they are added to the
740 Permission required: bse_edit_prodopt_add
745 my ($self, $req, $article, $articles, $msg, $errors) = @_;
747 $req->check_csrf('admin_add_option')
748 or return $self->csrf_error($req, $article, "admin_add_option", "Add Product Option");
750 $req->user_can(bse_edit_prodopt_add => $article)
751 or return $self->_service_error($req, $article, $articles, "Insufficient product access to add options");
754 $req->validate(fields => \%option_fields,
757 and return $self->_service_error($req, $article, $articles, undef,
761 require BSE::TB::ProductOptions;
762 require BSE::TB::ProductOptionValues;
763 my $option = BSE::TB::ProductOptions->make
765 product_id => $article->{id},
766 name => scalar($cgi->param('name')),
767 display_order => time,
772 for my $value_key (sort grep /^value/, keys %option_fields) {
773 my ($value) = $cgi->param($value_key);
774 if (defined $value && $value =~ /\S/) {
775 my $entry = BSE::TB::ProductOptionValues->make
777 product_option_id => $option->{id},
779 display_order => $order,
781 push @values, $entry;
787 and return $req->json_content
790 option => $option->data_only,
791 values => [ map $_->data_only, @values ]
794 return $self->refresh($article, $cgi, undef, "Option added");
801 rules => "required;positiveint",
806 my ($self, $req, $article, $errors) = @_;
810 $req->validate(fields => \%option_id,
812 my @option_ids = $cgi->param("option_id");
813 unless ($errors->{option_id}) {
815 or $errors->{option_id} = "This request accepts only one option_id";
817 unless ($errors->{option_id}) {
818 require BSE::TB::ProductOptions;
819 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
821 or $errors->{option_id} = "Unknown option id";
823 unless ($errors->{option_id}) {
824 $option->{product_id} = $article->{id}
825 or $errors->{option_id} = "Option doesn't belong to this product";
834 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
837 my $option = $self->_get_option($req, $article, \%errors);
839 and return $self->_service_error($req, $article, $articles, undef, \%errors);
841 $req->set_variable(option => $option);
842 $req->messages($errors);
843 my $it = BSE::Util::Iterate->new;
847 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
848 option => [ \&tag_hash, $option ],
851 single => "dboptionvalue",
852 plural => "dboptionvalues",
853 code => [ iter_option_values => $self, \$option ],
857 return $req->dyn_response($template, \%acts);
862 Produce a form to edit the given option.
874 option_id - option id. This must belong to the product identified by
879 Template: admin/prodopt_edit
881 Permission required: bse_edit_prodopt_edit
885 sub req_edit_option {
886 my ($self, $req, $article, $articles, $msg, $errors) = @_;
888 $req->user_can(bse_edit_prodopt_edit => $article)
889 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
891 return $self->_common_option('admin/prodopt_edit', $req, $article,
892 $articles, $msg, $errors);
899 description => "Option name",
900 rules => "required;dh_one_line",
905 description => "Default Value",
906 rules => "positiveint"
912 description => "Value",
913 rules => "required;dh_one_line",
919 Saves changes to an option.
921 On failure perform a service error.
923 Requires _csrfp for admin_save_option
925 For Ajax requests (or with a _ parameter), returns JSON like:
929 option: { <option data> },
930 values: [ { value data, value data, ... } ]
943 option_id - id of the option to save, must belong to the product
948 name - new value for the name field
952 default_value - id of the default value
956 save_enabled - if supplied and true, set enabled from the enabled
961 enabled - If supplied and true, enable the option, otherwise disable
962 it. Ignored unless save_enabled is true.
966 valueI<value-id> - set the displayed value for the value record
967 identified by I<value-id>. If these aren't supplied the values aren't
972 Permission required: bse_edit_prodopt_save
976 sub req_save_option {
977 my ($self, $req, $article, $articles) = @_;
981 $req->check_csrf("admin_save_option")
982 or return $self->csrf_error($req, $article, "admin_save_option", "Save Product Option");
984 $req->user_can(bse_edit_prodopt_edit => $article)
985 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
988 my $option = $self->_get_option($req, $article, \%errors);
990 and return $self->_service_error($req, $article, $articles, undef, \%errors, 'FIELD', "req_edit_option");
991 $req->validate(fields => \%option_name,
993 my @values = $option->values;
994 my %fields = map {; "value$_->{id}" => \%option_value } @values;
995 $req->validate(fields => \%fields,
998 my $default_value = $cgi->param('default_value');
999 if (!$errors{default_value} && $default_value) {
1000 grep $_->{id} == $default_value, @values
1001 or $errors{default_value} = "Unknown value selected as default";
1007 while ($index < 10 && defined $cgi->param("newvalue$index")) {
1008 my $field = "newvalue$index";
1009 my $value = $cgi->param($field);
1010 $req->validate(fields => { $field => \%option_value },
1011 errors => \%errors);
1012 push @new_values, $value;
1018 and return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD", "req_edit_option");
1020 my $name = $cgi->param("name");
1022 and $option->set_name($name);
1023 defined $default_value
1024 and $option->set_default_value($default_value);
1025 if ($cgi->param("save_enabled")) {
1026 my $enabled = $cgi->param("enabled") ? 1 : 0;
1027 $option->set_enabled($enabled);
1030 for my $value (@values) {
1031 my $new_value = $cgi->param("value$value->{id}");
1032 if (defined $new_value && $new_value ne $value->value) {
1033 $value->set_value($new_value);
1037 my $order = @values ? $values[-1]->display_order : time;
1038 for my $value (@new_values) {
1039 BSE::TB::ProductOptionValues->make
1041 product_option_id => $option->id,
1043 display_order => ++$order,
1048 and return $req->json_content
1051 option => $option->data_only,
1052 values => [ map $_->data_only, @values ],
1055 return $self->refresh($article, $req->cgi, undef,
1056 "Option '" . $option->name . "' saved");
1059 =item a_delconf_option
1061 Produce a form to confirm deletion of the given option.
1073 option_id - option id. This must belong to the product identified by
1078 Template: admin/prodopt_delete
1082 sub req_delconf_option {
1083 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1085 $req->user_can(bse_edit_prodopt_delete => $article)
1086 or return $self->_service_error($req, $article, $articles, "Insufficient product access to delete options");
1088 return $self->_common_option('admin/prodopt_delete', $req, $article,
1089 $articles, $msg, $errors);
1092 =item a_delete_option
1094 Delete the given option.
1096 On failure perform a service error.
1098 Requires _csrfp for admin_delete_option
1100 For Ajax requests (or with a _ parameter), returns JSON like:
1106 Permission required: bse_edit_prodopt_delete
1110 sub req_delete_option {
1111 my ($self, $req, $article, $articles) = @_;
1113 $req->check_csrf("admin_delete_option")
1114 or return $self->csrf_error($req, $article, "admin_delete_option", "Delete Product Option");
1116 $req->user_can(bse_edit_prodopt_delete => $article)
1117 or return $self->_service_error($req, $article, $articles, "Insufficient product access to delete options");
1120 my $option = $self->_get_option($req, $article, \%errors);
1122 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1123 my @values = $option->values;
1125 for my $value (@values) {
1131 and return $req->json_content
1136 return $self->refresh($article, $req->cgi, undef, "Option deleted");
1140 my %add_option_value_fields =
1144 description => "Option id",
1145 rules => "required;positiveint",
1149 description => "Value",
1150 rules => "required;dh_one_line",
1155 =item a_add_option_value
1157 Add a value to a product option.
1159 On failure perform a service error, see BSE::Edit::Article::_service_error.
1161 Requires _csrfp for admin_add_option_value
1163 For Ajax requests returns JSON like
1165 { success: 1, value: (valueobject) }
1167 Standard redirect on success otherwise.
1179 option_id - id of the option to add the value to
1183 value - text of the value to add.
1187 Permission required: bse_edit_prodopt_edit
1191 sub req_add_option_value {
1192 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1194 $req->check_csrf("admin_add_option_value")
1195 or return $self->csrf_error($req, $article, "admin_add_option_value", "Add Product Option Value");
1197 $req->user_can(bse_edit_prodopt_edit => $article)
1198 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1201 $req->validate(fields => \%add_option_value_fields,
1202 errors => \%errors);
1204 my $cgi = $req->cgi;
1205 unless ($errors{option_id}) {
1206 require BSE::TB::ProductOptions;
1207 $option = BSE::TB::ProductOptions->getByPkey($cgi->param("option_id"));
1208 defined $option && $option->{product_id}
1209 or $errors{option_id} = "Bad option id - either unknown or for a different product";
1212 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1214 my $value = $cgi->param("value");
1215 require BSE::TB::ProductOptionValues;
1216 my $entry = BSE::TB::ProductOptionValues->make
1218 product_option_id => $option->{id},
1220 display_order => time,
1224 and return $req->json_content
1227 value => $entry->data_only
1230 return $self->refresh($article, $cgi, undef, "Value added");
1234 my %option_value_id =
1238 rules => "required;positiveint",
1242 sub _get_option_value {
1243 my ($self, $req, $article, $errors) = @_;
1246 my $cgi = $req->cgi;
1247 $req->validate(fields => \%option_value_id,
1249 unless ($errors->{value_id}) {
1250 require BSE::TB::ProductOptionValues;
1251 $option_value = BSE::TB::ProductOptionValues->getByPkey($cgi->param("value_id"));
1253 or $errors->{value_id} = "Unknown option value id";
1256 unless ($errors->{value_id}) {
1257 $option = $option_value->option;
1258 defined $option && $option->{product_id} == $article->{id}
1259 or $errors->{value_id} = "Value has no option or doesn't belong to the product";
1265 return wantarray ? ( $option_value, $option ) : $option_value ;
1268 sub _common_option_value {
1269 my ($self, $template, $req, $article, $articles, $msg, $errors) = @_;
1272 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1274 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1276 $req->set_variable(option => $option);
1277 $req->set_variable(option_value => $option_value);
1281 $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors),
1282 option_value => [ \&tag_hash, $option_value ],
1283 option => [ \&tag_hash, $option ],
1286 return $req->dyn_response($template, \%acts);
1289 =item a_edit_option_value
1291 Displays a form to edit the value for a given option.
1299 id - id of the product
1303 value_id - id of he product option value to edit, must belong to the
1308 Template: admin/prodopt_value_edit
1310 Permission required: bse_edit_prodopt_edit
1314 sub req_edit_option_value {
1315 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1317 $req->user_can(bse_edit_prodopt_edit => $article)
1318 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1320 return $self->_common_option_value('admin/prodopt_value_edit', $req,
1321 $article, $articles, $msg, $errors);
1324 my %save_option_value_fields =
1328 rules => "required;dh_one_line",
1333 =item a_save_option_value
1335 Saves changes to an option.
1337 On failure perform a service error.
1339 Requires _csrfp for admin_save_option_value
1341 For Ajax requests (or with a _ parameter), returns JSON like:
1345 value: { value data }
1358 value_id - id of the value to save, must belong to the product
1363 value - new displayed value for the option value.
1367 Permission required: bse_edit_prodopt_edit
1371 sub req_save_option_value {
1372 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1374 $req->check_csrf("admin_save_option_value")
1375 or return $self->csrf_error($req, $article, "admin_save_option_value", "Save Product Option Value");
1377 $req->user_can(bse_edit_prodopt_edit => $article)
1378 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1381 $req->validate(fields => \%save_option_value_fields,
1382 errors => \%errors);
1383 my $option_value = $self->_get_option_value($req, $article, \%errors);
1385 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1387 my $cgi = $req->cgi;
1388 $option_value->{value} = $cgi->param("value");
1389 $option_value->save;
1392 and return $req->json_content
1395 value => $option_value->data_only
1398 return $self->refresh($article, $cgi, undef, "Value saved");
1401 =item a_confdel_option_value
1403 Displays a page confirming deletion of a product option value.
1415 value_id - option value id
1419 Template: admin/prodopt_value_delete
1421 Permission required: bse_edit_prodopt_edit
1425 sub req_confdel_option_value {
1426 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1428 $req->user_can(bse_edit_prodopt_edit => $article)
1429 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1431 return $self->_common_option_value('admin/prodopt_value_delete', $req,
1432 $article, $articles, $msg, $errors);
1435 =item a_delete_option_value
1437 Deletes a product option.
1439 On failure perform a service error.
1441 Requires _csrfp for admin_delete_option_value
1443 For Ajax requests (or with a _ parameter), returns JSON like:
1459 value_id - id of the value to delete, must belong to the product
1464 Permission required: bse_edit_prodopt_edit
1468 sub req_delete_option_value {
1469 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1471 $req->check_csrf("admin_delete_option_value")
1472 or return $self->csrf_error($req, $article, "admin_delete_option_value", "Delete Product Option Value");
1474 $req->user_can(bse_edit_prodopt_edit => $article)
1475 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1478 my $option_value = $self->_get_option_value($req, $article, \%errors);
1480 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1482 $option_value->remove;
1485 and return $req->json_content
1490 return $self->refresh($article, $req->cgi, undef, "Value removed");
1493 sub tag_dboptionsjson {
1494 my ($self, $article) = @_;
1497 my @options = $article->db_options;
1498 my @opt_cols = BSE::TB::ProductOption->columns;
1499 for my $option (@options) {
1500 my $entry = $option->data_only;
1501 $entry->{values} = [ map $_->data_only, $option->values ];
1502 push @result, $entry;
1506 my $json = JSON->new;
1507 return $json->encode(\@result);
1511 my ($self, $req, $article, $articles, $direction) = @_;
1513 $req->check_csrf("admin_move_option")
1514 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1516 $req->user_can(bse_edit_prodopt_move => $article)
1517 or return $self->_service_error($req, $article, $articles, "Insufficient product access to move options");
1520 my $option = $self->_get_option($req, $article, \%errors);
1522 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1523 my @options = $article->db_options;
1524 my ($index) = grep $options[$_]{id} == $option->{id}, 0 .. $#options
1525 or return $self->_service_error($req, $article, $articles, "Unknown option id");
1527 $options[$index] = $option;
1529 my $other_index = $index + $direction;
1530 $other_index >= 0 && $other_index < @options
1531 or return $self->_service_error($req, $article, $articles, "Can't move option beyond end");
1533 my $other = $options[$other_index];
1535 ($option->{display_order}, $other->{display_order}) =
1536 ($other->{display_order}, $option->{display_order});
1540 if ($req->is_ajax) {
1541 @options = sort { $a->{display_order} <=> $b->{display_order} } @options;
1542 return return $req->json_content
1545 order => [ map $_->{id}, @options ]
1549 return $self->refresh($article, $req->cgi, undef, "Option moved");
1552 =item a_option_moveup
1554 =item a_option_movedown
1556 Move a product option up/down through the options for a product.
1558 On failure perform a service error.
1560 Requires _csrfp for admin_move_option
1562 For Ajax requests (or with a _ parameter), returns JSON like:
1566 order: [ list of option ids ]
1579 option_id - option id. This must belong to the product identified by
1584 Permission required: bse_edit_prodopt_move
1588 sub req_option_moveup {
1589 my ($self, $req, $article, $articles) = @_;
1591 return $self->_option_move($req, $article, $articles, -1);
1594 sub req_option_movedown {
1595 my ($self, $req, $article, $articles) = @_;
1597 return $self->_option_move($req, $article, $articles, 1);
1600 =item a_option_reorder
1602 Move a product option up/down through the options for a product.
1604 On failure perform a service error.
1606 Requires _csrfp for admin_move_option
1608 For Ajax requests (or with a _ parameter), returns JSON like:
1612 order: [ list of option ids ]
1625 option_ids - option ids separated by commas. These must belong to the
1626 product identified by id.
1630 Permission required: bse_edit_prodopt_move
1634 sub req_option_reorder {
1635 my ($self, $req, $article, $articles) = @_;
1637 $req->check_csrf("admin_move_option")
1638 or return $self->csrf_error($req, $article, "admin_move_option", "Move Product Option");
1640 $req->user_can(bse_edit_prodopt_move => $article)
1641 or return $self->_service_error($req, $article, $articles, "Insufficient product access to move options");
1643 my @options = $article->db_options;
1644 my @order = map { split ',' } $req->cgi->param('option_ids');
1645 my %options = map { $_->{id} => $_ } @options;
1647 for my $id (@order) {
1648 my $option = delete $options{$id}
1650 push @new_options, $option;
1652 push @new_options, sort { $a->{display_order} <=> $b->{display_order} } values %options;
1653 my @display_order = map $_->{display_order}, @options;
1654 for my $index (0 .. $#new_options) {
1655 $new_options[$index]{display_order} = $display_order[$index];
1656 $new_options[$index]->save;
1660 and return $req->json_content
1663 order => [ map $_->{id}, @new_options ]
1666 return $self->refresh($article, $req->cgi, undef, "Options reordered");
1669 sub _option_value_move {
1670 my ($self, $req, $article, $articles, $direction) = @_;
1672 $req->check_csrf("admin_move_option_value")
1673 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1675 $req->user_can(bse_edit_prodopt_edit => $article)
1676 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1679 my ($option_value, $option) = $self->_get_option_value($req, $article, \%errors);
1681 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1682 my @values = $option->values;
1683 my ($index) = grep $values[$_]{id} == $option_value->{id}, 0 .. $#values
1684 or return $self->_service_error($req, $article, $articles, "Unknown option value id");
1686 $values[$index] = $option_value;
1688 my $other_index = $index + $direction;
1689 $other_index >= 0 && $other_index < @values
1690 or return $self->_service_error($req, $article, $articles, "Can't move option value beyond end");
1692 my $other = $values[$other_index];
1694 ($option_value->{display_order}, $other->{display_order}) =
1695 ($other->{display_order}, $option_value->{display_order});
1696 $option_value->save;
1699 # make sure the json gets the new order
1700 @values[$index, $other_index] = @values[$other_index, $index];
1703 and return $req->json_content
1706 order => [ map $_->{id}, @values ]
1709 return $self->refresh($article, $req->cgi, undef, "Value moved");
1712 =item a_option_value_moveup
1714 =item a_option_value_movedown
1716 Move a product option value up/down through the values for a product
1719 On failure perform a service error.
1721 Requires _csrfp for admin_move_option_value
1723 For Ajax requests (or with a _ parameter), returns JSON like:
1727 order: [ list of value ids ]
1740 value_id - option id. This must belong to the product identified by
1745 Permission required: bse_edit_prodopt_edit
1749 sub req_option_value_moveup {
1750 my ($self, $req, $article, $articles) = @_;
1752 return $self->_option_value_move($req, $article, $articles, -1);
1755 sub req_option_value_movedown {
1756 my ($self, $req, $article, $articles) = @_;
1758 return $self->_option_value_move($req, $article, $articles, 1);
1761 =item a_option_value_reorder
1763 Specify a new order for the values belonging to a product option.
1765 On failure perform a service error.
1767 Requires _csrfp for admin_move_option_value
1769 For Ajax requests (or with a _ parameter), returns JSON like:
1773 order: [ list of value ids ]
1786 option_id - the option to reorder values for
1790 value_ids - new order for values specified as value ids separated by
1795 Permission required: bse_edit_prodopt_edit
1799 sub req_option_value_reorder {
1800 my ($self, $req, $article, $articles) = @_;
1802 $req->check_csrf("admin_move_option_value")
1803 or return $self->csrf_error($req, $article, "admin_move_option_value", "Move Product Option Value");
1805 $req->user_can(bse_edit_prodopt_edit => $article)
1806 or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
1809 my $option = $self->_get_option($req, $article, \%errors);
1811 and return $self->_service_error($req, $article, $articles, undef, \%errors);
1812 my @order = map { split ',' } $req->cgi->param('value_ids');
1813 my @values = $option->values;
1814 my %values = map { $_->{id} => $_ } @values;
1816 for my $id (@order) {
1817 my $value = delete $values{$id}
1819 push @new_values, $value;
1821 push @new_values, sort { $a->{display_order} <=> $b->{display_order} } values %values;
1822 my @display_order = map $_->{display_order}, @values;
1823 for my $index (0 .. $#new_values) {
1824 $new_values[$index]{display_order} = $display_order[$index];
1825 $new_values[$index]->save;
1829 and return $req->json_content
1832 option => $option->data_only,
1833 order => [ map $_->{id}, @new_values ]
1836 return $self->refresh($article, $req->cgi, undef, "Values reordered");
1842 my $custom = $self->SUPER::custom_fields();
1844 require DevHelp::Validate;
1845 DevHelp::Validate->import;
1846 return DevHelp::Validate::dh_configure_fields
1850 PRODUCT_CUSTOM_FIELDS_CFG,
1851 BSE::DB->single->dbh,
1855 sub article_actions {
1860 $self->SUPER::article_actions,
1861 a_add_option => 'req_add_option',
1862 a_confdel_option => 'req_confdel_option',
1863 a_del_option => 'req_del_option',
1864 a_edit_option => 'req_edit_option',
1865 a_save_option => 'req_save_option',
1866 a_delconf_option => 'req_delconf_option',
1867 a_delete_option => 'req_delete_option',
1868 a_get_option => 'req_get_option',
1869 a_edit_option_value => 'req_edit_option_value',
1870 a_save_option_value => 'req_save_option_value',
1871 a_confdel_option_value => 'req_confdel_option_value',
1872 a_delete_option_value => 'req_delete_option_value',
1873 a_add_option_value => 'req_add_option_value',
1874 a_option_value_moveup => 'req_option_value_moveup',
1875 a_option_value_movedown => 'req_option_value_movedown',
1876 a_option_value_reorder => 'req_option_value_reorder',
1877 a_option_moveup => 'req_option_moveup',
1878 a_option_movedown => 'req_option_movedown',
1879 a_option_reorder => 'req_option_reorder',
1889 Tony Cook <tony@develop-help.com>