From 50a4b564e5fba42cd63ad6f919c4a013ae2c3308 Mon Sep 17 00:00:00 2001 From: JT Smith Date: Wed, 2 Apr 2008 22:46:40 +0000 Subject: [PATCH] added item and sku caching to cart ribbon discounts now work checklist form control works more like you'd expect added badge groups --- docs/upgrades/upgrade_7.5.2-7.5.3.pl | 10 +- lib/WebGUI/Asset.pm | 1 + lib/WebGUI/Asset/Sku/EMSRibbon.pm | 22 +++ lib/WebGUI/Asset/Sku/EMSTicket.pm | 130 ++++++++++++++++-- lib/WebGUI/Asset/Sku/EMSToken.pm | 15 ++ .../Asset/Wobject/EventManagementSystem.pm | 129 +++++++++++++++-- lib/WebGUI/Form/CheckList.pm | 2 +- lib/WebGUI/Shop/Cart.pm | 36 ++++- lib/WebGUI/Shop/CartItem.pm | 12 +- .../English/Asset_EventManagementSystem.pm | 84 ++++++++++- 10 files changed, 401 insertions(+), 40 deletions(-) diff --git a/docs/upgrades/upgrade_7.5.2-7.5.3.pl b/docs/upgrades/upgrade_7.5.2-7.5.3.pl index c0b7049a7..c4d199aea 100644 --- a/docs/upgrades/upgrade_7.5.2-7.5.3.pl +++ b/docs/upgrades/upgrade_7.5.2-7.5.3.pl @@ -116,6 +116,12 @@ sub upgradeEMS { ribbonAssetId varchar(22) binary not null, primary key (badgeId,ribbonAssetId) )"); + $db->write("create table EMSBadgeGroup ( + badgeGroupId varchar(22) binary not null primary key, + emsAssetId varchar(22) binary not null, + name varchar(100), + badgeList text + )"); $db->write("create table EMSBadge ( assetId varchar(22) binary not null, revisionDate bigint not null, @@ -132,7 +138,8 @@ sub upgradeEMS { duration float not null default 1.0, eventNumber int, location varchar(100), - relatedBadges mediumtext, + relatedBadgeGroups mediumtext, + relatedRibbons mediumtext, primary key (assetId, revisionDate) )"); $db->write("create table EMSToken ( @@ -144,6 +151,7 @@ sub upgradeEMS { $db->write("create table EMSRibbon ( assetId varchar(22) binary not null, revisionDate bigint not null, + percentageDiscount float not null default 10.0, price float not null default 0.00, primary key (assetId, revisionDate) )"); diff --git a/lib/WebGUI/Asset.pm b/lib/WebGUI/Asset.pm index c2fa15c66..ce5b59132 100644 --- a/lib/WebGUI/Asset.pm +++ b/lib/WebGUI/Asset.pm @@ -2325,6 +2325,7 @@ sub www_add { } my %properties = ( %prototypeProperties, + parentId => $self->getId, groupIdView => $self->get("groupIdView"), groupIdEdit => $self->get("groupIdEdit"), ownerUserId => $self->get("ownerUserId"), diff --git a/lib/WebGUI/Asset/Sku/EMSRibbon.pm b/lib/WebGUI/Asset/Sku/EMSRibbon.pm index 03964f974..0f433f710 100644 --- a/lib/WebGUI/Asset/Sku/EMSRibbon.pm +++ b/lib/WebGUI/Asset/Sku/EMSRibbon.pm @@ -63,6 +63,13 @@ sub definition { label => $i18n->get("price"), hoverHelp => $i18n->get("price help"), }, + percentageDiscount => { + tab => "shop", + fieldType => "float", + defaultValue => 10.0, + label => $i18n->get("percentage discount"), + hoverHelp => $i18n->get("percentage discount help"), + }, ); push(@{$definition}, { assetName => $i18n->get('ems ribbon'), @@ -192,6 +199,21 @@ sub www_addToCart { return $self->getParent->www_buildBadge($badgeId); } +#------------------------------------------------------------------- + +=head2 www_delete + +Override to return to appropriate page. + +=cut + +sub www_delete { + my ($self) = @_; + $self->SUPER::www_delete; + return $self->getParent->www_buildBadge(undef,'ribbons'); +} + + #------------------------------------------------------------------- =head2 www_edit () diff --git a/lib/WebGUI/Asset/Sku/EMSTicket.pm b/lib/WebGUI/Asset/Sku/EMSTicket.pm index 9231fd6a4..a07f51e3c 100644 --- a/lib/WebGUI/Asset/Sku/EMSTicket.pm +++ b/lib/WebGUI/Asset/Sku/EMSTicket.pm @@ -19,7 +19,6 @@ use Tie::IxHash; use base 'WebGUI::Asset::Sku'; - =head1 NAME Package WebGUI::Asset::Sku::EMSTicket @@ -86,8 +85,8 @@ sub definition { }, eventNumber => { tab => "properties", - fieldType => "integer", - defaultValue => $session->db->quickScalar("select max(eventNumber)+1 from EMSTicket"), + fieldType => "integer", + customDrawMethod=> 'drawEventNumberField', label => $i18n->get("event number"), hoverHelp => $i18n->get("event number help"), }, @@ -107,19 +106,25 @@ sub definition { hoverHelp => $i18n->get("duration help"), }, location => { + fieldType => "combo", tab => "properties", - fieldType => "combo", - options => $session->db->buildHashRef("select distinct(location) from EMSTicket order by location"), + customDrawMethod=> 'drawLocationField', label => $i18n->get("location"), hoverHelp => $i18n->get("location help"), }, - relatedBadges => { + relatedBadgeGroups => { tab => "properties", - fieldType => "checkList", - options => {}, - defaultValue => undef, - label => $i18n->get("related badges"), - hoverHelp => $i18n->get("related badges help"), + fieldType => "checkList", + customDrawMethod=> 'drawRelatedBadgeGroupsField', + label => $i18n->get("related badge groups"), + hoverHelp => $i18n->get("related badge groups help"), + }, + relatedRibbons => { + tab => "properties", + fieldType => "checkList", + customDrawMethod=> 'drawRelatedRibbonsField', + label => $i18n->get("related ribbons"), + hoverHelp => $i18n->get("related ribbons help"), }, ); push(@{$definition}, { @@ -133,6 +138,83 @@ sub definition { return $class->SUPER::definition($session, $definition); } +#------------------------------------------------------------------- + +=head2 drawEventNumberField () + +Draws the field for the eventNumber property. + +=cut + +sub drawEventNumberField { + my ($self, $params) = @_; + my $default = $self->session->db->quickScalar("select max(eventNumber)+1 from EMSTicket left join asset using (assetId) + where parentId=?",[$self->get('parentId')]); + return WebGUI::Form::integer($self->session, { + name => $params->{name}, + value => $self->get($params->{name}), + defaultValue => $default, + }); +} + +#------------------------------------------------------------------- + +=head2 drawLocationField () + +Draws the field for the location property. + +=cut + +sub drawLocationField { + my ($self, $params) = @_; + my $options = $self->session->db->buildHashRef("select distinct(location) from EMSTicket left join asset using (assetId) + where parentId=? order by location",[$self->get('parentId')]); + return WebGUI::Form::combo($self->session, { + name => $params->{name}, + value => $self->get($params->{name}), + options => $options, + }); +} + +#------------------------------------------------------------------- + +=head2 drawRelatedBadgeGroupsField () + +Draws the field for the relatedBadgeGroups property. + +=cut + +sub drawRelatedBadgeGroupsField { + my ($self, $params) = @_; + return WebGUI::Form::checkList($self->session, { + name => $params->{name}, + value => $self->get($params->{name}), + vertical => 1, + options => $self->getParent->getBadgeGroups, + }); +} + +#------------------------------------------------------------------- + +=head2 drawRelatedRibbonsField () + +Draws the field for the relatedRibbons property. + +=cut + +sub drawRelatedRibbonsField { + my ($self, $params) = @_; + my %ribbons = (); + foreach my $ribbon (@{$self->getParent->getRibbons}) { + $ribbons{$ribbon->getId} = $ribbon->getTitle; + } + return WebGUI::Form::checkList($self->session, { + name => $params->{name}, + value => $self->get($params->{name}), + vertical => 1, + options => \%ribbons, + }); +} #------------------------------------------------------------------- @@ -164,13 +246,20 @@ sub getMaxAllowedInCart { =head2 getPrice -Returns the value of the price field +Returns the value of the price field, after applying ribbon discounts. =cut sub getPrice { my $self = shift; - return $self->get("price"); + my @ribbonIds = split("\n", $self->get('relatedRibbons')); + my $price = $self->get("price"); + foreach my $item (@{$self->getCart->getItemsByAssetId(\@ribbonIds)}) { + my $ribbon = $item->getSku; + $price -= ($price * $ribbon->get('percentageDiscount') / 100); + last; + } + return $price; } #------------------------------------------------------------------- @@ -296,6 +385,21 @@ sub www_addToCart { return $self->getParent->www_buildBadge($badgeId); } +#------------------------------------------------------------------- + +=head2 www_delete + +Override to return to appropriate page. + +=cut + +sub www_delete { + my ($self) = @_; + $self->SUPER::www_delete; + return $self->getParent->www_buildBadge(undef,'tickets'); +} + + #------------------------------------------------------------------- =head2 www_edit () diff --git a/lib/WebGUI/Asset/Sku/EMSToken.pm b/lib/WebGUI/Asset/Sku/EMSToken.pm index 9be3b2c08..8a32f1907 100644 --- a/lib/WebGUI/Asset/Sku/EMSToken.pm +++ b/lib/WebGUI/Asset/Sku/EMSToken.pm @@ -189,6 +189,21 @@ sub www_addToCart { return $self->getParent->www_buildBadge($badgeId); } +#------------------------------------------------------------------- + +=head2 www_delete + +Override to return to appropriate page. + +=cut + +sub www_delete { + my ($self) = @_; + $self->SUPER::www_delete; + return $self->getParent->www_buildBadge(undef,'tokens'); +} + + #------------------------------------------------------------------- =head2 www_edit () diff --git a/lib/WebGUI/Asset/Wobject/EventManagementSystem.pm b/lib/WebGUI/Asset/Wobject/EventManagementSystem.pm index 9a676b638..6dc944b6d 100644 --- a/lib/WebGUI/Asset/Wobject/EventManagementSystem.pm +++ b/lib/WebGUI/Asset/Wobject/EventManagementSystem.pm @@ -109,6 +109,71 @@ sub definition { #------------------------------------------------------------------- +=head2 getBadges () + +Returns an array reference of badge objects. + +=cut + +sub getBadges { + my $self = shift; + return $self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSBadge']}); +} + +#------------------------------------------------------------------- + +=head2 getBadgeGroups () + +Returns a hash reference of id,name pairs of badge groups. + +=cut + +sub getBadgeGroups { + my $self = shift; + return $self->session->db->buildHashRef("select badgeGroupId,name from EMSBadgeGroup where emsAssetId=?",[$self->getId]); +} + +#------------------------------------------------------------------- + +=head2 getRibbons () + +Returns an array reference of ribbon objects. + +=cut + +sub getRibbons { + my $self = shift; + return $self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSRibbon']}); +} + +#------------------------------------------------------------------- + +=head2 getTickets () + +Returns an array reference of ticket objects. + +=cut + +sub getTickets { + my $self = shift; + return $self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSTicket']}); +} + +#------------------------------------------------------------------- + +=head2 getTokens () + +Returns an array reference of badge objects. + +=cut + +sub getTokens { + my $self = shift; + return $self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSToken']}); +} + +#------------------------------------------------------------------- + =head2 prepareView ( ) See WebGUI::Asset::prepareView() for details. @@ -181,9 +246,12 @@ sub view { my $output = q|
-

Add a badge +

+ Add a badgeView Events - • View Cart

+ • View Cart + • Badge Groups +

|.$self->get('badgeInstructions').q|

@@ -394,7 +462,6 @@ sub www_buildBadge { }); } $var{otherBadgesInCart} = \@otherBadges; - #return JSON->new->pretty->encode(\@otherBadges); # render return $self->processStyle($self->processTemplate(\%var,$self->get('badgeBuilderTemplateId'))); @@ -410,6 +477,8 @@ Deletes a badge group. sub www_deleteBadgeGroup { my $self = shift; + return $self->session->privilege->insufficient() unless $self->canEdit; + $self->session->db->deleteRow("EMSBadgeGroup","badgeGroupId",$self->session->form->get("badgeGroupId")); return $self->www_manageBadgeGroups; } @@ -424,29 +493,33 @@ Displays an edit screen for a badge group. sub www_editBadgeGroup { my $self = shift; + return $self->session->privilege->insufficient() unless $self->canEdit; my ($form, $db) = $self->session->quick(qw(form db)); my $f = WebGUI::HTMLForm->new($self->session, action=>$self->getUrl); my $badgeGroup = $db->getRow("EMSBadgeGroup","badgeGroupId",$form->get('badgeGroupId')); + $badgeGroup->{badgeList} = ($badgeGroup->{badgeList} ne "") ? JSON::decode_json($badgeGroup->{badgeList}) : []; my $i18n = WebGUI::International->new($self->session, "Asset_EventManagementSystem"); $f->hidden(name=>'func', value=>'editBadgeGroupSave'); $f->hidden(name=>'badgeGroupId', value=>$form->get('badgeGroupId')); $f->text( name => 'name', value => $badgeGroup->{name}, - label =>, - hoverHelp =>, + label => $i18n->get('badge group name'), + hoverHelp => $i18n->get('badge group name help'), ); $f->checkList( name => 'badgeList', - value => JSON::decode_json($badgeGroup->{badgeList}), + value => $badgeGroup->{badgeList}, options => $db->buildHashRef("select asset.assetId,assetData.title from asset left join assetData using (assetId) where asset.parentId=? and assetData.revisionDate= - (SELECT max(revisionDate) from assetData where assetData.assetId=asset.assetId)", - [$self->getId]), - label => , - hoverHelp =>, - ); - return $self->processStyle($f->print); + (SELECT max(revisionDate) from assetData where assetData.assetId=asset.assetId) + and asset.className='WebGUI::Asset::Sku::EMSBadge'", [$self->getId]), + vertical => 1, + label => $i18n->get('badge list'), + hoverHelp => $i18n->get('badge list help'), + ); + $f->submit; + return $self->processStyle('

'.$i18n->get('badge groups').'

'.$f->print); } @@ -460,6 +533,16 @@ Saves a badge group. sub www_editBadgeGroupSave { my $self = shift; + return $self->session->privilege->insufficient() unless $self->canEdit; + my $form = $self->session->form; + my $id = $form->get("badgeGroupId") || "new"; + my @badgeList = $form->get("badgeList",'checkList'); + $self->session->db->setRow("EMSBadgeGroup","badgeGroupId",{ + badgeGroupId => $id, + emsAssetId => $self->getId, + name => $form->get('name'), + badgeList => JSON::encode_json(\@badgeList), + }); return $self->www_manageBadgeGroups; } @@ -478,7 +561,7 @@ sub www_getBadgesAsJson { return $session->privilege->insufficient() unless $self->canView; my ($db, $form) = $session->quick(qw(db form)); my %results = (); - foreach my $badge (@{$self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSBadge']})}) { + foreach my $badge (@{$self->getBadges}) { push(@{$results{records}}, { title => $badge->getTitle, description => $badge->get('description'), @@ -643,7 +726,7 @@ sub www_getRibbonsAsJson { return $session->privilege->insufficient() unless $self->canView; my ($db, $form) = $session->quick(qw(db form)); my %results = (); - foreach my $ribbon (@{$self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSRibbon']})}) { + foreach my $ribbon (@{$self->getRibbons}) { push(@{$results{records}}, { title => $ribbon->getTitle, description => $ribbon->get('description'), @@ -754,7 +837,7 @@ sub www_getTokensAsJson { return $session->privilege->insufficient() unless $self->canView; my ($db, $form) = $session->quick(qw(db form)); my %results = (); - foreach my $token (@{$self->getLineage(['children'],{returnObjects=>1, includeOnlyClasses=>['WebGUI::Asset::Sku::EMSToken']})}) { + foreach my $token (@{$self->getTokens}) { push(@{$results{records}}, { title => $token->getTitle, description => $token->get('description'), @@ -796,6 +879,22 @@ Displays a list of badge groups. sub www_manageBadgeGroups { my $self = shift; + my $session = $self->session; + return $session->privilege->insufficient() unless $self->canView; + my $i18n = WebGUI::International->new($session, 'Asset_EventManagementSystem'); + my $output = '

'.$i18n->get('badge groups') + .q|

|.$i18n->get('add a badge group').q| + • |.$i18n->get('view badges').q| + • |.$i18n->get('view tickets').q| +

|; + my $groups = $session->db->read("select badgeGroupId,name from EMSBadgeGroup where emsAssetId=?",[$self->getId]); + my $badgeGroups = $self->getBadgeGroups; + foreach my $id (keys %{$badgeGroups}) { + $output .= q|
[|.$i18n->get('delete').q| + / |.$i18n->get('edit').q|] + |.$badgeGroups->{$id}.q|
|; + } + return $self->processStyle($output); } #------------------------------------------------------------------- diff --git a/lib/WebGUI/Form/CheckList.pm b/lib/WebGUI/Form/CheckList.pm index f7e4e40d1..c3a4c621b 100644 --- a/lib/WebGUI/Form/CheckList.pm +++ b/lib/WebGUI/Form/CheckList.pm @@ -134,7 +134,7 @@ sub toHtml { tie my %options, 'Tie::IxHash', $self->orderedHash(); foreach my $key (keys %options) { $i++; - my $checked = (grep { $_ eq $key } @{ $self->get('value') }) + my $checked = (grep { $_ eq $key } @{ $self->correctValues($self->get('value')) }) ? 1 : 0 ; diff --git a/lib/WebGUI/Shop/Cart.pm b/lib/WebGUI/Shop/Cart.pm index 55a006f13..df9e1549b 100644 --- a/lib/WebGUI/Shop/Cart.pm +++ b/lib/WebGUI/Shop/Cart.pm @@ -37,6 +37,7 @@ These subroutines are available from this package: readonly session => my %session; private properties => my %properties; private error => my %error; +private itemCache => my %itemCache; #------------------------------------------------------------------- @@ -114,6 +115,7 @@ sub delete { $self->empty; $self->session->db->write("delete from cart where cartId=?",[$self->getId]); undef $self; + %itemCache{ref $self} = {}; return undef; } @@ -129,7 +131,8 @@ sub empty { my ($self) = @_; foreach my $item (@{$self->getItems}) { $item->remove; - } + } + %itemCache{ref $self} = {}; } #------------------------------------------------------------------- @@ -238,7 +241,13 @@ sub getItem { unless (defined $itemId && $itemId =~ m/^[A-Za-z0-9_-]{22}$/) { WebGUI::Error::InvalidParam->throw(error=>"Need an itemId."); } - return WebGUI::Shop::CartItem->new($self, $itemId); + my $id = ref $self; + if (exists $itemCache{$id}{$itemId}) { + return $itemCache{$id}{$itemId}; + } + my $item = WebGUI::Shop::CartItem->new($self, $itemId); + $itemCache{$id}{$itemId} = $item; + return $item; } #------------------------------------------------------------------- @@ -261,6 +270,28 @@ sub getItems { #------------------------------------------------------------------- +=head2 getItemsByAssetId ( assetIds ) + +Returns an array reference of WebGUI::Asset::Sku objects that have a specific asset id that are in the cart. + +=head3 assetIds + +An array reference of assetIds to look for. + +=cut + +sub getItemsByAssetId { + my ($self, $assetIds) = @_; + my @itemsObjects = (); + my $items = $self->session->db->read("select itemId from cartItem where cartId=? and assetId in (".$self->session->db->quoteAndJoin($assetIds).")",[$self->getId]); + while (my ($itemId) = $items->array) { + push(@itemsObjects, $self->getItem($itemId)); + } + return \@itemsObjects; +} + +#------------------------------------------------------------------- + =head2 getShipper () Returns the WebGUI::Shop::ShipDriver object that is attached to this cart for shipping. @@ -461,6 +492,7 @@ Remove an item from the cart and then display the cart again. sub www_removeItem { my $self = shift; my $item = $self->getItem($self->session->form->get("itemId"))->remove; + delete $itemCache{ref $self}{$item->getId}; return $self->www_view; } diff --git a/lib/WebGUI/Shop/CartItem.pm b/lib/WebGUI/Shop/CartItem.pm index bd17139e7..6198d24b5 100644 --- a/lib/WebGUI/Shop/CartItem.pm +++ b/lib/WebGUI/Shop/CartItem.pm @@ -28,7 +28,7 @@ These subroutines are available from this package: readonly cart => my %cart; private properties => my %properties; - +private skuCache => my %skuCache; #------------------------------------------------------------------- @@ -174,7 +174,15 @@ Returns an instanciated WebGUI::Asset::Sku object for this cart item. sub getSku { my ($self) = @_; - my $asset = WebGUI::Asset->newByDynamicClass($self->cart->session, $self->get("assetId")); + my $id = ref $self; + my $asset = ''; + if (exists $skuCache{$id}{$self->get("assetId")}) { + $asset = $skuCache{$id}{$self->get("assetId")}; + } + else { + $asset = WebGUI::Asset->newByDynamicClass($self->cart->session, $self->get("assetId")); + $skuCache{$id}{$self->get("assetId")} = $asset; + } $asset->applyOptions($self->get("options")); return $asset; } diff --git a/lib/WebGUI/i18n/English/Asset_EventManagementSystem.pm b/lib/WebGUI/i18n/English/Asset_EventManagementSystem.pm index d434b8bb9..f6b574972 100644 --- a/lib/WebGUI/i18n/English/Asset_EventManagementSystem.pm +++ b/lib/WebGUI/i18n/English/Asset_EventManagementSystem.pm @@ -2,9 +2,57 @@ package WebGUI::i18n::English::Asset_EventManagementSystem; use strict; our $I18N = { + 'percentage discount' => { + message => q|Percentage Discount|, + lastUpdated => 0, + context => q|a ribbon property label|, + }, + + 'percentage discount help' => { + message => q|What percentage discount will be applied to the tickets if the user purchases this ribbon?|, + lastUpdated => 0, + context => q|help for a ribbon property label|, + }, + + 'badge group name' => { + message => q|Badge Group Name|, + lastUpdated => 0, + context => q|a badge group property label|, + }, + + 'badge group name help' => { + message => q|Enter a name to group these badges under.|, + lastUpdated => 0, + context => q|help for a badge group property label|, + }, + + 'badge list' => { + message => q|Badge List|, + lastUpdated => 0, + context => q|a badge group property label|, + }, + + 'badge list help' => { + message => q|Select the badges that belong to this group.|, + lastUpdated => 0, + context => q|help for a badge group property label|, + }, + + 'badge groups' => { + message => q|Badge Groups|, + lastUpdated => 0, + context => q|a header label|, + }, + 'hours' => { message => q|Hours|, lastUpdated => 0, + context => q|subtext for the duration form field|, + }, + + 'add a badge group' => { + message => q|Add A Badge Group|, + lastUpdated => 0, context => q|a link label|, }, @@ -14,6 +62,18 @@ our $I18N = { context => q|a link label|, }, + 'view tickets' => { + message => q|View Tickets|, + lastUpdated => 0, + context => q|a link label|, + }, + + 'view badges' => { + message => q|View Badges|, + lastUpdated => 0, + context => q|a link label|, + }, + 'buy badge' => { message => q|Buy A Badge|, lastUpdated => 0, @@ -296,16 +356,28 @@ our $I18N = { context => q|a button on the add badge to cart screen|, }, - 'related badges' => { - message => q|Related Badges|, + 'related badge groups' => { + message => q|Related Badge Groups|, lastUpdated => 0, - context => q|a property label|, + context => q|a ticket property label|, }, - 'related badges help' => { - message => q|Check the badges that can act as a prerequisite to being able to attend this event.|, + 'related badge groups help' => { + message => q|Check the badge groups that can act as a prerequisite to being able to attend this event. If none are checked, then any badge will do.|, lastUpdated => 0, - context => q|help for a property label|, + context => q|help for a ticket property label|, + }, + + 'related ribbons' => { + message => q|Related Ribbons|, + lastUpdated => 0, + context => q|a ribbon ticket label|, + }, + + 'related ribbons help' => { + message => q|Check the ribbons that can provide a discount for this ticket.|, + lastUpdated => 0, + context => q|help for a ticket property label|, }, 'time zone' => {