Merge pull request #16709 from snipe/#16699-fix-email-locales-when-none-set-on-user

Fixed #16699 - Better handle user locales in mailables
This commit is contained in:
snipe 2025-04-15 16:44:38 +01:00 committed by GitHub
commit 0be50e803e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 17 deletions

View file

@ -339,6 +339,7 @@ class UsersController extends Controller
$users = $users->where(function ($query) use ($request) { $users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search')) $query->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%') ->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('email', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%'); ->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
}); });
} }

View file

@ -53,7 +53,7 @@ class ProfileController extends Controller
$user->enable_confetti = $request->input('enable_confetti', false); $user->enable_confetti = $request->input('enable_confetti', false);
if (! config('app.lock_passwords')) { if (! config('app.lock_passwords')) {
$user->locale = $request->input('locale', 'en-US'); $user->locale = $request->input('locale');
} }
if ((Gate::allows('self.two_factor')) && ((Setting::getSettings()->two_factor_enabled == '1') && (! config('app.lock_passwords')))) { if ((Gate::allows('self.two_factor')) && ((Setting::getSettings()->two_factor_enabled == '1') && (! config('app.lock_passwords')))) {

View file

@ -63,11 +63,9 @@ class CheckoutableListener
} }
$ccEmails = array_filter($adminCcEmailsArray); $ccEmails = array_filter($adminCcEmailsArray);
$mailable = $this->getCheckoutMailType($event, $acceptance); $mailable = $this->getCheckoutMailType($event, $acceptance);
$notifiable = $this->getNotifiables($event); $notifiable = $this->getNotifiableUsers($event);
if ($event->checkedOutTo->locale) {
$mailable->locale($event->checkedOutTo->locale);
}
// Send email notifications // Send email notifications
try { try {
/** /**
@ -79,7 +77,7 @@ class CheckoutableListener
if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() || if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() ||
$this->checkoutableShouldSendEmail($event)) { $this->checkoutableShouldSendEmail($event)) {
Log::info('Sending checkout email, Locale: ' . ($event->checkedOutTo->locale ?? 'default')); //Log::info('Sending checkout email, Locale: ' . ($event->checkedOutTo->locale ?? 'default'));
if (!empty($notifiable)) { if (!empty($notifiable)) {
Mail::to($notifiable)->cc($ccEmails)->send($mailable); Mail::to($notifiable)->cc($ccEmails)->send($mailable);
} elseif (!empty($ccEmails)) { } elseif (!empty($ccEmails)) {
@ -161,10 +159,8 @@ class CheckoutableListener
} }
$ccEmails = array_filter($adminCcEmailsArray); $ccEmails = array_filter($adminCcEmailsArray);
$mailable = $this->getCheckinMailType($event); $mailable = $this->getCheckinMailType($event);
$notifiable = $this->getNotifiables($event); $notifiable = $this->getNotifiableUsers($event);
if ($event->checkedOutTo?->locale) {
$mailable->locale($event->checkedOutTo->locale);
}
// Send email notifications // Send email notifications
try { try {
/** /**
@ -175,7 +171,6 @@ class CheckoutableListener
*/ */
if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() || if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() ||
$this->checkoutableShouldSendEmail($event)) { $this->checkoutableShouldSendEmail($event)) {
Log::info('Sending checkin email, Locale: ' . ($event->checkedOutTo->locale ?? 'default'));
if (!empty($notifiable)) { if (!empty($notifiable)) {
Mail::to($notifiable)->cc($ccEmails)->send($mailable); Mail::to($notifiable)->cc($ccEmails)->send($mailable);
} elseif (!empty($ccEmails)){ } elseif (!empty($ccEmails)){
@ -324,17 +319,26 @@ class CheckoutableListener
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note); return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
} }
private function getNotifiables($event){
/**
* This gets the recipient objects based on the type of checkoutable.
* The 'name' property for users is set in the boot method in the User model.
*
* @see \App\Models\User::boot()
* @param $event
* @return mixed
*/
private function getNotifiableUsers($event){
if($event->checkedOutTo instanceof Asset){ if($event->checkedOutTo instanceof Asset){
$event->checkedOutTo->load('assignedTo'); $event->checkedOutTo->load('assignedTo');
return $event->checkedOutTo->assignedto?->email ?? ''; return $event->checkedOutTo->assignedto;
} }
else if($event->checkedOutTo instanceof Location) { else if($event->checkedOutTo instanceof Location) {
return $event->checkedOutTo->manager?->email ?? ''; return $event->checkedOutTo->manager;
} }
else{ else{
return $event->checkedOutTo?->email ?? ''; return $event->checkedOutTo;
} }
} }
private function webhookSelected(){ private function webhookSelected(){

View file

@ -20,6 +20,7 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Laravel\Passport\HasApiTokens; use Laravel\Passport\HasApiTokens;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends SnipeModel implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, HasLocalePreference class User extends SnipeModel implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, HasLocalePreference
{ {
@ -139,6 +140,29 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'manager' => ['first_name', 'last_name', 'username'], 'manager' => ['first_name', 'last_name', 'username'],
]; ];
/**
* This sets the name property on the user. It's not a real field in the database
* (since we use first_name and last_name), but the Laravel mailable method
* uses this to determine the name of the user to send emails to.
*
* We only have to do this on the User model and no other models because other
* first-class objects have a name field.
* @return void
*/
public $name;
protected static function boot()
{
parent::boot();
static::retrieved(function($user){
$user->name = $user->getFullNameAttribute();
});
}
/** /**
* Internally check the user permission for the given section * Internally check the user permission for the given section
* *
@ -279,6 +303,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->activated == 1; return $this->activated == 1;
} }
/** /**
* Returns the full name attribute * Returns the full name attribute
* *
@ -844,10 +869,22 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $query->leftJoin('companies as companies_user', 'users.company_id', '=', 'companies_user.id')->orderBy('companies_user.name', $order); return $query->leftJoin('companies as companies_user', 'users.company_id', '=', 'companies_user.id')->orderBy('companies_user.name', $order);
} }
public function preferredLocale()
/**
* Get the preferred locale for the user.
*
* This uses the HasLocalePreference contract to determine the user's preferred locale,
* used by Laravel's mail system to determine the locale for sending emails.
* https://laravel.com/docs/11.x/mail#user-preferred-locales
*
*/
public function preferredLocale(): string
{ {
return $this->locale; return $this->locale ?? Setting::getSettings()->locale ?? config('app.locale');
} }
public function getUserTotalCost(){ public function getUserTotalCost(){
$asset_cost= 0; $asset_cost= 0;
$license_cost= 0; $license_cost= 0;

View file

@ -40,6 +40,19 @@ class UsersForSelectListTest extends TestCase
$this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, 'Luke'))); $this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, 'Luke')));
} }
public function testUsersCanBeSearchedByEmail()
{
User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker', 'email' => 'luke@jedis.org']);
Passport::actingAs(User::factory()->create());
$response = $this->getJson(route('api.users.selectlist', ['search' => 'luke@jedis']))->assertOk();
$results = collect($response->json('results'));
$this->assertEquals(1, $results->count());
$this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, 'Luke')));
}
public function testUsersScopedToCompanyWhenMultipleFullCompanySupportEnabled() public function testUsersScopedToCompanyWhenMultipleFullCompanySupportEnabled()
{ {
$this->settings->enableMultipleFullCompanySupport(); $this->settings->enableMultipleFullCompanySupport();