mirror of
https://github.com/openclassify/openclassify.git
synced 2026-03-09 17:55:27 -05:00
Merge branch 'master' of https://github.com/openclassify/openclassify into muammertop
This commit is contained in:
commit
7c794633f2
@ -14,8 +14,9 @@ return [
|
||||
'google_statistic_code',
|
||||
'ogImage',
|
||||
'free_currencyconverterapi_key',
|
||||
'hide_price_categories',
|
||||
'tcmb_exchange_url',
|
||||
'enabled_currencies',
|
||||
'tcmb_exchange_url'
|
||||
],
|
||||
],
|
||||
'ads' => [
|
||||
|
||||
@ -94,6 +94,14 @@ return [
|
||||
"default_value" => "1eea72940f3868c77420"
|
||||
]
|
||||
],
|
||||
'hide_price_categories' => [
|
||||
'type' => 'anomaly.field_type.checkboxes',
|
||||
'config' => [
|
||||
'options' => function (\Visiosoft\CatsModule\Category\Contract\CategoryRepositoryInterface $categoryRepository) {
|
||||
return $categoryRepository->mainCats()->pluck('name', 'id')->all();
|
||||
},
|
||||
],
|
||||
],
|
||||
'default_GET' => [
|
||||
'type' => 'anomaly.field_type.boolean',
|
||||
'bind' => 'adv.default_GET',
|
||||
|
||||
@ -61,6 +61,10 @@ return [
|
||||
'default_country' => [
|
||||
'name' => 'Default Ad Country',
|
||||
],
|
||||
'hide_price_categories' => [
|
||||
'name' => 'Hide Price On Categories',
|
||||
'instructions' => 'The price will be hidden when you create an ad or view an ad under these categories.'
|
||||
],
|
||||
'free_currencyconverterapi_key' => [
|
||||
'name' => 'Currency Converter API Key'
|
||||
],
|
||||
|
||||
@ -89,7 +89,6 @@
|
||||
|
||||
{{ addBlock('ads-list/row-bottom')|raw }}
|
||||
|
||||
{# <div class="category-tabs"></div>#}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -90,8 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row form-group select-price
|
||||
{{ setting_value('visiosoft.module.advs::price_area_hidden') ? 'hidden' }}">
|
||||
<div class="row form-group select-price{{ hidePrice ? ' hidden' }}">
|
||||
|
||||
<label class="col-sm-2 col-xs-12">
|
||||
{{ form.fields.price.label|raw }}
|
||||
|
||||
@ -67,7 +67,7 @@ class AdvRepository extends EntryRepository implements AdvRepositoryInterface
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!setting_value('visiosoft.module.location::hide_location_filter') and isset($param['country'])) {
|
||||
if (!setting_value('visiosoft.module.location::hide_location_filter')) {
|
||||
$country = !empty($param['country']) ? $param['country'] : setting_value('visiosoft.module.location::default_country');
|
||||
if ($country) {
|
||||
$query = $query->where('country_id', $country);
|
||||
|
||||
@ -255,6 +255,13 @@ class AdvsController extends PublicController
|
||||
foreach ($advs as $index => $ad) {
|
||||
$advs[$index]->detail_url = $this->adv_model->getAdvDetailLinkByModel($ad, 'list');
|
||||
$advs[$index] = $this->adv_model->AddAdsDefaultCoverImage($ad);
|
||||
|
||||
$foreign_currencies = json_decode($advs[$index]->foreign_currencies, true);
|
||||
if (isset($_COOKIE['currency']) && $advs[$index]->foreign_currencies && array_key_exists($_COOKIE['currency'], $foreign_currencies)) {
|
||||
$advs[$index]->currency = $_COOKIE['currency'];
|
||||
$advs[$index]->price = $foreign_currencies[$_COOKIE['currency']];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($isActiveCustomFields) {
|
||||
@ -483,9 +490,9 @@ class AdvsController extends PublicController
|
||||
$adv = $this->adv_repository->getListItemAdv($id);
|
||||
}
|
||||
|
||||
if ($adv && ((!$adv->expired() && $adv->getStatus() === 'approved') || $adv->created_by_id === \auth()->id())) {
|
||||
if ((auth()->user() and auth()->user()->hasRole('admin')) or ($adv && ((!$adv->expired() && $adv->getStatus() === 'approved') || $adv->created_by_id === \auth()->id()))) {
|
||||
// Check if created by exists
|
||||
if (!$adv->created_by) {
|
||||
if ((auth()->user() and !auth()->user()->hasRole('admin')) and !$adv->created_by) {
|
||||
$this->messages->error('visiosoft.module.advs::message.this_ad_is_not_valid_anymore');
|
||||
return $this->redirect->route('visiosoft.module.advs::list');
|
||||
}
|
||||
@ -583,10 +590,22 @@ class AdvsController extends PublicController
|
||||
|
||||
$configurations = $this->optionConfigurationRepository->getConf($adv->id);
|
||||
|
||||
$foreign_currencies = json_decode($adv->foreign_currencies, true);
|
||||
|
||||
if (isset($_COOKIE['currency']) && $adv->foreign_currencies && array_key_exists($_COOKIE['currency'], $foreign_currencies)) {
|
||||
$adv->currency = $_COOKIE['currency'];
|
||||
$adv->price = $foreign_currencies[$_COOKIE['currency']];
|
||||
}
|
||||
|
||||
// Check if hide price
|
||||
$hidePrice = false;
|
||||
if ($hidePriceCats = setting_value('visiosoft.module.advs::hide_price_categories')) {
|
||||
$hidePrice = in_array($adv['cat1'], $hidePriceCats);
|
||||
}
|
||||
|
||||
if ($adv->created_by_id == isset(auth()->user()->id) or $adv->status == "approved") {
|
||||
return $this->view->make('visiosoft.module.advs::ad-detail/detail', compact('adv', 'complaints',
|
||||
'recommended_advs', 'categories', 'features', 'options', 'configurations'));
|
||||
'recommended_advs', 'categories', 'features', 'options', 'configurations', 'hidePrice'));
|
||||
} else {
|
||||
return back();
|
||||
}
|
||||
@ -943,9 +962,17 @@ class AdvsController extends PublicController
|
||||
->edit($adv, $categories, $cats);
|
||||
}
|
||||
|
||||
// Check if hide price
|
||||
$hidePrice = false;
|
||||
if (setting_value('visiosoft.module.advs::price_area_hidden')) {
|
||||
$hidePrice = true;
|
||||
} elseif ($hidePriceCats = setting_value('visiosoft.module.advs::hide_price_categories')) {
|
||||
$hidePrice = in_array($adv['cat1'], $hidePriceCats);
|
||||
}
|
||||
|
||||
return $this->view->make(
|
||||
'visiosoft.module.advs::new-ad/new-create',
|
||||
compact('id', 'cats_d', 'cats', 'Cloudinary', 'adv', 'custom_fields', 'options')
|
||||
compact('id', 'cats_d', 'cats', 'Cloudinary', 'adv', 'custom_fields', 'options', 'hidePrice')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -916,7 +916,7 @@
|
||||
.iti__flag {
|
||||
height: 15px;
|
||||
box-shadow: 0px 0px 1px 0px #888;
|
||||
background-image: url('{{ img("visiosoft.theme.base::images/flags.png").url }}');
|
||||
background-image: url('{{ img("visiosoft.theme.base::images/flags.png").path }}');
|
||||
background-repeat: no-repeat;
|
||||
background-color: #DBDBDB;
|
||||
background-position: 20px 0; }
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?php namespace Visiosoft\DefaultadminTheme\Listener;
|
||||
|
||||
use Anomaly\Streams\Platform\Entry\EntryModel;
|
||||
use Anomaly\Streams\Platform\Ui\Table\Component\Header\Header;
|
||||
use Illuminate\Support\Collection;
|
||||
use Anomaly\Streams\Platform\Ui\Table\Component\Filter\Type\SearchFilter;
|
||||
@ -53,7 +54,11 @@ class AddGsmFilter
|
||||
$builder->setColumns([
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'email' => [
|
||||
'value' => function (EntryModel $entry) {
|
||||
return str_ends_with($entry->email, '@example.com') ? '' : $entry->email;
|
||||
}
|
||||
],
|
||||
'gsm_phone',
|
||||
'created_at' => [
|
||||
'value' => 'entry.created_at'
|
||||
|
||||
@ -484,4 +484,14 @@ return [
|
||||
'education_part' => [
|
||||
'name' => 'State of Education'
|
||||
],
|
||||
|
||||
'notified_new_updates' => [
|
||||
'name' => 'Notified New Updates',
|
||||
],
|
||||
'notified_about_ads' => [
|
||||
'name' => 'Notified About Ads',
|
||||
],
|
||||
'receive_messages_email' => [
|
||||
'name' => 'Receive Messages Email',
|
||||
],
|
||||
];
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
return [
|
||||
'file' => [
|
||||
'name' => 'foto\\'s'
|
||||
'name' => 'foto\'s'
|
||||
],
|
||||
'first_name' => [
|
||||
'name' => 'Voornaam'
|
||||
|
||||
@ -18,9 +18,10 @@ class UsersController extends AdminController
|
||||
$term = request()->term;
|
||||
if ($term) {
|
||||
return $userRepository->newQuery()
|
||||
->select(DB::raw("CONCAT_WS('', first_name, ' ', last_name, ' (', gsm_phone, ' || ', email, ')') AS name"), 'id')
|
||||
->select(DB::raw("CONCAT_WS('', first_name, ' ', last_name, ' (', gsm_phone, ' || ', display_name, ' || ', email, ')') AS name"), 'id')
|
||||
->where('first_name', 'LIKE', "%$term%")
|
||||
->orWhere('last_name', 'LIKE', "%$term%")
|
||||
->orWhere('display_name', 'LIKE', "%$term%")
|
||||
->orWhere('gsm_phone', 'LIKE', "%$term%")
|
||||
->limit(setting_value('visiosoft.module.advs::user_filter_limit'))
|
||||
->pluck('name', 'id');
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
<?php namespace Visiosoft\ProfileModule\Http\Middleware;
|
||||
|
||||
use Anomaly\FilesModule\File\Command\GetFile;
|
||||
use Anomaly\Streams\Platform\View\ViewTemplate;
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OGImage
|
||||
{
|
||||
use DispatchesJobs;
|
||||
|
||||
private $template;
|
||||
|
||||
public function __construct(ViewTemplate $template)
|
||||
{
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (($ogImage = session()->get('ogImage')) && ($file = $this->dispatch(new GetFile($ogImage)))) {
|
||||
$this->template->set(
|
||||
'og_image',
|
||||
$file->make()->url()
|
||||
);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace Visiosoft\ProfileModule\Http\Middleware;
|
||||
<?php namespace Visiosoft\ProfileModule\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
@ -8,7 +6,6 @@ use Illuminate\Http\Request;
|
||||
|
||||
class authCheck
|
||||
{
|
||||
|
||||
private $auth;
|
||||
private $request;
|
||||
|
||||
@ -18,15 +15,12 @@ class authCheck
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Guard $auth
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($this->auth->check()) {
|
||||
return redirect($this->request->get('redirect', '/'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ use Visiosoft\ProfileModule\Education\Contract\EducationRepositoryInterface;
|
||||
use Visiosoft\ProfileModule\Education\EducationModel;
|
||||
use Visiosoft\ProfileModule\Education\EducationRepository;
|
||||
use Visiosoft\ProfileModule\Http\Middleware\authCheck;
|
||||
use Visiosoft\ProfileModule\Http\Middleware\OGImage;
|
||||
use Visiosoft\ProfileModule\Profile\Password\ForgotPassFormBuilder;
|
||||
use Visiosoft\ProfileModule\Profile\Password\PasswordFormBuilder;
|
||||
use Visiosoft\ProfileModule\Profile\Profile\ProfileFormBuilder;
|
||||
@ -68,6 +69,7 @@ class ProfileModuleServiceProvider extends AddonServiceProvider
|
||||
'profile/class/extendTime/{id},{type}' => 'Visiosoft\ProfileModule\Http\Controller\MyProfileController@extendAds',
|
||||
'profile/message/show/{id}' => 'Visiosoft\ProfileModule\Http\Controller\MyProfileController@showMessage',
|
||||
'profile/close-account' => [
|
||||
'middleware' => 'auth',
|
||||
'as' => 'visiosoft.module.profile::profile_close_account',
|
||||
'uses' => 'Visiosoft\ProfileModule\Http\Controller\MyProfileController@disableAccount'
|
||||
],
|
||||
@ -83,7 +85,8 @@ class ProfileModuleServiceProvider extends AddonServiceProvider
|
||||
// RegisterController
|
||||
'register' => [
|
||||
'middleware' => [
|
||||
authCheck::class
|
||||
authCheck::class,
|
||||
OGImage::class
|
||||
],
|
||||
'ttl' => 0,
|
||||
'uses' => 'Anomaly\UsersModule\Http\Controller\RegisterController@register',
|
||||
|
||||
@ -20,7 +20,7 @@ class UsersFieldsSeeder extends Seeder
|
||||
|
||||
$customFields = [
|
||||
[
|
||||
'name' => 'File',
|
||||
'name' => 'visiosoft.module.profile::field.file.name',
|
||||
'slug' => 'file',
|
||||
'type' => 'visiosoft.field_type.singlefile',
|
||||
'config' => [
|
||||
@ -29,7 +29,7 @@ class UsersFieldsSeeder extends Seeder
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'Country',
|
||||
'name' => 'visiosoft.module.profile::field.country.name',
|
||||
'slug' => 'country',
|
||||
'type' => 'anomaly.field_type.relationship',
|
||||
'config' => [
|
||||
@ -38,42 +38,42 @@ class UsersFieldsSeeder extends Seeder
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'City',
|
||||
'name' => 'visiosoft.module.profile::field.city.name',
|
||||
'slug' => 'city',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
],
|
||||
[
|
||||
'name' => 'District',
|
||||
'name' => 'visiosoft.module.profile::field.district.name',
|
||||
'slug' => 'district',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
],
|
||||
[
|
||||
'name' => 'Neighborhood',
|
||||
'name' => 'visiosoft.module.profile::field.neighborhood.name',
|
||||
'slug' => 'neighborhood',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
],
|
||||
[
|
||||
'name' => 'Village',
|
||||
'name' => 'visiosoft.module.profile::field.village.name',
|
||||
'slug' => 'village',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
],
|
||||
[
|
||||
'name' => 'Gsm Phone',
|
||||
'name' => 'visiosoft.module.profile::field.gsm_phone.name',
|
||||
'slug' => 'gsm_phone',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => 'Land Phone',
|
||||
'name' => 'visiosoft.module.profile::field.land_phone.name',
|
||||
'slug' => 'land_phone',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => 'Office Phone',
|
||||
'name' => 'visiosoft.module.profile::field.office_phone.name',
|
||||
'slug' => 'office_phone',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => 'Register Type',
|
||||
'name' => 'visiosoft.module.profile::field.register_type.name',
|
||||
'slug' => 'register_type',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
"config" => [
|
||||
@ -81,12 +81,12 @@ class UsersFieldsSeeder extends Seeder
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'Identification Number',
|
||||
'name' => 'visiosoft.module.profile::field.identification_number.name',
|
||||
'slug' => 'identification_number',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => 'Notified New Updates',
|
||||
'name' => 'visiosoft.module.profile::field.notified_new_updates.name',
|
||||
'slug' => 'notified_new_updates',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
'config' => [
|
||||
@ -96,7 +96,7 @@ class UsersFieldsSeeder extends Seeder
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'Notified About Ads',
|
||||
'name' => 'visiosoft.module.profile::field.notified_about_ads.name',
|
||||
'slug' => 'notified_about_ads',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
'config' => [
|
||||
@ -106,7 +106,7 @@ class UsersFieldsSeeder extends Seeder
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'Receive Messages Email',
|
||||
'name' => 'visiosoft.module.profile::field.receive_messages_email.name',
|
||||
'slug' => 'receive_messages_email',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
'config' => [
|
||||
@ -116,7 +116,7 @@ class UsersFieldsSeeder extends Seeder
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.birthday.name'),
|
||||
'name' => 'visiosoft.module.profile::field.birthday.name',
|
||||
'slug' => 'birthday',
|
||||
'type' => 'anomaly.field_type.datetime',
|
||||
'config' => [
|
||||
@ -125,32 +125,32 @@ class UsersFieldsSeeder extends Seeder
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.education.name'),
|
||||
'name' => 'visiosoft.module.profile::field.education.name',
|
||||
'slug' => 'education',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.state_of_education.name'),
|
||||
'name' => 'visiosoft.module.profile::field.state_of_education.name',
|
||||
'slug' => 'state_of_education',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.profession.name'),
|
||||
'name' => 'visiosoft.module.profile::field.profession.name',
|
||||
'slug' => 'profession',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.education_part.name'),
|
||||
'name' => 'visiosoft.module.profile::field.education_part.name',
|
||||
'slug' => 'education_part',
|
||||
'type' => 'anomaly.field_type.select',
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.facebook_address.name'),
|
||||
'name' => 'visiosoft.module.profile::field.facebook_address.name',
|
||||
'slug' => 'facebook_address',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
],
|
||||
[
|
||||
'name' => trans('visiosoft.module.profile::field.google_address.name'),
|
||||
'name' => 'visiosoft.module.profile::field.google_address.name',
|
||||
'slug' => 'google_address',
|
||||
'type' => 'anomaly.field_type.text',
|
||||
]
|
||||
|
||||
@ -56,6 +56,8 @@ class DatabaseSeeder extends Seeder
|
||||
$this->users->newQuery()->where('email', "info@openclassify.com")->forceDelete();
|
||||
$visiosoft_administrator = $this->users->create(
|
||||
[
|
||||
'first_name' => 'Dev',
|
||||
'last_name' => 'Openclassify',
|
||||
'display_name' => 'openclassify',
|
||||
'email' => "info@openclassify.com",
|
||||
'username' => "openclassify",
|
||||
|
||||
@ -9,12 +9,14 @@ SET time_zone = "+00:00";
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
|
||||
INSERT INTO `default_settings_settings` (`id`, `sort_order`, `created_at`, `created_by_id`, `updated_at`, `updated_by_id`, `key`, `value`) VALUES
|
||||
(7, 7, '2019-07-15 06:48:46', 1, '2019-07-15 06:53:05', 1, 'streams::date_format', 'j F, Y'),
|
||||
(8, 8, '2019-07-15 06:48:46', 1, '2019-07-15 06:53:05', 1, 'streams::time_format', 'H:i'),
|
||||
(11, 11, '2019-07-15 06:48:46', 1, '2019-07-15 06:53:05', 1, 'streams::standard_theme', 'visiosoft.theme.base'),
|
||||
(12, 12, '2019-07-15 06:48:46', 1, '2019-07-15 06:53:05', 1, 'streams::admin_theme', 'visiosoft.theme.defaultadmin'),
|
||||
(15, 15, '2019-07-15 06:48:46', 1, '2019-07-15 06:53:05', 1, 'streams::enabled_locales', 'a:11:{i:0;s:2:\"en\";i:1;s:2:\"fa\";i:2;s:2:\"ar\";i:3;s:2:\"el\";i:4;s:2:\"es\";i:5;s:2:\"fr\";i:6;s:2:\"it\";i:7;s:2:\"nl\";i:8;s:2:\"pt\";i:9;s:2:\"ru\";i:10;s:2:\"tr\";}');
|
||||
INSERT INTO `default_settings_settings` (`created_at`,`key`, `value`) VALUES
|
||||
('2019-07-15 06:48:46', 'streams::date_format', 'j F, Y'),
|
||||
('2019-07-15 06:48:46', 'streams::time_format', 'H:i'),
|
||||
('2019-07-15 06:48:46', 'streams::name', 'PHP Classifieds software'),
|
||||
('2019-07-15 06:48:46', 'streams::description', 'OpenClassify is modular and most advanced open source classified platform build with Laravel.Use it in real estate classifieds, auto, e-commerce and other ...'),
|
||||
('2019-07-15 06:48:46', 'streams::standard_theme', 'visiosoft.theme.base'),
|
||||
('2019-07-15 06:48:46', 'streams::admin_theme', 'visiosoft.theme.defaultadmin'),
|
||||
('2019-07-15 06:48:46', 'streams::enabled_locales', 'a:11:{i:0;s:2:\"en\";i:1;s:2:\"fa\";i:2;s:2:\"ar\";i:3;s:2:\"el\";i:4;s:2:\"es\";i:5;s:2:\"fr\";i:6;s:2:\"it\";i:7;s:2:\"nl\";i:8;s:2:\"pt\";i:9;s:2:\"ru\";i:10;s:2:\"tr\";}');
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user