(I use terraform 0.12.20)
I have the following snippet in my code:
locals {
x = merge(values({
for location in local.locations: location => {
for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
location = location
apsvc_name = apsvc_name
}
}
})...)
The x local is then output. Applying the configuration results in:
Error: Invalid expanding argument value
on ..\..\hosting-modules\web\app_hosting.tf line 35, in locals:
35: x = merge(values({
36: for location in local.locations: location => {
37: for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
38: location = location
39: apsvc_name = apsvc_name
40: }
41: }
42: })...)
The expanding argument (indicated by ...) must be of a tuple, list, or set
type.
Now if I strip the call to merge(...) and assign the result of values to x, the result is output just fine:
x = [
{
"centralus-backoffice" = {
"apsvc_name" = "backoffice"
"location" = "centralus"
}
"centralus-gateway" = {
"apsvc_name" = "gateway"
"location" = "centralus"
}
"centralus-javascriptclient" = {
"apsvc_name" = "javascriptclient"
"location" = "centralus"
}
},
{
"eastus2-backoffice" = {
"apsvc_name" = "backoffice"
"location" = "eastus2"
}
"eastus2-gateway" = {
"apsvc_name" = "gateway"
"location" = "eastus2"
}
"eastus2-javascriptclient" = {
"apsvc_name" = "javascriptclient"
"location" = "eastus2"
}
},
]
And it is clearly a list, so it should qualify for ... just fine.
What am I missing?
EDIT 1
Replacing locations with a compile time list works:
locals {
locations = ["abc", "xyz"]
x = merge(values({
for location in local.locations: location => {
for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
location = location
apsvc_name = apsvc_name
}
}
})...)
}
Which results in:
Outputs:
x = {
"abc-backoffice" = {
"apsvc_name" = "backoffice"
"location" = "abc"
}
"abc-gateway" = {
"apsvc_name" = "gateway"
"location" = "abc"
}
"abc-javascriptclient" = {
"apsvc_name" = "javascriptclient"
"location" = "abc"
}
"xyz-backoffice" = {
"apsvc_name" = "backoffice"
"location" = "xyz"
}
"xyz-gateway" = {
"apsvc_name" = "gateway"
"location" = "xyz"
}
"xyz-javascriptclient" = {
"apsvc_name" = "javascriptclient"
"location" = "xyz"
}
}
But replace "abc" with a variable value and it stops working:
locals {
locations = [var.primary_location, "xyz"]
x = merge(values({
for location in local.locations: location => {
for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
location = location
apsvc_name = apsvc_name
}
}
})...)
}
And bang:
Error: Invalid expanding argument value
on ..\..\hosting-modules\web\app_hosting.tf line 5, in locals:
5: x = merge(values({
6: for location in local.locations: location => {
7: for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
8: location = location
9: apsvc_name = apsvc_name
10: }
11: }
12: })...)
|----------------
| local.locations is tuple with 2 elements
The expanding argument (indicated by ...) must be of a tuple, list, or set
type.
EDIT 2
So, the code that should work (I improved it - values is not needed):
merge([
for location in local.locations: {
for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
location = location
apsvc_name = apsvc_name
}
}
]...)
does not work for mysterious reasons. I ended up with the following workaround:
_y1 = [
for location in local.locations: {
for apsvc_name in var.apsvc_names: "${location}-${apsvc_name}" => {
location = location
apsvc_name = apsvc_name
}
}
]
_y2 = flatten([for m in local._y1: [for k,v in m: {
key = k
value = v
}]])
x = zipmap(local._y2.*.key, local._y2.*.value)
That one does work.
EDIT 3
https://github.com/hashicorp/terraform/issues/24033
Related
I'm building a profile page for users of my app (nextjs 12) and when I add authentication middleware it works fine locally but when deployed to the production server (apache shared hosting with nodejs v 14 installed) it throws error 500
my node version installed locally is v16.16.0 could that affect this issue?
any suggestions?
profile.tsx:
const ProfileEditor = () => {
const dispatch = useDispatch();
const {
profile,
profileLogin,
user
} = useSelector((state: RootState) => state.Auth);
useEffect(() => {
dispatch(FetchProfileAsync());
}, []);
const [preview, setPreview] = useState < string > ();
const handleFormSubmit = async(values: any, e: any) => {
dispatch(UpdateProfileAsync(values as any));
};
let initialValues = {
image: profile ? .image ? .full_path,
first_name: profile ? .first_name,
last_name: profile ? .last_name,
email: profile ? .email,
phone: profile ? .phone,
};
const FILE_SIZE = 160 * 1024;
const SUPPORTED_FORMATS = ['image/jpg', 'image/png'];
const checkoutSchema = yup.object().shape({
image: yup.mixed().required('A file is required'),
// .test('fileSize', 'File too large', (value) => value && value.size <= FILE_SIZE)
// .test('fileFormat', 'Unsupported Format', (value) => value && SUPPORTED_FORMATS.includes(value.type)),
first_name: yup.string().required('required'),
last_name: yup.string().required('required'),
email: yup.string().email('invalid email').required('required'),
phone: yup.string().required('required'),
});
return ( <
CustomerDashboardLayout >
<
DashboardPageHeader icon = {
Person
}
title = 'Edit Profile'
navigation = { < CustomerDashboardNavigation / >
}
/>
<
Card1 >
<
Formik enableReinitialize initialValues = {
initialValues
}
validationSchema = {
checkoutSchema
}
onSubmit = {
(values: any, e: any) => handleFormSubmit(values, e)
} >
{
({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue
}) => ( <
form onSubmit = {
handleSubmit
} >
<
FlexBox alignItems = 'flex-end'
mb = {
3
} >
<
Avatar src = {
preview ? ? user ? .image ? .full_path
}
sx = {
{
height: 64,
width: 64
}
}
alt = 'user image' / >
{ /* <img src={user?.image?.full_path} height={100} width={100} alt='fdss' /> */ }
<
Box ml = {-2.5
} >
<
label htmlFor = 'profile-image' >
<
Button component = 'span'
color = 'secondary'
sx = {
{
bgcolor: 'grey.300',
height: 'auto',
p: '8px',
borderRadius: '50%',
}
} >
<
CameraEnhance fontSize = 'small' / >
<
/Button> <
/label> <
/Box> <
Box display = 'none' >
<
input name = 'image'
onChange = {
(newFile: any) => {
setFieldValue('image', newFile.currentTarget.files[0]);
FileToBase64(newFile.currentTarget.files[0] as any).then((res) => setPreview(res));
}
}
id = 'profile-image'
accept = 'image/*'
type = 'file' /
>
<
/Box> <
/FlexBox>
<
Box mb = {
4
} >
<
Grid container spacing = {
3
} >
<
Grid item md = {
6
}
xs = {
12
} >
<
ETextField mb = {
1.5
}
name = 'first_name'
label = 'First Name'
placeholder = 'First Name'
variant = 'outlined'
size = 'small'
fullWidth onBlur = {
handleBlur
}
onChange = {
handleChange
}
value = {
values.first_name || ''
}
error = {!!touched.first_name && !!errors.first_name
}
helperText = {
touched.first_name && errors.first_name
}
/> <
/Grid> <
Grid item md = {
6
}
xs = {
12
} >
<
ETextField mb = {
1.5
}
name = 'last_name'
label = 'Last Name'
placeholder = 'Last Name'
variant = 'outlined'
size = 'small'
fullWidth onBlur = {
handleBlur
}
onChange = {
handleChange
}
value = {
values.last_name || ''
}
error = {!!touched.last_name && !!errors.last_name
}
helperText = {
touched.last_name && errors.last_name
}
/> <
/Grid> <
Grid item md = {
6
}
xs = {
12
} >
<
ETextField mb = {
1.5
}
name = 'email'
label = 'Email'
placeholder = 'exmple#mail.com'
variant = 'outlined'
size = 'small'
type = 'email'
fullWidth onBlur = {
handleBlur
}
onChange = {
handleChange
}
value = {
values.email || ''
}
error = {!!touched.email && !!errors.email
}
helperText = {
touched.email && errors.email
}
/> <
/Grid> <
Grid item md = {
6
}
xs = {
12
} >
<
ETextField mb = {
1.5
}
name = 'phone'
label = 'Phone'
placeholder = '09********'
variant = 'outlined'
size = 'small'
fullWidth onBlur = {
handleBlur
}
onChange = {
handleChange
}
value = {
values.phone || ''
}
error = {!!touched.phone && !!errors.phone
}
helperText = {
touched.phone && errors.phone
}
/> <
/Grid> <
/Grid> <
/Box>
<
ELoadingButton type = 'submit'
variant = 'contained'
loading = {
profileLogin === 'loading'
}
buttonText = 'Save Changes'
loadingPosition = 'start'
fullWidth = {
true
}
color = 'primary' /
>
<
/form>
)
} <
/Formik> <
/Card1> <
/CustomerDashboardLayout>
);
};
export default ProfileEditor;
middlware.tsx
// eslint-disable-next-line #next/next/no-server-import-in-page
import { NextResponse } from 'next/server';
// eslint-disable-next-line #next/next/no-server-import-in-page
import type { NextRequest } from 'next/server';
import { KEY_TOKEN_COOKIE } from 'src/constants';
const middleware = (request: NextRequest, response: NextResponse) => {
const token = request?.cookies[KEY_TOKEN_COOKIE];
if (!token) return NextResponse.rewrite(new URL('/error/auth', request.url));
};
export default middleware;
I have one complicated object in terraform, the structure as below
what I need to do is transform this structure into another one(I have posted the desired structure) then create the resource with foreach in terraform
StorageSettings{
storage{
fileshare{
directories{}
}
}
}
actual code here
locals {
StorageSettings = {
CoreBackup = {
fileshare = {
shareA = {
name = "myshare"
storage_account_name = "sa1corebackup"
quota = 100
directories = {
dirA = {
name = "app"
share_name = "myshare"
storage_account_name = "sa1corebackup"
}
}
}
shareB = {
name = "myshare2"
storage_account_name = "sa1corebackup"
quota = 150
}
}
}
MGMTackup = {
fileshare = {
ShareA = {
name = "myshare"
storage_account_name = "mgmtbackup"
quota = 200
enabled_protocol = "SMB"
acl = {
id = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI"
access_policy = {
permissions = "rwdl"
start = "2019-07-02T09:38:21.0000000Z"
expiry = "2019-07-02T10:38:21.0000000Z"
}
}
directories = {
dirA = {
name = "app"
share_name = "myshare"
storage_account_name = "mgmtbackup"
}
dirB = {
name = "backup"
share_name = "myshare"
storage_account_name = "mgmtbackup"
}
}
}
ShareB = {
name = "myshare2"
storage_account_name = "mgmtbackup"
quota = 100
enabled_protocol = "SMB"
acl = {
id = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI"
access_policy = {
permissions = "rwdl"
start = "2019-07-02T09:38:21.0000000Z"
expiry = "2019-07-02T10:38:21.0000000Z"
}
}
directories = {
dirA = {
name = "app"
share_name = "myshare"
storage_account_name = "mgmtbackup"
}
dirB = {
name = "backup"
share_name = "myshare"
storage_account_name = "mgmtbackup"
}
}
}
}
}
}
}
My question is how to transform the structure to the below one?
storage+fileshare{
directories{
}
}
I tried the below code
filesharesettings = { for p, v in local.StorageSettings : p => v.fileshare if try(v.fileshare, null) != null }
directorysettings = {
for Storage, FileShareSettings in local.filesharesettings : Storage =>
{ for FileShare, FileShareSetting in FileShareSettings : "${Storage} ${FileShare}" => FileShareSetting.directories if lookup(FileShareSetting, "directories", "") != "" }
}
but only get the result like this
storage{
storage+fileshare{
directories{
}
}
}
current output as below,
please note it's not my expect output, I tried to convert the structure with some code(already posted), and below is the output, my expect output structure is this
storage+fileshare{
directories{
}
}
Changes to Outputs:
+ dd = {
+ CoreBackup = {
+ CoreBackup shareA = {
+ dirA = {
+ name = "app"
+ share_name = "myshare"
+ storage_account_name = "sa1corebackup"
}
}
}
+ MGMTackup = {
+ MGMTackup ShareA = {
+ dirA = {
+ name = "app"
+ share_name = "myshare"
+ storage_account_name = "mgmtbackup"
}
+ dirB = {
+ name = "backup"
+ share_name = "myshare"
+ storage_account_name = "mgmtbackup"
}
}
+ MGMTackup ShareB = {
+ dirA = {
+ name = "app"
+ share_name = "myshare"
+ storage_account_name = "mgmtbackup"
}
+ dirB = {
+ name = "backup"
+ share_name = "myshare"
+ storage_account_name = "mgmtbackup"
}
}
}
}
Could anyone kindly help me out here?
Your data source is rather complex, but I think you can do that using three for loops:
directorysettings = merge(
flatten([for storage_name, fileshares in local.StorageSettings:
{for share_name, share in fileshares["fileshare"]:
"${storage_name}+${share_name}" => {
for directory_name, directory in share["directories"]:
directory_name => directory
} if lookup(share, "directories", "") != ""
}
])...)
which gives directorysettings:
{
"CoreBackup+shareA" = {
"dirA" = {
"name" = "app"
"share_name" = "myshare"
"storage_account_name" = "sa1corebackup"
}
}
"MGMTackup+ShareA" = {
"dirA" = {
"name" = "app"
"share_name" = "myshare"
"storage_account_name" = "mgmtbackup"
}
"dirB" = {
"name" = "backup"
"share_name" = "myshare"
"storage_account_name" = "mgmtbackup"
}
}
"MGMTackup+ShareB" = {
"dirA" = {
"name" = "app"
"share_name" = "myshare"
"storage_account_name" = "mgmtbackup"
}
"dirB" = {
"name" = "backup"
"share_name" = "myshare"
"storage_account_name" = "mgmtbackup"
}
}
}
I have a key array of objects which user have for days and their timings for their shop i.e
user A -
{
"name" : "shop1",
"timings" : [
{
"Monday" : "10am to 7pm"
},
{
"Thursday" : "Closed"
},
{
"Friday" : "9am to 6pm"
},
{
"Sunday" : "Closed"
},
{
"Wednesday" : "10am to 7pm"
},
{
"Tuesday" : "10am to 7pm"
},
{
"Saturday" : "10am to 7pm"
}
]}
Now I need to show these timing by days in frontend i.e
Monday-Saturday:9am to 7pm, Thursday,Sunday: Closed
So far I've tried to loop them and searched all days which are closed i.e
function filterByValue(array, string) {
return array.filter(o =>
Object.keys(o).some(k => o[k].toLowerCase().includes(string.toLowerCase())));
}
let closedDays = filterByValue(element.timings, 'Closed')
console.log("whtt",closedDays); // [{name: 'Lea', country: 'Italy'}]
let res = closedDays.map(x => Object.keys(x)[0]);
but for open days I can't figure out any suitable solution which can be used here. It would be a great help if you suggest something
const timings = [{"Monday": "10am to 7pm"}, {"Thursday": "Closed"}, {"Friday": "9am to 6pm"}, {"Sunday": "Closed"}, {"Wednesday": "10am to 7pm"}, {"Tuesday": "10am to 7pm"}, {"Saturday": "10am to 7pm"}];
const weekDaysInOrder = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
//Step1 little change list of objects
const newTimings = changeTimingsStructure(timings);
let obj = {}
sortTimings(newTimings).forEach(el => {
if (el.timing in obj) {
const maxIndex = Math.max(...obj[el.timing].map(el => el.indexOfDay));
if (maxIndex + 1 < el.indexOfDay && el.timing !== 'Closed') {
obj = pushToObjectArr(el.timing + '-v2', el, obj);
} else {
obj = pushToObjectArr(el.timing, el, obj);
}
} else {
obj = pushToObjectArr(el.timing, el, obj);
}
})
displayTimings(obj);
function changeTimingsStructure(timings) {
const newTimings = [];
timings.forEach(day => {
const dayName = Object.keys(day)[0]
const timing = day[dayName];
newTimings.push({
day: dayName,
timing,
indexOfDay: weekDaysInOrder.findIndex(dayInWeek => dayInWeek === dayName)
})
})
return newTimings;
}
function sortTimings(timings) {
return timings.sort(((a, b) => a.indexOfDay - b.indexOfDay));
}
function pushToObjectArr(key, el, obj) {
if (key in obj) {
obj[key].push(el);
} else {
obj[key] = [el]
}
return obj
}
function displayTimings(obj) {
let closedText = '';
for (const groupOfDays in obj) {
if (groupOfDays !== 'Closed') {
if (obj[groupOfDays].length > 1) {
console.log(`${obj[groupOfDays][0].day} - ${obj[groupOfDays][obj[groupOfDays].length - 1].day}: ${groupOfDays}`);
} else {
console.log(`${obj[groupOfDays][0].day}: ${obj[groupOfDays][0].timing}`);
}
} else {
closedText = `${obj[groupOfDays].map(dayObj => dayObj.day).join(', ')}: Closed`;
}
}
console.log(closedText);
}
Try this, final output:
Here I have created this utility in order to format the input as per your expected output.
let data = {name:'shop1',timings:[{Monday:'10am to 7pm'},{Thursday:'Closed'},{Friday:'9am to 6pm'},{Sunday:'Closed'},{Wednesday:'10am to 7pm'},{Tuesday:'10am to 7pm'},{Saturday:'10am to 7pm'}]};
const formatData = data => {
return data.reduce(
(result, d) => {
const value = Object.values(d)[0];
const key = Object.keys(d)[0];
if (value === 'Closed') {
result.closed.days.push(key);
if (!result.closed.values) {
result.closed.value = value;
}
} else {
result.open.days.push(key);
result.open.timings.push(value);
}
return result;
},
{
closed: { days: [], value: '' },
open: { days: [], timings: [] },
}
);
};
const formattedData = formatData(data.timings);
const formatTimings = timings => {
const formattedTimings= timings.reduce((result, time) => {
const times = time
.split('to')
.map(t => parseInt(t.replace(/\D+/g, '')))
.filter(Boolean);
result.from.push(times[0]);
result.to.push(times[1]);
return result
}, {from :[], to: []});
return `${Math.min(...formattedTimings.from)}am to ${Math.max(...formattedTimings.to)}pm`
};
const openDaysTimings = formatTimings(formattedData.open.timings);
const displayTimings = (days, time) => {
return `${days.join(", ")}: ${time}`
}
console.log(displayTimings(formattedData.closed.days, formattedData.closed.value));
console.log(displayTimings(formattedData.open.days, openDaysTimings));
Invalid shipping method in magento 1.8.1?
When my cart value in 950 - 999 INR but below 950 or above 999 working fine?
One page controller code is below.
Kindly help me
onePageController.php
<?php
require_once Mage::getModuleDir('controllers', 'Mage_Checkout').DS.'OnepageController.php';
class Webly_QuickCheckout_OnepageController extends Mage_Checkout_OnepageController
{
public function saveBillingAction()
{
if ($this->_expireAjax()) {
return;
}
if ($this->getRequest()->isPost()) {
// $postData = $this->getRequest()->getPost('billing', array());
// $data = $this->_filterPostData($postData);
$data = $this->getRequest()->getPost('billing', array());
$customerAddressId = $this->getRequest()->getPost('billing_address_id', false);
if (isset($data['email'])) {
$data['email'] = trim($data['email']);
}
$data['use_for_shipping'] = 1;
$result = $this->getOnepage()->saveBilling($data, $customerAddressId);
if (!isset($result['error'])) {
if (!isset($result['error'])) {
if ($this->getOnepage()->getQuote()->isVirtual()) {
$result['goto_section'] = 'payment';
$result['update_section'] = array(
'name' => 'payment-method',
'html' => $this->_getPaymentMethodsHtml()
);
} elseif (isset($data['use_for_shipping']) && $data['use_for_shipping'] == 1) {
$quote =$this->getOnepage()->getQuote();
$quote->getBaseGrandTotal();
$result = $this->getOnepage()->saveShipping($data, $customerAddressId);
if ($quote->getBaseGrandTotal() < 1000) {
$method = 'inchoo_shipping_fixed';
} else {
$method = 'inchoo_shipping_free';
}
// $method = 'flatrate_flatrate';
$result = $this->getOnepage()->saveShippingMethod($method);
Mage::getSingleton('checkout/type_onepage')->getQuote()->getShippingAddress()-> setShippingMethod($method)->save();
$result['update_section'] = array(
'name' => 'payment-method',
'html' => $this->_getPaymentMethodsHtml()
);
$result['goto_section'] = 'payment';
} else {
$result['goto_section'] = 'shipping';
}
}
}
$this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
}
}
}
Can anyone help me in the implementation of nested set behavior https://github.com/creocoder/yii2-nested-sets with Yii2 Menu Widget?
Basically I want the hierarchical sidebar navigation menu like this http://www.surtex-instruments.co.uk/surgical/general-surgical-instruments
Thanks.
In your Model class define this functions:
public static function getTree($selected, $search)
{
$categories = Categories::find()->orderBy('lft')->asArray()->all();
$tree = [];
$index = 0;
foreach ($categories as $category) {
if ($category['parent_category_id'] === NULL) {
$tree[$index]['text'] = $category['category_' . Yii::$app->language];
if ($search) {
$tree[$index]['href'] = Url::to(['products', 'category' => $category['category_id'], 'search' => $search, 'string' => $category['category_' . Yii::$app->language]]);
} else {
$tree[$index]['href'] = Url::to(['products', 'category' => $category['category_id'], 'string' => $category['category_' . Yii::$app->language] ]);
}
$tree[$index]['id'] = $category['category_id'];
if ($selected) {
if ($selected['category_id'] == $category['category_id']) {
$tree[$index]['state']['selected'] = true;
}
if ($selected['lft'] >= $category['lft'] && $selected['rgt'] <= $category['rgt']) {
$tree[$index]['state']['expanded'] = true;
}
}
if ($category['lft'] + 1 != $category['rgt']) {
Categories::getNodes($tree[$index], $categories, $selected, $search);
}
$index++;
}
}
return $tree;
}
private static function getNodes(&$tree, $categories, $selected, $search)
{
$index = 0;
foreach ($categories as $category) {
if ($tree['id'] == $category['parent_category_id']) {
$tree['nodes'][$index]['text'] = $category['category_' . Yii::$app->language];
if ($search) {
$tree['nodes'][$index]['href'] = Url::to(['products', 'category' => $category['category_id'], 'search' => $search, 'string' => $category['category_' . Yii::$app->language]]);
} else {
$tree['nodes'][$index]['href'] = Url::to(['products', 'category' => $category['category_id'], 'string' => $category['category_' . Yii::$app->language]]);
}
$tree['nodes'][$index]['id'] = $category['category_id'];
if ($selected) {
if ($selected['category_id'] == $category['category_id']) {
$tree['nodes'][$index]['state']['selected'] = true;
}
if ($selected['lft'] >= $category['lft'] && $selected['rgt'] <= $category['rgt']) {
$tree['nodes'][$index]['state']['expanded'] = true;
}
}
if ($category['lft'] + 1 != $category['rgt']) {
Categories::getNodes($tree['nodes'][$index], $categories, $selected, $search);
}
$index++;
}
}
}
and use this extension execut/yii2-widget-bootstraptreeview
In the controller file get the menu like this:
public function actionProducts($category = false)
{
...
$data = Categories::getTree($category,'');
In your view file
<?php
...
use execut\widget\TreeView;
...
$onSelect = new JsExpression(<<<JS
function (undefined, item) {
window.location.href = item.href;
}
JS
);
?>
<?= TreeView::widget([
'data' => $data,
'template' => TreeView::TEMPLATE_SIMPLE,
'clientOptions' => [
'onNodeSelected' => $onSelect,
'onhoverColor' => "#fff",
'enableLinks' => true,
'borderColor' => '#fff',
'collapseIcon' => 'fa fa-angle-down',
'expandIcon' => 'fa fa-angle-right',
'levels' => 1,
'selectedBackColor' => '#fff',
'selectedColor' => '#2eaadc',
],
]); ?>