Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/promotions.cgi
Diff

/var/www/vhosts/3dshawn.com/shop.3dshawn.com/promotions.cgi

added on local at 2026-07-01 22:09:44

Added
+511
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 39592c3efea3
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# ShopCart - Seller Promotions
4#
5# List page + new/edit form for the seller's own coupons, sales, bundles
6# and flash sales. Posts to promotion_action.cgi for create/update/delete.
7#
8# Consolidates the former /promotion_action.cgi (POST handler) via
9# SCRIPT_NAME dispatch. The promotion_action.cgi file is a wrapper that
10# `do`s this file.
11#======================================================================
12use strict;
13use warnings;
14
15use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com';
16use CGI;
17use MODS::Template;
18use MODS::DBConnect;
19use MODS::Login;
20use MODS::ShopCart::Config;
21use MODS::ShopCart::Wrapper;
22use MODS::ShopCart::Promotions;
23
24my $q = CGI->new;
25my $form = $q->Vars;
26my $auth = MODS::Login->new;
27my $wrap = MODS::ShopCart::Wrapper->new;
28my $tfile = MODS::Template->new;
29my $db = MODS::DBConnect->new;
30my $cfg = MODS::ShopCart::Config->new;
31my $promo = MODS::ShopCart::Promotions->new;
32my $DB = $cfg->settings('database_name');
33
34$|=1;
35my $userinfo = $auth->login_verify();
36if (!$userinfo) {
37 print "Status: 302 Found\nLocation: /login.cgi\n\n";
38 exit;
39}
40my $uid = $userinfo->{user_id};
41$uid =~ s/[^0-9]//g;
42
43my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'promotions';
44
45if ($entry eq 'promotion_action') {
46 require MODS::ShopCart::Permissions;
47 MODS::ShopCart::Permissions->new->require_feature($userinfo, 'edit_promotions');
48 _handle_action($q, $form);
49 exit;
50}
51
52require MODS::ShopCart::Permissions;
53MODS::ShopCart::Permissions->new->require_feature($userinfo, 'view_promotions');
54
55my $tutorial_mode = ($form->{tutorial} && $form->{tutorial} eq '1') ? 1 : 0;
56
57# View mode: list (default) | new | edit
58my $view = ($form->{new} && $form->{new} eq '1') ? 'new'
59 : ($form->{edit_id} && $form->{edit_id} =~ /^\d+$/) ? 'edit'
60 : 'list';
61my $edit_id = ($form->{edit_id} || '');
62$edit_id =~ s/[^0-9]//g;
63
64my $dbh = $db->db_connect();
65my $schema_ready = $promo->schema_ready($db, $dbh, $DB);
66
67# Find the seller's primary storefront (one per user today).
68my $sf = $db->db_readwrite($dbh,
69 qq~SELECT id, name FROM `${DB}`.storefronts WHERE user_id='$uid' ORDER BY id LIMIT 1~,
70 $ENV{SCRIPT_NAME}, __LINE__);
71my $store_id = ($sf && $sf->{id}) ? $sf->{id} : 0;
72my $store_name = ($sf && $sf->{name}) ? $sf->{name} : 'Your storefront';
73
74# Load list rows (always, used to render the counts + table when view=list).
75my @rows_raw = $schema_ready ? $promo->list_for_seller($db, $dbh, $DB, $uid, 100) : ();
76
77my @rows;
78my $cnt_active = 0; my $cnt_scheduled = 0; my $cnt_paused = 0; my $cnt_expired = 0;
79foreach my $r (@rows_raw) {
80 my $effective = $promo->effective_status($r);
81 $cnt_active++ if $effective eq 'active';
82 $cnt_scheduled++ if $effective eq 'scheduled';
83 $cnt_paused++ if $effective eq 'paused';
84 $cnt_expired++ if $effective eq 'expired' || $effective eq 'exhausted';
85
86 push @rows, _row_for_template($r, $promo, $db, $dbh, $DB);
87}
88
89# Edit-mode load.
90my %edit_row;
91my @edit_models;
92if ($view eq 'edit' && $edit_id) {
93 my $e = $promo->get_for_seller($db, $dbh, $DB, $edit_id, $uid);
94 if ($e && $e->{id}) {
95 %edit_row = _row_for_edit_form($e);
96 @edit_models = $promo->models_for_promo($db, $dbh, $DB, $edit_id);
97 } else {
98 $view = 'list';
99 }
100}
101
102# Available models for the picker (only on new/edit views).
103my @user_models;
104if ($view eq 'new' || $view eq 'edit') {
105 @user_models = $db->db_readwrite_multiple($dbh, qq~
106 SELECT id, title, status
107 FROM `${DB}`.models
108 WHERE user_id='$uid' AND purged_at IS NULL
109 ORDER BY title ASC
110 LIMIT 500
111 ~, $ENV{SCRIPT_NAME}, __LINE__);
112 # Mark which model_ids are linked when editing.
113 my %sel = map { $_->{model_id} => 1 } @edit_models;
114 foreach my $m (@user_models) {
115 $m->{is_selected} = $sel{ $m->{id} } ? 1 : 0;
116 }
117}
118
119# Flash banner from action handler redirects.
120my %flash;
121if ($form->{ok} eq 'created') { %flash = (kind=>'ok', msg=>'Promotion created.'); }
122elsif ($form->{ok} eq 'updated') { %flash = (kind=>'ok', msg=>'Promotion updated.'); }
123elsif ($form->{ok} eq 'deleted') { %flash = (kind=>'ok', msg=>'Promotion deleted.'); }
124elsif ($form->{ok} eq 'activated') { %flash = (kind=>'ok', msg=>'Promotion is now active.'); }
125elsif ($form->{ok} eq 'paused') { %flash = (kind=>'ok', msg=>'Promotion paused.'); }
126elsif ($form->{err} eq 'bad_input') { %flash = (kind=>'err', msg=>'Required fields missing.'); }
127elsif ($form->{err} eq 'denied') { %flash = (kind=>'err', msg=>'Action not permitted.'); }
128
129$db->db_disconnect($dbh);
130
131my $tvars = {
132 user_id => $uid,
133 store_id => $store_id,
134 store_name => $store_name,
135
136 is_list => ($view eq 'list') ? 1 : 0,
137 is_new => ($view eq 'new') ? 1 : 0,
138 is_edit => ($view eq 'edit') ? 1 : 0,
139
140 rows => \@rows,
141 has_rows => scalar(@rows) ? 1 : 0,
142 cnt_active => $cnt_active,
143 cnt_scheduled => $cnt_scheduled,
144 cnt_paused => $cnt_paused,
145 cnt_expired => $cnt_expired,
146 cnt_total => scalar(@rows),
147
148 edit => \%edit_row,
149 has_edit => %edit_row ? 1 : 0,
150 user_models => \@user_models,
151 has_user_models => scalar(@user_models) ? 1 : 0,
152
153 schema_ready => $schema_ready ? 1 : 0,
154 schema_missing => $schema_ready ? 0 : 1,
155
156 flash_kind => $flash{kind} || '',
157 flash_msg => $flash{msg} || '',
158 has_flash => %flash ? 1 : 0,
159
160 tutorial_mode => $tutorial_mode,
161 not_tutorial_mode => $tutorial_mode ? 0 : 1,
162};
163
164# Tutorial sample data: only meaningful on the list view.
165if ($tutorial_mode && $view eq 'list') {
166 $tvars->{rows} = [
167 { id=>1, name=>'Spring 20 percent off', kind_label=>'Coupon', is_coupon=>1, is_sale=>0, is_bundle=>0, is_flash=>0,
168 discount_label=>'20% off', code=>'SPRING20', code_present=>1, public_slug=>'spring20', slug_present=>1,
169 status=>'active', status_label=>'Active', is_active=>1, is_paused=>0, is_scheduled=>0, is_expired=>0, is_draft=>0,
170 starts_at=>'2026-04-01 00:00:00', ends_at=>'2026-06-01 00:00:00', has_window=>1,
171 current_redemptions=>87, max_redemptions=>500, has_cap=>1, cap_label=>'87 of 500 used',
172 discount_given_display=>'$1,160.00', edit_url=>'/promotions.cgi?edit_id=1', tutorial_demo=>1 },
173 { id=>2, name=>'Skirmisher bundle special', kind_label=>'Bundle', is_coupon=>0, is_sale=>0, is_bundle=>1, is_flash=>0,
174 discount_label=>'Bundle for $39.00', code=>'', code_present=>0, public_slug=>'skirmisher-bundle', slug_present=>1,
175 status=>'active', status_label=>'Active', is_active=>1, is_paused=>0, is_scheduled=>0, is_expired=>0, is_draft=>0,
176 starts_at=>'2026-05-01 00:00:00', ends_at=>'2026-07-01 00:00:00', has_window=>1,
177 current_redemptions=>34, max_redemptions=>0, has_cap=>0, cap_label=>'34 used',
178 discount_given_display=>'$612.00', edit_url=>'/promotions.cgi?edit_id=2', tutorial_demo=>1 },
179 { id=>3, name=>'Memorial Day flash sale', kind_label=>'Flash sale', is_coupon=>0, is_sale=>0, is_bundle=>0, is_flash=>1,
180 discount_label=>'30% off', code=>'', code_present=>0, public_slug=>'memorial-day', slug_present=>1,
181 status=>'scheduled', status_label=>'Scheduled', is_active=>0, is_paused=>0, is_scheduled=>1, is_expired=>0, is_draft=>0,
182 starts_at=>'2026-05-23 00:00:00', ends_at=>'2026-05-27 23:59:00', has_window=>1,
183 current_redemptions=>0, max_redemptions=>200, has_cap=>1, cap_label=>'0 of 200 used',
184 discount_given_display=>'$0.00', edit_url=>'/promotions.cgi?edit_id=3', tutorial_demo=>1 },
185 { id=>4, name=>'First-time buyer 10 off', kind_label=>'Coupon', is_coupon=>1, is_sale=>0, is_bundle=>0, is_flash=>0,
186 discount_label=>'$10.00 off', code=>'WELCOME10', code_present=>1, public_slug=>'welcome10', slug_present=>1,
187 status=>'active', status_label=>'Active', is_active=>1, is_paused=>0, is_scheduled=>0, is_expired=>0, is_draft=>0,
188 starts_at=>'2026-01-01 00:00:00', ends_at=>'', has_window=>0,
189 current_redemptions=>142, max_redemptions=>0, has_cap=>0, cap_label=>'142 used',
190 discount_given_display=>'$1,420.00', edit_url=>'/promotions.cgi?edit_id=4', tutorial_demo=>1 },
191 { id=>5, name=>'Old test promo', kind_label=>'Sale', is_coupon=>0, is_sale=>1, is_bundle=>0, is_flash=>0,
192 discount_label=>'15% off', code=>'', code_present=>0, public_slug=>'', slug_present=>0,
193 status=>'expired', status_label=>'Expired', is_active=>0, is_paused=>0, is_scheduled=>0, is_expired=>1, is_draft=>0,
194 starts_at=>'2026-03-01 00:00:00', ends_at=>'2026-03-31 23:59:00', has_window=>1,
195 current_redemptions=>62, max_redemptions=>0, has_cap=>0, cap_label=>'62 used',
196 discount_given_display=>'$540.00', edit_url=>'/promotions.cgi?edit_id=5', tutorial_demo=>1 },
197 ];
198 $tvars->{has_rows} = 1;
199 $tvars->{cnt_active} = 3;
200 $tvars->{cnt_scheduled} = 1;
201 $tvars->{cnt_paused} = 0;
202 $tvars->{cnt_expired} = 1;
203 $tvars->{cnt_total} = 5;
204 $tvars->{schema_ready} = 1;
205 $tvars->{schema_missing}= 0;
206}
207
208print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
209
210my $body = join('', $tfile->template('shopcart_promotions.html', $tvars, $userinfo));
211
212$wrap->render({
213 userinfo => $userinfo,
214 page_key => 'promotions',
215 title => 'Promotions',
216 body => $body,
217});
218
219# ----------------------------------------------------------------- helpers
220sub _row_for_template {
221 my ($r, $promo, $db, $dbh, $DB) = @_;
222 my $eff = $promo->effective_status($r);
223 return {
224 id => $r->{id},
225 name => _h($r->{name} || ''),
226 kind_label => _kind_label($r->{kind}),
227 is_coupon => ($r->{kind} eq 'coupon') ? 1 : 0,
228 is_sale => ($r->{kind} eq 'sale') ? 1 : 0,
229 is_bundle => ($r->{kind} eq 'bundle') ? 1 : 0,
230 is_flash => ($r->{kind} eq 'flash_sale') ? 1 : 0,
231 discount_label => $promo->discount_label($r),
232 code => _h($r->{code} || ''),
233 code_present => ($r->{code} && $r->{code} ne '') ? 1 : 0,
234 public_slug => _h($r->{public_slug} || ''),
235 slug_present => ($r->{public_slug} && $r->{public_slug} ne '') ? 1 : 0,
236 status => $eff,
237 status_label => $promo->status_label($eff),
238 is_active => $eff eq 'active' ? 1 : 0,
239 is_paused => $eff eq 'paused' ? 1 : 0,
240 is_scheduled => $eff eq 'scheduled' ? 1 : 0,
241 is_expired => ($eff eq 'expired' || $eff eq 'exhausted') ? 1 : 0,
242 is_draft => $eff eq 'draft' ? 1 : 0,
243 starts_at => $r->{starts_at} || '',
244 ends_at => $r->{ends_at} || '',
245 has_window => (($r->{starts_at} || $r->{ends_at}) ? 1 : 0),
246 current_redemptions => $r->{current_redemptions} || $promo->redemption_count($db, $dbh, $DB, $r->{id}),
247 max_redemptions => defined $r->{max_redemptions} ? $r->{max_redemptions} : 0,
248 has_cap => (defined $r->{max_redemptions} && $r->{max_redemptions} > 0) ? 1 : 0,
249 cap_label => _cap_label($r),
250 discount_given_display => '$' . sprintf('%.2f', ($promo->total_discount_given_cents($db, $dbh, $DB, $r->{id}) || 0) / 100),
251 edit_url => '/promotions.cgi?edit_id=' . $r->{id},
252 };
253}
254
255sub _row_for_edit_form {
256 my ($r) = @_;
257 my $pct = ($r->{discount_value_bp} || 0) / 100;
258 my $amt = ($r->{discount_value_cents} || 0) / 100;
259 my $min = ($r->{minimum_order_cents} || 0) / 100;
260 return (
261 id => $r->{id},
262 name => _h($r->{name} || ''),
263 description => _h($r->{description} || ''),
264 kind => $r->{kind},
265 is_coupon => ($r->{kind} eq 'coupon') ? 1 : 0,
266 is_sale => ($r->{kind} eq 'sale') ? 1 : 0,
267 is_bundle => ($r->{kind} eq 'bundle') ? 1 : 0,
268 is_flash => ($r->{kind} eq 'flash_sale') ? 1 : 0,
269 discount_type => $r->{discount_type},
270 is_dtype_pct => ($r->{discount_type} eq 'percent') ? 1 : 0,
271 is_dtype_amt => ($r->{discount_type} eq 'fixed_amount') ? 1 : 0,
272 is_dtype_bun => ($r->{discount_type} eq 'bundle_price') ? 1 : 0,
273 is_dtype_free => ($r->{discount_type} eq 'free') ? 1 : 0,
274 discount_value_pct => sprintf('%.0f', $pct),
275 discount_value_amt => sprintf('%.2f', $amt),
276 minimum_order => sprintf('%.2f', $min),
277 code => _h($r->{code} || ''),
278 public_slug => _h($r->{public_slug} || ''),
279 starts_at_local => _to_local_input($r->{starts_at}),
280 ends_at_local => _to_local_input($r->{ends_at}),
281 max_redemptions => defined $r->{max_redemptions} ? $r->{max_redemptions} : '',
282 max_per_user => defined $r->{max_per_user} ? $r->{max_per_user} : '',
283 new_customers_only => $r->{new_customers_only} ? 1 : 0,
284 status => $r->{status},
285 status_label => MODS::ShopCart::Promotions->status_label($r->{status}),
286 is_status_active => ($r->{status} eq 'active') ? 1 : 0,
287 is_status_paused => ($r->{status} eq 'paused') ? 1 : 0,
288 is_status_draft => ($r->{status} eq 'draft') ? 1 : 0,
289 );
290}
291
292sub _to_local_input {
293 my $ts = shift;
294 return '' unless defined $ts && $ts ne '';
295 if ($ts =~ /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/) {
296 return "$1T$2"; # HTML datetime-local format
297 }
298 return '';
299}
300
301sub _kind_label {
302 my $k = shift || '';
303 return 'Coupon' if $k eq 'coupon';
304 return 'Sale' if $k eq 'sale';
305 return 'Bundle' if $k eq 'bundle';
306 return 'Flash sale' if $k eq 'flash_sale';
307 return ucfirst $k;
308}
309
310sub _cap_label {
311 my ($r) = @_;
312 my $cur = $r->{current_redemptions} || 0;
313 if (defined $r->{max_redemptions} && $r->{max_redemptions} > 0) {
314 return "$cur of $r->{max_redemptions} used";
315 }
316 return "$cur used";
317}
318
319sub _h {
320 my $s = shift;
321 $s //= '';
322 $s =~ s/&/&/g;
323 $s =~ s/</&lt;/g;
324 $s =~ s/>/&gt;/g;
325 $s =~ s/"/&quot;/g;
326 return $s;
327}
328
329#======================================================================
330# promotion_action entry: POST handler
331# act=create -- create a new promotion
332# act=update -- update an existing promotion (with ownership check)
333# act=activate -- set status active
334# act=pause -- set status paused
335# act=archive -- set status archived
336# act=delete -- delete promotion (with ownership check)
337#======================================================================
338sub _handle_action {
339 my ($q, $form) = @_;
340
341 my $act = defined $form->{act} ? $form->{act} : '';
342
343 my $dbh = $db->db_connect();
344 unless ($promo->schema_ready($db, $dbh, $DB)) {
345 $db->db_disconnect($dbh);
346 _redirect('/promotions.cgi?err=schema');
347 return;
348 }
349
350 # Find the seller's primary storefront -- new rows are attached to it.
351 my $sf = $db->db_readwrite($dbh,
352 qq~SELECT id FROM `${DB}`.storefronts WHERE user_id='$uid' ORDER BY id LIMIT 1~,
353 $ENV{SCRIPT_NAME}, __LINE__);
354 my $store_id = ($sf && $sf->{id}) ? $sf->{id} : 0;
355
356 if ($act eq 'create') {
357 unless ($store_id) {
358 $db->db_disconnect($dbh);
359 _redirect('/promotions.cgi?err=no_storefront');
360 return;
361 }
362 my $new_id = $promo->create($db, $dbh, $DB,
363 scope => 'storefront',
364 storefront_id => $store_id,
365 kind => $form->{kind},
366 name => $form->{name},
367 description => $form->{description},
368 discount_type => $form->{discount_type},
369 discount_value_bp => _percent_to_bp($form->{discount_value_pct}),
370 discount_value_cents => _dollars_to_cents($form->{discount_value_amt}),
371 minimum_order_cents => _dollars_to_cents($form->{minimum_order}),
372 code => $form->{code},
373 public_slug => $form->{public_slug},
374 starts_at => $form->{starts_at_local},
375 ends_at => $form->{ends_at_local},
376 max_redemptions => $form->{max_redemptions},
377 max_per_user => $form->{max_per_user},
378 new_customers_only => $form->{new_customers_only},
379 status => $form->{publish_now} ? 'active' : 'draft',
380 created_by_user_id => $uid,
381 );
382 unless ($new_id) {
383 $db->db_disconnect($dbh);
384 _redirect('/promotions.cgi?err=bad_input');
385 return;
386 }
387 _attach_models($promo, $db, $dbh, $DB, $new_id, $form);
388 $db->db_disconnect($dbh);
389 _redirect('/promotions.cgi?ok=created');
390 return;
391 }
392
393 if ($act eq 'update') {
394 my $id = $form->{id}; $id =~ s/[^0-9]//g;
395 my $row = $id ? $promo->get_for_seller($db, $dbh, $DB, $id, $uid) : {};
396 unless ($row && $row->{id}) {
397 $db->db_disconnect($dbh);
398 _redirect('/promotions.cgi?err=denied');
399 return;
400 }
401 my $ok = $promo->update($db, $dbh, $DB, $id,
402 scope => 'storefront',
403 storefront_id => $store_id,
404 kind => $form->{kind},
405 name => $form->{name},
406 description => $form->{description},
407 discount_type => $form->{discount_type},
408 discount_value_bp => _percent_to_bp($form->{discount_value_pct}),
409 discount_value_cents => _dollars_to_cents($form->{discount_value_amt}),
410 minimum_order_cents => _dollars_to_cents($form->{minimum_order}),
411 code => $form->{code},
412 public_slug => $form->{public_slug},
413 starts_at => $form->{starts_at_local},
414 ends_at => $form->{ends_at_local},
415 max_redemptions => $form->{max_redemptions},
416 max_per_user => $form->{max_per_user},
417 new_customers_only => $form->{new_customers_only},
418 );
419 unless ($ok) {
420 $db->db_disconnect($dbh);
421 _redirect('/promotions.cgi?err=bad_input');
422 return;
423 }
424 # Refresh model attachments.
425 $promo->clear_models($db, $dbh, $DB, $id);
426 _attach_models($promo, $db, $dbh, $DB, $id, $form);
427
428 $db->db_disconnect($dbh);
429 _redirect('/promotions.cgi?ok=updated');
430 return;
431 }
432
433 if ($act eq 'activate' || $act eq 'pause' || $act eq 'archive') {
434 my $id = $form->{id}; $id =~ s/[^0-9]//g;
435 my $row = $id ? $promo->get_for_seller($db, $dbh, $DB, $id, $uid) : {};
436 unless ($row && $row->{id}) {
437 $db->db_disconnect($dbh);
438 _redirect('/promotions.cgi?err=denied');
439 return;
440 }
441 my $new_status =
442 $act eq 'activate' ? 'active' :
443 $act eq 'pause' ? 'paused' :
444 'archived';
445 $promo->set_status($db, $dbh, $DB, $id, $new_status);
446 $db->db_disconnect($dbh);
447 _redirect('/promotions.cgi?ok=' . ($act eq 'activate' ? 'activated' : $act eq 'pause' ? 'paused' : 'updated'));
448 return;
449 }
450
451 if ($act eq 'delete') {
452 my $id = $form->{id}; $id =~ s/[^0-9]//g;
453 my $row = $id ? $promo->get_for_seller($db, $dbh, $DB, $id, $uid) : {};
454 unless ($row && $row->{id}) {
455 $db->db_disconnect($dbh);
456 _redirect('/promotions.cgi?err=denied');
457 return;
458 }
459 $promo->delete_promo($db, $dbh, $DB, $id);
460 $db->db_disconnect($dbh);
461 _redirect('/promotions.cgi?ok=deleted');
462 return;
463 }
464
465 $db->db_disconnect($dbh);
466 _redirect('/promotions.cgi?err=unknown_act');
467 return;
468}
469
470# ------------------------------------------------------------- action helpers
471sub _redirect {
472 my $url = shift;
473 print "Status: 302 Found\nLocation: $url\n\n";
474}
475
476sub _percent_to_bp {
477 my $v = shift;
478 return 0 unless defined $v && $v ne '';
479 $v =~ s/[^0-9.]//g;
480 return 0 unless length $v;
481 my $bp = int($v * 100);
482 $bp = 0 if $bp < 0;
483 $bp = 10000 if $bp > 10000;
484 return $bp;
485}
486
487sub _dollars_to_cents {
488 my $v = shift;
489 return 0 unless defined $v && $v ne '';
490 $v =~ s/[^0-9.]//g;
491 return 0 unless length $v;
492 return int($v * 100 + 0.5);
493}
494
495# Pull the selected model ids out of the form (checkbox names like
496# model_id[]). Attach each at the requested role.
497sub _attach_models {
498 my ($promo, $db, $dbh, $DB, $promo_id, $form) = @_;
499 # CGI may store multi-valued fields as "\0"-joined strings.
500 my @ids;
501 my $raw = $form->{'model_ids'};
502 if (defined $raw && $raw ne '') {
503 @ids = split(/\0/, $raw);
504 }
505 foreach my $mid (@ids) {
506 $mid =~ s/[^0-9]//g;
507 next unless $mid;
508 my $role = $form->{"role_$mid"} || 'target';
509 $promo->add_model($db, $dbh, $DB, $promo_id, $mid, $role);
510 }
511}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help