Browse Source

Merge pull request #68 from nafiesl/cemetery_location

Person Cemetery Location
master
Nafies Luthfi 5 years ago
committed by GitHub
parent
commit
6e18a0f6cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .env.example
  2. 52
      app/Http/Controllers/UsersController.php
  3. 5
      app/Http/Requests/Users/UpdateRequest.php
  4. 36
      app/User.php
  5. 14
      app/UserMetadata.php
  6. 8
      config/leaflet.php
  7. 26
      database/factories/ModelFactory.php
  8. 36
      database/migrations/2021_04_04_215601_create_user_metadata_table.php
  9. 8
      resources/lang/en/address.php
  10. 3
      resources/lang/en/app.php
  11. 2
      resources/lang/en/user.php
  12. 8
      resources/lang/id/address.php
  13. 3
      resources/lang/id/app.php
  14. 2
      resources/lang/id/user.php
  15. 91
      resources/views/users/death.blade.php
  16. 51
      resources/views/users/edit.blade.php
  17. 5
      resources/views/users/partials/action-buttons.blade.php
  18. 10
      resources/views/users/partials/edit_death.blade.php
  19. 1
      routes/web.php
  20. 68
      tests/Feature/UsersProfileTest.php
  21. 69
      tests/Unit/UserTest.php

7
.env.example

@ -35,4 +35,9 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
LEAFLET_MAP_ZOOM_LEVEL=4
LEAFLET_MAP_DETAIL_ZOOM_LEVEL=18
LEAFLET_MAP_CENTER_LATITUDE="-0.87887"
LEAFLET_MAP_CENTER_LONGITUDE="117.4863"

52
app/Http/Controllers/UsersController.php

@ -6,7 +6,10 @@ use App\Couple;
use App\Http\Requests\Users\UpdateRequest;
use App\Jobs\Users\DeleteAndReplaceUser;
use App\User;
use App\UserMetadata;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Ramsey\Uuid\Uuid;
use Storage;
class UsersController extends Controller
@ -97,6 +100,21 @@ class UsersController extends Controller
}
/**
* Show user death info.
*
* @param \App\User $user
* @return \Illuminate\View\View
*/
public function death(User $user)
{
$mapZoomLevel = config('leaflet.detail_zoom_level');
$mapCenterLatitude = $user->getMetadata('cemetery_location_latitude');
$mapCenterLongitude = $user->getMetadata('cemetery_location_longitude');
return view('users.death', compact('user', 'mapZoomLevel', 'mapCenterLatitude', 'mapCenterLongitude'));
}
/**
* Show the form for editing the specified User.
*
* @param \App\User $user
@ -113,7 +131,18 @@ class UsersController extends Controller
$validTabs = ['death', 'contact_address', 'login_account'];
return view('users.edit', compact('user', 'replacementUsers', 'validTabs'));
$mapZoomLevel = config('leaflet.zoom_level');
$mapCenterLatitude = $user->getMetadata('cemetery_location_latitude');
$mapCenterLongitude = $user->getMetadata('cemetery_location_longitude');
if ($mapCenterLatitude && $mapCenterLongitude) {
$mapZoomLevel = config('leaflet.detail_zoom_level');
}
$mapCenterLatitude = $mapCenterLatitude ?: config('leaflet.map_center_latitude');
$mapCenterLongitude = $mapCenterLongitude ?: config('leaflet.map_center_longitude');
return view('users.edit', compact(
'user', 'replacementUsers', 'validTabs', 'mapZoomLevel', 'mapCenterLatitude', 'mapCenterLongitude'
));
}
/**
@ -125,7 +154,11 @@ class UsersController extends Controller
*/
public function update(UpdateRequest $request, User $user)
{
$user->update($request->validated());
$userAttributes = $request->validated();
$user->update($userAttributes);
$userAttributes = collect($userAttributes);
$this->updateUserMetadata($user, $userAttributes);
return redirect()->route('users.show', $user->id);
}
@ -232,4 +265,19 @@ class UsersController extends Controller
return $allMariageList;
}
private function updateUserMetadata(User $user, Collection $userAttributes)
{
foreach (User::METADATA_KEYS as $key) {
if ($userAttributes->has($key) == false) {
continue;
}
$userMeta = UserMetadata::firstOrNew(['user_id' => $user->id, 'key' => $key]);
if (!$userMeta->exists) {
$userMeta->id = Uuid::uuid4()->toString();
}
$userMeta->value = $userAttributes->get($key);
$userMeta->save();
}
}
}

5
app/Http/Requests/Users/UpdateRequest.php

@ -39,6 +39,11 @@ class UpdateRequest extends FormRequest
'email' => 'nullable|string|max:255',
'password' => 'nullable|min:6|max:15',
'birth_order' => 'nullable|numeric|min:1',
'cemetery_location_name' => 'nullable|string|max:255',
'cemetery_location_address' => 'nullable|string|max:255',
'cemetery_location_latitude' => 'required_with:cemetery_location_longitude|nullable|string|max:255',
'cemetery_location_longitude' => 'required_with:cemetery_location_latitude|nullable|string|max:255',
];
}

36
app/User.php

@ -11,6 +11,13 @@ class User extends Authenticatable
{
use Notifiable;
const METADATA_KEYS = [
'cemetery_location_name',
'cemetery_location_address',
'cemetery_location_latitude',
'cemetery_location_longitude',
];
/**
* Indicates if the IDs are auto-incrementing.
*
@ -314,4 +321,33 @@ class User extends Authenticatable
return Carbon::now()->diffInDays($this->birthday, false);
}
}
public function metadata()
{
return $this->hasMany(UserMetadata::class, 'user_id', 'id');
}
public function getMetadata($key = null, $defaultValue = null)
{
$metadata = $this->metadata;
if (is_null($key)) {
$metadataCollection = [];
foreach ($metadata as $metaKey => $metaValue) {
$metadataCollection[$metaKey] = $metaValue;
}
return collect($metadataCollection);
}
$meta = $metadata->filter(function ($meta) use ($key) {
return $meta->key == $key;
})->first();
if ($meta) {
return $meta->value;
}
return $defaultValue;
}
}

14
app/UserMetadata.php

@ -0,0 +1,14 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class UserMetadata extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = ['id', 'user_id', 'key', 'value'];
}

8
config/leaflet.php

@ -0,0 +1,8 @@
<?php
return [
'zoom_level' => env('LEAFLET_MAP_ZOOM_LEVEL', 4),
'detail_zoom_level' => env('LEAFLET_MAP_DETAIL_ZOOM_LEVEL', 18),
'map_center_latitude' => env('LEAFLET_MAP_CENTER_LATITUDE', '-0.87887'),
'map_center_longitude' => env('LEAFLET_MAP_CENTER_LONGITUDE', '117.4863'),
];

26
database/factories/ModelFactory.php

@ -2,6 +2,7 @@
use App\Couple;
use App\User;
use App\UserMetadata;
/*
|--------------------------------------------------------------------------
@ -18,10 +19,10 @@ use App\User;
$factory->define(User::class, function (Faker\Generator $faker) {
$name = $faker->name;
return [
'id' => $faker->uuid,
'name' => $name,
'nickname' => $name,
'gender_id' => rand(1, 2),
'id' => $faker->uuid,
'name' => $name,
'nickname' => $name,
'gender_id' => rand(1, 2),
'manager_id' => $faker->uuid,
];
});
@ -36,15 +37,26 @@ $factory->state(User::class, 'female', function (Faker\Generator $faker) {
$factory->define(Couple::class, function (Faker\Generator $faker) {
return [
'id' => $faker->uuid,
'id' => $faker->uuid,
'husband_id' => function () {
return factory(User::class)->states('male')->create()->id;
},
'wife_id' => function () {
'wife_id' => function () {
return factory(User::class)->states('female')->create()->id;
},
'manager_id' => function () {
return factory(User::class)->create()->id;
},
];
});
});
$factory->define(UserMetadata::class, function (Faker\Generator $faker) {
return [
'id' => $faker->uuid,
'user_id' => function () {
return factory(User::class)->create()->id;
},
'key' => $faker->name,
'value' => $faker->sentence,
];
});

36
database/migrations/2021_04_04_215601_create_user_metadata_table.php

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserMetadataTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_metadata', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('user_id');
$table->string('key')->index();
$table->text('value')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_metadata');
}
}

8
resources/lang/en/address.php

@ -0,0 +1,8 @@
<?php
return [
'address' => 'Address',
'location_name' => 'Location Name',
'latitude' => 'Latitude',
'longitude' => 'Longitude',
];

3
resources/lang/en/app.php

@ -39,4 +39,7 @@ return [
'download' => 'Download',
'delete' => 'Delete',
'cancel' => 'Cancel',
'open_in_google_map' => 'Open in Google Map',
'data_not_available' => 'Data not available.',
];

2
resources/lang/en/user.php

@ -51,6 +51,8 @@ return [
'phone' => 'Phone',
'manager' => 'Manager',
'cemetery_location' => 'Cemetary Location',
// Photo
'reupload_photo' => 'Re-upload Photo',
'update_photo' => 'Update Photo',

8
resources/lang/id/address.php

@ -0,0 +1,8 @@
<?php
return [
'address' => 'Alamat',
'location_name' => 'Nama Lokasi',
'latitude' => 'Latitude',
'longitude' => 'Longitude',
];

3
resources/lang/id/app.php

@ -39,4 +39,7 @@ return [
'download' => 'Download',
'delete' => 'Hapus',
'cancel' => 'Batal',
'open_in_google_map' => 'Buka di Google Map',
'data_not_available' => 'Data tidak tersedia.',
];

2
resources/lang/id/user.php

@ -51,6 +51,8 @@ return [
'phone' => 'Telp.',
'manager' => 'Pengelola',
'cemetery_location' => 'Lokasi Makam',
// Photo
'reupload_photo' => 'Upload ulang Foto',
'update_photo' => 'Update Foto',

91
resources/views/users/death.blade.php

@ -0,0 +1,91 @@
@extends('layouts.user-profile')
@section('subtitle', __('user.death'))
@section('user-content')
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
@can('edit', $user)
{{ link_to_route('users.edit', __('app.edit'), [$user->id, 'tab' => 'death'], ['class' => 'pull-right']) }}
@endcan
<h3 class="panel-title">{{ __('user.death') }}</h3>
</div>
<table class="table">
<tbody>
<tr>
<th>{{ __('address.location_name') }}</th>
<td>{{ $user->getMetadata('cemetery_location_name') }}</td>
</tr>
<tr>
<th>{{ __('address.address') }}</th>
<td>{{ $user->getMetadata('cemetery_location_address') }}</td>
</tr>
<tr>
<th>{{ __('user.dod') }}</th>
<td>{{ $user->dod ?: $user->yod }}</td>
</tr>
<tr>
<th>{{ __('user.age') }}</th>
<td>
@if ($user->age)
{!! $user->age_string !!}
@endif
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ __('user.cemetery_location') }}</h3></div>
@if ($mapCenterLatitude && $mapCenterLongitude)
<div class="panel-body"><div id="mapid"></div></div>
<div class="panel-footer">
@php
$locationCoordinate = $mapCenterLatitude.','.$mapCenterLongitude.'/@'.$mapCenterLatitude.','.$mapCenterLongitude.','.$mapZoomLevel.'z';
@endphp
{{ link_to(
'https://www.google.com/maps/place/'.$locationCoordinate,
__('app.open_in_google_map'),
['class' => 'btn btn-default btn-block', 'target' => '_blank']
) }}
</div>
@else
<div class="panel-body">{{ __('app.data_not_available') }}</div>
@endif
</div>
</div>
</div>
@endsection
@if ($mapCenterLatitude && $mapCenterLongitude)
@section('ext_css')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""/>
<style>
#mapid { height: 300px; }
</style>
@endsection
@section('script')
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
<script>
var mapCenter = [{{ $mapCenterLatitude }}, {{ $mapCenterLongitude }}];
var map = L.map('mapid').setView(mapCenter, {{ $mapZoomLevel }});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
var marker = L.marker(mapCenter).addTo(map);
</script>
@endsection
@endif

51
resources/views/users/edit.blade.php

@ -34,6 +34,9 @@
{{ Form::close() }}
<div class="col-md-6">
@includeWhen(request('tab') == null || !in_array(request('tab'), $validTabs), 'users.partials.update_photo')
@if (request('tab') == 'death')
<div id="mapid"></div>
@endif
</div>
</div>
</div>
@ -43,10 +46,26 @@
@section('ext_css')
<link href="{{ asset('css/plugins/jquery.datetimepicker.css') }}" rel="stylesheet">
@if (request('tab') == 'death')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""/>
<style>
#mapid { height: 300px; }
</style>
@endif
@endsection
@section('script')
<script src="{{ asset('js/plugins/jquery.datetimepicker.js') }}"></script>
@if (request('tab') == 'death')
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
@endif
<script>
(function() {
$('#dob,#dod').datetimepicker({
@ -56,5 +75,37 @@
scrollInput: false
});
})();
@if (request('tab') == 'death')
var mapCenter = [{{ $mapCenterLatitude }}, {{ $mapCenterLongitude }}];
var map = L.map('mapid').setView(mapCenter, {{ $mapZoomLevel }});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
var marker = L.marker(mapCenter).addTo(map);
function updateMarker(lat, lng) {
marker
.setLatLng([lat, lng])
.bindPopup("Your location : " + marker.getLatLng().toString())
.openPopup();
return false;
};
map.on('click', function(e) {
let latitude = e.latlng.lat.toString().substring(0, 15);
let longitude = e.latlng.lng.toString().substring(0, 15);
$('#cemetery_location_latitude').val(latitude);
$('#cemetery_location_longitude').val(longitude);
updateMarker(latitude, longitude);
});
var updateMarkerByInputs = function() {
return updateMarker( $('#cemetery_location_latitude').val() , $('#cemetery_location_longitude').val());
}
$('#cemetery_location_latitude').on('input', updateMarkerByInputs);
$('#cemetery_location_longitude').on('input', updateMarkerByInputs);
@endif
</script>
@endsection

5
resources/views/users/partials/action-buttons.blade.php

@ -6,4 +6,7 @@
{{ link_to_route('users.chart', trans('app.show_family_chart'), [$user->id], ['class' => Request::segment(3) == 'chart' ? 'btn btn-default active' : 'btn btn-default']) }}
{{ link_to_route('users.tree', trans('app.show_family_tree'), [$user->id], ['class' => Request::segment(3) == 'tree' ? 'btn btn-default active' : 'btn btn-default']) }}
{{ link_to_route('users.marriages', trans('app.show_marriages'), [$user->id], ['class' => Request::segment(3) == 'marriages' ? 'btn btn-default active' : 'btn btn-default']) }}
</div>
@if ($user->yod)
{{ link_to_route('users.death', trans('user.death'), [$user->id], ['class' => Request::segment(3) == 'death' ? 'btn btn-default active' : 'btn btn-default']) }}
@endif
</div>

10
resources/views/users/partials/edit_death.blade.php

@ -2,3 +2,13 @@
<div class="col-md-6">{!! FormField::text('yod', ['label' => __('user.yod'), 'placeholder' => __('app.example').' 2003']) !!}</div>
<div class="col-md-6">{!! FormField::text('dod', ['label' => __('user.dod'), 'placeholder' => __('app.example').' 2003-10-17']) !!}</div>
</div>
<fieldset>
<legend>{{ __('user.cemetery_location') }}</legend>
{!! FormField::text('cemetery_location_name', ['label' => __('address.location_name'), 'value' => old('cemetery_location_name', $user->getMetadata('cemetery_location_name'))]) !!}
{!! FormField::textarea('cemetery_location_address', ['label' => __('address.address'), 'value' => old('cemetery_location_address', $user->getMetadata('cemetery_location_address'))]) !!}
<div class="row">
<div class="col-md-6">{!! FormField::text('cemetery_location_latitude', ['label' => __('address.latitude'), 'value' => old('cemetery_location_latitude', $user->getMetadata('cemetery_location_latitude'))]) !!}</div>
<div class="col-md-6">{!! FormField::text('cemetery_location_longitude', ['label' => __('address.longitude'), 'value' => old('cemetery_location_longitude', $user->getMetadata('cemetery_location_longitude'))]) !!}</div>
</div>
</fieldset>

1
routes/web.php

@ -35,6 +35,7 @@ Route::get('users/{user}/edit', 'UsersController@edit')->name('users.edit');
Route::patch('users/{user}', 'UsersController@update')->name('users.update');
Route::get('users/{user}/chart', 'UsersController@chart')->name('users.chart');
Route::get('users/{user}/tree', 'UsersController@tree')->name('users.tree');
Route::get('users/{user}/death', 'UsersController@death')->name('users.death');
Route::patch('users/{user}/photo-upload', 'UsersController@photoUpload')->name('users.photo-upload');
Route::delete('users/{user}', 'UsersController@destroy')->name('users.destroy');

68
tests/Feature/UsersProfileTest.php

@ -4,6 +4,8 @@ namespace Tests\Feature;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Ramsey\Uuid\Uuid;
use Storage;
use Tests\TestCase;
@ -168,6 +170,72 @@ class UsersProfileTest extends TestCase
}
/** @test */
public function user_can_update_died_person_cemetary_location()
{
$user = $this->loginAsUser();
$this->visit(route('users.edit', [$user->id, 'tab' => 'death']));
$this->seePageIs(route('users.edit', [$user->id, 'tab' => 'death']));
$this->submitForm(trans('app.update'), [
'dod' => '',
'yod' => '2003',
'cemetery_location_name' => 'Some name',
'cemetery_location_address' => 'Some address',
'cemetery_location_latitude' => '-3.333333',
'cemetery_location_longitude' => '114.583333',
]);
$this->seeInDatabase('users', [
'id' => $user->id,
'dod' => null,
'yod' => '2003',
]);
$this->seeInDatabase('user_metadata', [
'user_id' => $user->id,
'key' => 'cemetery_location_name',
'value' => 'Some name',
]);
$this->seeInDatabase('user_metadata', [
'user_id' => $user->id,
'key' => 'cemetery_location_address',
'value' => 'Some address',
]);
$this->seeInDatabase('user_metadata', [
'user_id' => $user->id,
'key' => 'cemetery_location_latitude',
'value' => '-3.333333',
]);
$this->seeInDatabase('user_metadata', [
'user_id' => $user->id,
'key' => 'cemetery_location_longitude',
'value' => '114.583333',
]);
}
/** @test */
public function user_metadata_can_be_prefilled_on_the_edit_form()
{
$user = $this->loginAsUser();
DB::table('user_metadata')->insert([
'id' => Uuid::uuid4()->toString(),
'user_id' => $user->id,
'key' => 'cemetery_location_name',
'value' => 'Some place name',
]);
$this->visit(route('users.edit', [$user->id, 'tab' => 'death']));
$this->seePageIs(route('users.edit', [$user->id, 'tab' => 'death']));
$this->seeElement('input', [
'name' => 'cemetery_location_name',
'value' => 'Some place name',
]);
}
/** @test */
public function manager_can_add_login_account_on_a_user()
{
$manager = $this->loginAsUser();

69
tests/Unit/UserTest.php

@ -4,9 +4,12 @@ namespace Tests\Unit;
use App\Couple;
use App\User;
use App\UserMetadata;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Ramsey\Uuid\Nonstandard\Uuid;
use Tests\TestCase;
class UserTest extends TestCase
@ -110,6 +113,70 @@ class UserTest extends TestCase
}
/** @test */
public function a_user_has_many_metadata_relation()
{
$user = factory(User::class)->create();
$metadata = factory(UserMetadata::class)->create(['user_id' => $user->id]);
$this->assertInstanceOf(Collection::class, $user->metadata);
$this->assertInstanceOf(UserMetadata::class, $user->metadata->first());
}
/** @test */
public function user_model_has_get_metadata_method()
{
$user = factory(User::class)->create();
$this->assertNull($user->getMetadata('cemetery_location_address'));
DB::table('user_metadata')->insert([
'id' => Uuid::uuid4()->toString(),
'user_id' => $user->id,
'key' => 'cemetery_location_address',
'value' => 'Some address',
]);
$user = $user->fresh();
$this->assertEquals('Some address', $user->getMetadata('cemetery_location_address'));
}
/** @test */
public function user_model_get_metadata_method_returns_all_metadata_if_key_is_null()
{
$user = factory(User::class)->create();
$this->assertEmpty($user->getMetadata());
DB::table('user_metadata')->insert([
'id' => Uuid::uuid4()->toString(),
'user_id' => $user->id,
'key' => 'cemetery_location_address',
'value' => 'Some address',
]);
$user = $user->fresh();
$this->assertCount(1, $user->getMetadata());
}
/** @test */
public function user_model_get_metadata_method_accepts_a_default_value()
{
$user = factory(User::class)->create();
$this->assertEquals('Default value', $user->getMetadata('some_missing_key', 'Default value'));
DB::table('user_metadata')->insert([
'id' => Uuid::uuid4()->toString(),
'user_id' => $user->id,
'key' => 'some_missing_key',
'value' => 'Some value',
]);
$user = $user->fresh();
$this->assertEquals('Some value', $user->getMetadata('some_missing_key', 'Default value'));
}
/** @test */
public function user_have_mother_link_method()
{
$mother = factory(User::class)->create();

Loading…
Cancel
Save