move product import to shelf

This commit is contained in:
JT Smith 2008-07-12 21:40:27 +00:00
parent 3642628dc6
commit cc0105a9a4
9 changed files with 317 additions and 432 deletions

View file

@ -2,6 +2,7 @@
- fixed: Payment Methods Hover Help Incomplete
- fixed: Payment Method Titles Don't Match Buttons
- fixed: Gallery: Description Text appearing in Album
- Moved product imports to shelves
- fixed: Deleting Ticket from Event Management System Asset
- fixed: Thingy Search Broken

View file

@ -27,20 +27,18 @@ my $quiet; # this line required
my $session = start(); # this line required
# upgrade functions go here
moveProductImportToShelf($session);
finish($session); # this line required
#----------------------------------------------------------------------------
# Describe what our function does
#sub exampleFunction {
# my $session = shift;
# print "\tWe're doing some stuff here that you should know about... " unless $quiet;
# # and here's our code
# print "DONE!\n" unless $quiet;
#}
sub moveProductImportToShelf {
my $session = shift;
print "\tMoving product import to shelves... " unless $quiet;
unlink "../../lib/WebGUI/Shop/Products.pm";
$session->db->write("update asset set isSystem=0 where assetId='PBproductimportnode001'");
print "DONE!\n" unless $quiet;
}
# -------------- DO NOT EDIT BELOW THIS LINE --------------------------------

View file

@ -410,23 +410,6 @@ sub getPrice {
}
}
#-------------------------------------------------------------------
=head2 getProductImportNode ( session )
Constructor. Returns the product import node object. This is where the product import system will create new products.
=head3 session
A reference to the current session.
=cut
sub getProductImportNode {
my $class = shift;
my $session = shift;
return WebGUI::Asset->newByDynamicClass($session, 'PBproductimportnode001');
}
#-------------------------------------------------------------------

View file

@ -15,6 +15,10 @@ use List::MoreUtils;
use Tie::IxHash;
use WebGUI::International;
use base 'WebGUI::Asset::Wobject';
use WebGUI::Text;
use WebGUI::Storage;
use WebGUI::Exception::Shop;
use WebGUI::Asset::Sku::Product;
#-------------------------------------------------------------------
@ -50,6 +54,189 @@ sub definition {
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
=head2 exportProducts ( )
Export all products from the WebGUI system in a CSV file. For details
about the file format, see importProducts.
Returns a temporary WebGUI::Storage object containing the file. The
file will be named siteProductData.csv.
=cut
sub exportProducts {
my $self = shift;
my $session = $self->session;
my @columns = qw{sku shortdescription price weight quantity};
my $productData = WebGUI::Text::joinCSV(qw{mastersku title}, @columns) . "\n";
@columns = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @columns;
my $getAProduct = WebGUI::Asset::Sku::Product->getIsa($session);
while (my $product = $getAProduct->()) {
my $mastersku = $product->get('sku');
my $title = $product->getTitle;
my $collateri = $product->getAllCollateral('variantsJSON');
foreach my $collateral (@{ $collateri }) {
my @productFields = @{ $collateral }{ @columns };
$productData .= WebGUI::Text::joinCSV($mastersku, $title, @productFields);
$productData .= "\n";
}
}
my $storage = WebGUI::Storage->createTemp($session);
$storage->addFileFromScalar('siteProductData.csv', $productData);
return $storage;
}
#-------------------------------------------------------------------
=head2 importProducts ( $filePath )
Import products into the WebGUI system. If the master sku of a product
exists in the system, it will be updated. If master skus do not exist,
they will be added.
The first line of the file should contain only the name of the columns,
in any order. It may not contain comments.
These are the column names, each is required:
=over 4
=item *
mastersku
=item *
sku
=item *
title
=item *
shortdescription
=item *
price
=item *
weight
=item *
quantity
=back
The following lines will contain product information. Blank
lines and anything following a '#' sign will be ignored from
the second line of the file, on to the end.
Returns 1 if the import has taken place. This is to help you know
if old data has been deleted and new has been inserted.
=cut
sub importProducts {
my $self = shift;
my $filePath = shift;
my $session = $self->session;
WebGUI::Error::InvalidParam->throw(error => q{Must provide the path to a file})
unless $filePath;
WebGUI::Error::InvalidFile->throw(error => qq{File could not be found}, brokenFile => $filePath)
unless -e $filePath;
WebGUI::Error::InvalidFile->throw(error => qq{File is not readable}, brokenFile => $filePath)
unless -r $filePath;
open my $table, '<', $filePath or
WebGUI::Error->throw(error => qq{Unable to open $filePath for reading: $!\n});
my $headers;
$headers = <$table>;
chomp $headers;
my @headers = WebGUI::Text::splitCSV($headers);
WebGUI::Error::InvalidFile->throw(error => qq{Bad header found in the CSV file}, brokenFile => $filePath)
unless (join(q{-}, sort @headers) eq 'mastersku-price-quantity-shortdescription-sku-title-weight')
and (scalar @headers == 7);
my @productData = ();
my $line = 1;
while (my $productRow = <$table>) {
chomp $productRow;
$productRow =~ s/\s*#.+$//;
next unless $productRow;
local $_;
my @productRow = WebGUI::Text::splitCSV($productRow);
WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line)
unless scalar @productRow == 7;
push @productData, [ @productRow ];
}
return unless scalar @productData;
##Okay, if we got this far, then the data looks fine.
my $fetchProductId = $session->db->prepare('select p.assetId from Product as p join sku as s on p.assetId=s.assetId and p.revisionDate=s.revisionDate where s.sku=? order by p.revisionDate DESC limit 1');
my $node = $self;
@headers = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @headers;
my @collateralFields = grep { $_ ne 'title' and $_ ne 'mastersku' } @headers;
PRODUCT: foreach my $productRow (@productData) {
my %productRow;
##Order the data according to the headers, in whatever order they exist.
@productRow{ @headers } = @{ $productRow };
##Isolate just the collateral from the other product information
my %productCollateral;
@productCollateral{ @collateralFields } = @productRow{ @collateralFields };
$fetchProductId->execute([$productRow{mastersku}]);
my $asset = $fetchProductId->hashRef;
##If the assetId exists, we update data for it
if ($asset->{assetId}) {
$session->log->warn("Modifying an existing product: $productRow{sku} = $asset->{assetId}\n");
my $assetId = $asset->{assetId};
my $product = WebGUI::Asset->newPending($session, $assetId);
##Error handling for locked assets
if ($product->isLocked) {
$session->log->warn("Product is locked");
next PRODUCT if $product->isLocked;
}
if ($productRow{title} ne $product->getTitle) {
$product->update({ title => $product->fixTitle($productRow{title}) });
}
my $collaterals = $product->getAllCollateral('variantsJSON');
my $collateralSet = 0;
ROW: foreach my $collateral (@{ $collaterals }) {
next ROW unless $collateral->{sku} eq $productRow{sku};
@{ $collateral}{ @collateralFields } = @productCollateral{ @collateralFields }; ##preserve the variant Id field, assign all others
$product->setCollateral('variantsJSON', 'variantId', $collateral->{variantId}, $collateral);
$collateralSet=1;
}
if (!$collateralSet) {
##It must be a new variant
$product->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral);
}
}
else {
##Insert a new product;
$session->log->warn("Making a new product: $productRow{sku}\n");
my $newProduct = $node->addChild({className => 'WebGUI::Asset::Sku::Product'});
$newProduct->update({
title => $newProduct->fixTitle($productRow{title}),
sku => $productRow{mastersku},
});
$newProduct->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral);
$newProduct->commit;
}
}
return 1;
}
#-------------------------------------------------------------------
@ -131,5 +318,102 @@ sub view {
}
#-------------------------------------------------------------------
sub www_edit {
my $self = shift;
my $i18n = WebGUI::International->new($self->session, 'Asset_Shelf');
if ($self->getId ne "new") {
$self->getAdminConsole->addSubmenuItem($self->getUrl('func=exportProducts'),$i18n->get("export"));
$self->getAdminConsole->addSubmenuItem($self->getUrl('func=importProducts'),$i18n->get("import"));
}
return $self->SUPER::www_edit();
}
#-------------------------------------------------------------------
=head2 www_exportProducts ( )
Export all product SKUs as a CSV file. Returns a WebGUI::Storage
object containg the product file, named 'siteProductData.csv'.
=cut
sub www_exportProducts {
my $self = shift;
my $session = $self->session;
return $session->privilege->insufficient
unless $self->canEdit;
my $storage = $self->exportProducts();
$session->http->setRedirect($storage->getUrl($storage->getFiles->[0]));
return "redirect";
}
#-------------------------------------------------------------------
=head2 www_importProducts ( )
Import new product data from a file provided by the user. This will create new products
or alter existing products.
=cut
sub www_importProducts {
my $self = shift;
my $session = $self->session;
return $session->privilege->insufficient unless $self->canEdit;
my $i18n=WebGUI::International->new($session, 'Asset_Shelf');
my $status_message;
if ( $session->form->get('doit')) {
my $storage = WebGUI::Storage->create($session);
my $productFile = $storage->addFileFromFormPost('importFile', 1);
eval {
$self->importProducts($storage->getPath($productFile)) if $productFile;
};
my $exception;
if ($exception = Exception::Class->caught('WebGUI::Error::InvalidFile')) {
$status_message = sprintf 'A problem was found with your file: %s',
$exception->error;
if ($exception->brokenLine) {
$status_message .= sprintf ' on line %d', $exception->brokenLine;
}
}
elsif ($exception = Exception::Class->caught()) {
$status_message = sprintf 'A problem happened during the import: %s', $exception->error;
}
else {
$status_message = $i18n->get('import successful');
##Copy and paste from Asset.pm, www_editSave
if ($self->session->setting->get("autoRequestCommit")) {
# Make sure version tag hasn't already been committed by another process
my $versionTag = WebGUI::VersionTag->getWorking($self->session, "nocreate");
if ($versionTag && $self->session->setting->get("skipCommitComments")) {
$versionTag->requestCommit;
}
elsif ($versionTag) {
$self->session->http->setRedirect(
$self->getUrl("op=commitVersionTag;tagId=".WebGUI::VersionTag->getWorking($self->session)->getId)
);
return undef;
}
}
}
}
my $output;
if ($status_message) {
$output = '<div id="status_message">'.$status_message.'</div>';
}
$output .= WebGUI::Form::formHeader($session,{action => $self->getUrl})
. WebGUI::Form::hidden($session, {name=>"func", value=>"importProducts"})
. WebGUI::Form::hidden($session, {name=>"doit", value=>1})
. q{<input type="file" name="importFile" size="10" />}
. WebGUI::Form::submit($session,{value=>$i18n->get('import'), extras=>q{style="float: left;"} })
. WebGUI::Form::formFooter($session);
return $self->getAdminConsole->render($output, $i18n->get('import'));
}
1;

View file

@ -21,7 +21,6 @@ use WebGUI::Shop::AddressBook;
use WebGUI::Shop::Cart;
use WebGUI::Shop::Credit;
use WebGUI::Shop::Pay;
use WebGUI::Shop::Products;
use WebGUI::Shop::Ship;
use WebGUI::Shop::Tax;
use WebGUI::Shop::Transaction;
@ -190,28 +189,6 @@ sub www_pay {
#-------------------------------------------------------------------
=head2 www_products ()
Hand off to the tax system.
=cut
sub www_products {
my $session = shift;
my $output = undef;
my $method = "www_".$session->form->get("method");
my $products = WebGUI::Shop::Products->new($session);
if ($method ne "www_" && WebGUI::Shop::Products->can($method)) {
$output = $products->$method();
}
else {
WebGUI::Error::MethodNotFound->throw(error=>"Couldn't call non-existant method $method", method=>$method);
}
return $output;
}
#-------------------------------------------------------------------
=head2 www_ship ()
Hand off to the shipper.

View file

@ -33,13 +33,18 @@ The current WebGUI session object.
A URL to safely append to the end of the page URL.
=head3 $query
The post query (?) parameters you'd like to add to the URL.
=cut
#-------------------------------------------------------------------
sub process {
my $session = shift;
my $url = shift;
my $pageUrl = $session->url->page();
my $query = shift;
my $pageUrl = $session->url->page($query);
if ($url) {
my $uri = URI->new($pageUrl);
##Append the requested URL to the path part of the URL

View file

@ -1,357 +0,0 @@
package WebGUI::Shop::Products;
use strict;
use Class::InsideOut qw{ :std };
use WebGUI::Text;
use WebGUI::Storage;
use WebGUI::Exception::Shop;
use WebGUI::Shop::Admin;
use WebGUI::Asset::Sku::Product;
=head1 NAME
Package WebGUI::Shop::Products
=head1 DESCRIPTION
This package handles importing and exporting products into the Shop system, mainly
to be compatible with third-party systems, such as inventory control. If you want to
export your Products into another WebGUI site, please use the Asset Export system
instead.
=head1 METHODS
These subroutines are available from this package:
=cut
readonly session => my %session;
#-------------------------------------------------------------------
=head2 exportProducts ( )
Export all products from the WebGUI system in a CSV file. For details
about the file format, see importProducts.
Returns a temporary WebGUI::Storage object containing the file. The
file will be named siteProductData.csv.
=cut
sub exportProducts {
my $self = shift;
my $session = $self->session;
my @columns = qw{sku shortdescription price weight quantity};
my $productData = WebGUI::Text::joinCSV(qw{mastersku title}, @columns) . "\n";
@columns = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @columns;
my $getAProduct = WebGUI::Asset::Sku::Product->getIsa($session);
while (my $product = $getAProduct->()) {
my $mastersku = $product->get('sku');
my $title = $product->getTitle;
my $collateri = $product->getAllCollateral('variantsJSON');
foreach my $collateral (@{ $collateri }) {
my @productFields = @{ $collateral }{ @columns };
$productData .= WebGUI::Text::joinCSV($mastersku, $title, @productFields);
$productData .= "\n";
}
}
my $storage = WebGUI::Storage->createTemp($session);
$storage->addFileFromScalar('siteProductData.csv', $productData);
return $storage;
}
#-------------------------------------------------------------------
=head2 importProducts ( $filePath )
Import products into the WebGUI system. If the master sku of a product
exists in the system, it will be updated. If master skus do not exist,
they will be added.
The first line of the file should contain only the name of the columns,
in any order. It may not contain comments.
These are the column names, each is required:
=over 4
=item *
mastersku
=item *
sku
=item *
title
=item *
shortdescription
=item *
price
=item *
weight
=item *
quantity
=back
The following lines will contain product information. Blank
lines and anything following a '#' sign will be ignored from
the second line of the file, on to the end.
Returns 1 if the import has taken place. This is to help you know
if old data has been deleted and new has been inserted.
=cut
sub importProducts {
my $self = shift;
my $filePath = shift;
my $session = $self->session;
WebGUI::Error::InvalidParam->throw(error => q{Must provide the path to a file})
unless $filePath;
WebGUI::Error::InvalidFile->throw(error => qq{File could not be found}, brokenFile => $filePath)
unless -e $filePath;
WebGUI::Error::InvalidFile->throw(error => qq{File is not readable}, brokenFile => $filePath)
unless -r $filePath;
open my $table, '<', $filePath or
WebGUI::Error->throw(error => qq{Unable to open $filePath for reading: $!\n});
my $headers;
$headers = <$table>;
chomp $headers;
my @headers = WebGUI::Text::splitCSV($headers);
WebGUI::Error::InvalidFile->throw(error => qq{Bad header found in the CSV file}, brokenFile => $filePath)
unless (join(q{-}, sort @headers) eq 'mastersku-price-quantity-shortdescription-sku-title-weight')
and (scalar @headers == 7);
my @productData = ();
my $line = 1;
while (my $productRow = <$table>) {
chomp $productRow;
$productRow =~ s/\s*#.+$//;
next unless $productRow;
local $_;
my @productRow = WebGUI::Text::splitCSV($productRow);
WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line)
unless scalar @productRow == 7;
push @productData, [ @productRow ];
}
return unless scalar @productData;
##Okay, if we got this far, then the data looks fine.
my $fetchProductId = $session->db->prepare('select p.assetId from Product as p join sku as s on p.assetId=s.assetId and p.revisionDate=s.revisionDate where s.sku=? order by p.revisionDate DESC limit 1');
my $node = WebGUI::Asset::Sku::Product->getProductImportNode($session);
@headers = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @headers;
my @collateralFields = grep { $_ ne 'title' and $_ ne 'mastersku' } @headers;
PRODUCT: foreach my $productRow (@productData) {
my %productRow;
##Order the data according to the headers, in whatever order they exist.
@productRow{ @headers } = @{ $productRow };
##Isolate just the collateral from the other product information
my %productCollateral;
@productCollateral{ @collateralFields } = @productRow{ @collateralFields };
$fetchProductId->execute([$productRow{mastersku}]);
my $asset = $fetchProductId->hashRef;
##If the assetId exists, we update data for it
if ($asset->{assetId}) {
$session->log->warn("Modifying an existing product: $productRow{sku} = $asset->{assetId}\n");
my $assetId = $asset->{assetId};
my $product = WebGUI::Asset->newPending($session, $assetId);
##Error handling for locked assets
if ($product->isLocked) {
$session->log->warn("Product is locked");
next PRODUCT if $product->isLocked;
}
if ($productRow{title} ne $product->getTitle) {
$product->update({ title => $product->fixTitle($productRow{title}) });
}
my $collaterals = $product->getAllCollateral('variantsJSON');
my $collateralSet = 0;
ROW: foreach my $collateral (@{ $collaterals }) {
next ROW unless $collateral->{sku} eq $productRow{sku};
@{ $collateral}{ @collateralFields } = @productCollateral{ @collateralFields }; ##preserve the variant Id field, assign all others
$product->setCollateral('variantsJSON', 'variantId', $collateral->{variantId}, $collateral);
$collateralSet=1;
}
if (!$collateralSet) {
##It must be a new variant
$product->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral);
}
}
else {
##Insert a new product;
$session->log->warn("Making a new product: $productRow{sku}\n");
my $newProduct = $node->addChild({className => 'WebGUI::Asset::Sku::Product'});
$newProduct->update({
title => $newProduct->fixTitle($productRow{title}),
sku => $productRow{mastersku},
});
$newProduct->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral);
$newProduct->commit;
}
}
return 1;
}
#-------------------------------------------------------------------
=head2 new ( $session )
Constructor for the WebGUI::Shop::Products. Returns a WebGUI::Shop::Products object.
=cut
sub new {
my $class = shift;
my $session = shift;
my $self = {};
bless $self, $class;
register $self;
$session{ id $self } = $session;
return $self;
}
#-------------------------------------------------------------------
=head2 www_exportProducts ( )
Export all product SKUs as a CSV file. Returns a WebGUI::Storage
object containg the product file, named 'siteProductData.csv'.
=cut
sub www_exportProducts {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my $storage = $self->exportProducts();
$session->http->setRedirect($storage->getUrl($storage->getFiles->[0]));
return "redirect";
}
#-------------------------------------------------------------------
=head2 www_importProducts ( )
Import new product data from a file provided by the user. This will create new products
or alter existing products.
=cut
sub www_importProducts {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my $storage = WebGUI::Storage->create($session);
my $productFile = $storage->addFileFromFormPost('importFile', 1);
eval {
$self->importProducts($storage->getPath($productFile)) if $productFile;
};
my ($exception, $status_message);
if ($exception = Exception::Class->caught('WebGUI::Error::InvalidFile')) {
$status_message = sprintf 'A problem was found with your file: %s',
$exception->error;
if ($exception->brokenLine) {
$status_message .= sprintf ' on line %d', $exception->brokenLine;
}
}
elsif ($exception = Exception::Class->caught()) {
$status_message = sprintf 'A problem happened during the import: %s', $exception->error;
}
else {
my $i18n = WebGUI::International->new($session, 'Shop');
$status_message = $i18n->get('import successful');
##Copy and paste from Asset.pm, www_editSave
if ($self->session->setting->get("autoRequestCommit")) {
# Make sure version tag hasn't already been committed by another process
my $versionTag = WebGUI::VersionTag->getWorking($self->session, "nocreate");
if ($versionTag && $self->session->setting->get("skipCommitComments")) {
$versionTag->requestCommit;
}
elsif ($versionTag) {
$self->session->http->setRedirect(
$self->getUrl("op=commitVersionTag;tagId=".WebGUI::VersionTag->getWorking($self->session)->getId)
);
return undef;
}
}
}
return $self->www_manage($status_message);
}
#-------------------------------------------------------------------
=head2 www_manage ( $status_message )
User interface to synchronize product data. Provides an interface for
exporting all products on the site, and importing sets of products.
=head3 $status_message
An status message generated when import or export is called that needs to be
displayed back to the user.
=cut
sub www_manage {
my $self = shift;
my $status_message = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
##YUI specific datatable CSS
my ($style, $url) = $session->quick(qw(style url));
##Default CSS
$style->setRawHeadTags('<style type="text/css"> #paging a { color: #0000de; } #search, #export form { display: inline; } </style>');
my $i18n=WebGUI::International->new($session, 'Shop');
my $exportForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=products;method=exportProducts')})
. WebGUI::Form::submit($session,{value=>$i18n->get('export'), extras=>q{style="float: left;"} })
. WebGUI::Form::formFooter($session);
my $importForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=products;method=importProducts')})
. WebGUI::Form::submit($session,{value=>$i18n->get('import'), extras=>q{style="float: left;"} })
. q{<input type="file" name="importFile" size="10" />}
. WebGUI::Form::formFooter($session);
my $output;
if ($status_message) {
$output = sprintf <<EODIV, $status_message;
<div id="status_message">%s</div>
EODIV
}
$output .= sprintf <<EODIV, $exportForm, $importForm, $session->url->append(WebGUI::Asset::Sku::Product->getProductImportNode($session)->getUrl, 'op=assetManager'), $i18n->get('view products');
<div id="importExport">%s%s</div>
<p><a href="%s">%s</a></p>
EODIV
return $admin->getAdminConsole->render($output, $i18n->get('products'));
}
1;

View file

@ -3,6 +3,24 @@ package WebGUI::i18n::English::Asset_Shelf;
use strict;
our $I18N = {
'import' => {
message => q|Import Products|,
lastUpdated => 1212550974,
context => q|Label for bringing data into the Shop (Tax, Product, etc.)|
},
'export' => {
message => q|Export Products|,
lastUpdated => 1212550978,
context => q|Label for taking data out of the Shop (Tax, Product, etc.)|,
},
'import successful' => {
message => q|Your products have been imported.|,
lastUpdated => 1213047491,
context => q|Message telling the user the their products have been imported successfully.|
},
'price' => {
message => q|The price of this sku.|,
lastUpdated => 0,

View file

@ -927,30 +927,6 @@ our $I18N = {
context => q|Period name for a yearly subscription.|
},
'import' => {
message => q|Import|,
lastUpdated => 1212550974,
context => q|Label for bringing data into the Shop (Tax, Product, etc.)|
},
'export' => {
message => q|Export|,
lastUpdated => 1212550978,
context => q|Label for taking data out of the Shop (Tax, Product, etc.)|,
},
'import successful' => {
message => q|Your products have been imported.|,
lastUpdated => 1213047491,
context => q|Message telling the user the their products have been imported successfully.|
},
'view products' => {
message => q|View all imported products|,
lastUpdated => 1213047491,
context => q|Label for a shortcut to the import products folder|
},
'address1 help' => {
message => q|The first address line.|,
lastUpdated => 1213121298,