1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 00:11:35 +02:00

Dynamically apply locale (#3140)

* Minor fixes for OpenAPI docs for clients

* Add fields to company transformer

* Padding email templates, system level and custom

* Minor fixes for email template subject

* Working on Email Templates

* Clean up User model, remove redundant permissions methods

* Implement Locale for API

* Implement Locale middleware for client routes
This commit is contained in:
David Bomba 2019-12-11 07:25:54 +11:00 committed by GitHub
parent ec5cbe66a0
commit 550cb42722
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 572 additions and 73 deletions

View File

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Mail\TemplateEmail;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendTestEmails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:send-test-emails';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends Test Emails to check templates';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->sendTemplateEmails('plain');
$this->sendTemplateEmails('light');
$this->sendTemplateEmails('dark');
}
private function sendTemplateEmails($template)
{
$message = [
'title' => 'Invoice XJ-3838',
'body' => '<div>"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"</div>',
'subject' => 'The Test Subject',
'footer' => 'Lovely Footer Texts',
];
$user = User::whereEmail('user@example.com')->first();
if(!$user){
$user = factory(\App\Models\User::class)->create([
'confirmation_code' => '123',
]);
}
$cc_emails = [config('ninja.testvars.test_email')];
$bcc_emails = [config('ninja.testvars.test_email')];
Mail::to(config('ninja.testvars.test_email'))
->cc($cc_emails)
->bcc($bcc_emails)
//->replyTo(also_available_if_needed)
->send(new TemplateEmail($message, $template, $user));
}
}

View File

@ -17,7 +17,8 @@ class EmailTemplateDefaults
{
public static function emailInvoiceSubject()
{
return Parsedown::instance()->line(self::transformText('invoice_subject'));
return ctrans('invoice_subject', ['number'=>'$number', 'account'=>'$company']);
//return Parsedown::instance()->line(self::transformText('invoice_subject'));
}
public static function emailInvoiceTemplate()
@ -27,7 +28,9 @@ class EmailTemplateDefaults
public static function emailQuoteSubject()
{
return Parsedown::instance()->line(self::transformText('quote_subject'));
return ctrans('quote_subject', ['number'=>'$number', 'account'=>'$company']);
//return Parsedown::instance()->line(self::transformText('quote_subject'));
}
public static function emailQuoteTemplate()
@ -37,7 +40,8 @@ class EmailTemplateDefaults
public static function emailPaymentSubject()
{
return Parsedown::instance()->line(self::transformText('payment_subject'));
return ctrans('texts.payment_subject');
//return Parsedown::instance()->line(self::transformText('payment_subject'));
}
public static function emailPaymentTemplate()
@ -47,7 +51,9 @@ class EmailTemplateDefaults
public static function emailReminder1Subject()
{
return Parsedown::instance()->line(self::transformText('reminder_subject'));
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']);
// return Parsedown::instance()->line(self::transformText('reminder_subject'));
}
public static function emailReminder1Template()
@ -57,7 +63,8 @@ class EmailTemplateDefaults
public static function emailReminder2Subject()
{
return Parsedown::instance()->line(self::transformText('reminder_subject'));
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']);
// return Parsedown::instance()->line(self::transformText('reminder_subject'));
}
public static function emailReminder2Template()
@ -67,7 +74,8 @@ class EmailTemplateDefaults
public static function emailReminder3Subject()
{
return Parsedown::instance()->line(self::transformText('reminder_subject'));
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']);
// return Parsedown::instance()->line(self::transformText('reminder_subject'));
}
public static function emailReminder3Template()
@ -77,7 +85,8 @@ class EmailTemplateDefaults
public static function emailReminderEndlessSubject()
{
return Parsedown::instance()->line(self::transformText('reminder_subject'));
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']);
// return Parsedown::instance()->line(self::transformText('reminder_subject'));
}
public static function emailReminderEndlessTemplate()

View File

@ -20,10 +20,10 @@ use Illuminate\Support\Facades\Cache;
* @param string translation string key
* @return string
*/
function ctrans(string $string) : string
function ctrans(string $string, $replace = [], $locale = null) : string
{
//todo pass through the cached version of the custom strings here else return trans();
return trans($string);
return trans($string, $replace, $locale);
}

View File

@ -72,15 +72,6 @@ class Kernel extends HttpKernel
\App\Http\Middleware\StartupCheck::class,
\App\Http\Middleware\QueryLogging::class,
],
'api_db' => [
\App\Http\Middleware\SetDb::class,
],
'web_db' => [
\App\Http\Middleware\SetWebDb::class,
],
'url_db' => [
\App\Http\Middleware\UrlSetDb::class,
]
];
/**
@ -111,5 +102,9 @@ class Kernel extends HttpKernel
'password_protected' => \App\Http\Middleware\PasswordProtection::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'portal_enabled' => \App\Http\Middleware\ClientPortalEnabled::class,
'url_db' => \App\Http\Middleware\UrlSetDb::class,
'web_db' => \App\Http\Middleware\SetWebDb::class,
'api_db' => \App\Http\Middleware\SetDb::class,
'locale' => \App\Http\Middleware\Locale::class,
];
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Middleware;
use App\Models\Language;
use Closure;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
class Locale
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
/*LOCALE SET */
if ($request->has('lang')) {
$locale = $request->input('lang');
App::setLocale($locale);
} elseif (auth('contact')->user()) {
App::setLocale(auth('contact')->user()->client->locale());
} elseif (auth()->user()) {
App::setLocale(auth()->user()->company()->getLocale());
}
else
App::setLocale(config('ninja.i18n.locale'));
return $next($request);
}
}

View File

@ -11,13 +11,15 @@
namespace App\Http\Middleware;
use App\Models\Language;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request as Input;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Closure;
/**
@ -64,10 +66,7 @@ class StartupCheck
}
}
}
// $end = microtime(true) - $start;
// Log::error("middleware cost = {$end} ms");
$response = $next($request);
return $response;

View File

@ -0,0 +1,49 @@
<?php
namespace App\Mail;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class TemplateEmail extends Mailable
{
use Queueable, SerializesModels;
private $template; //the template to use
private $message; //the message array (subject and body)
private $user; //the user the email will be sent from
public function __construct($message, $template, $user)
{
$this->message = $message;
$this->template = $template;
$this->user = $user;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
/*Alter Run Time Mailer configuration (driver etc etc) to regenerate the Mailer Singleton*/
//if using a system level template
$template_name = 'email.template.'.$this->template;
return $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version
->subject($this->message['subject'])
->view($template_name, [
'body' => $this->message['body'],
'footer' => $this->message['footer'],
'title' => $this->message['title'],
]);
}
}

View File

@ -23,6 +23,7 @@ use App\Models\DatetimeFormat;
use App\Models\Filterable;
use App\Models\GatewayType;
use App\Models\GroupSetting;
use App\Models\Language;
use App\Models\Timezone;
use App\Models\User;
use App\Utils\Traits\CompanyGatewaySettings;
@ -174,6 +175,16 @@ class Client extends BaseModel
return Timezone::find($this->getSetting('timezone_id'));
}
public function language()
{
return Language::find($this->getSetting('language_id'));
}
public function locale()
{
return $this->language()->locale;
}
public function date_format()
{
$date_formats = Cache::get('date_formats');

View File

@ -90,7 +90,6 @@ class User extends Authenticatable implements MustVerifyEmail
protected $casts = [
'settings' => 'object',
'permissions' => 'object',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
@ -197,19 +196,15 @@ class User extends Authenticatable implements MustVerifyEmail
}
/**
* Returns a object of user permissions
* Returns a comma separated list of user permissions
*
* @return stdClass
* @return comma separated list
*/
public function permissions()
{
$permissions = json_decode($this->company_user->permissions);
if (! $permissions)
return [];
return $this->company_user->permissions;
return $permissions;
}
/**
@ -274,18 +269,6 @@ class User extends Authenticatable implements MustVerifyEmail
}
/**
* Flattens a stdClass representation of the User Permissions
* into a Collection
*
* @return Collection
*/
public function permissionsFlat() :Collection
{
return collect($this->permissions())->flatten();
}
/**
* Returns true if permissions exist in the map
@ -296,27 +279,8 @@ class User extends Authenticatable implements MustVerifyEmail
public function hasPermission($permission) : bool
{
return (stripos($this->company_user->permissions, $permission) !== false);
// return $this->permissionsFlat()->contains($permission);
}
/**
* Returns a array of permission for the mobile application
*
* @return array
*/
public function permissionsMap() : array
{
$keys = array_values((array) $this->permissions());
$values = array_fill(0, count($keys), true);
return array_combine($keys, $values);
}
public function documents()

View File

@ -59,6 +59,7 @@ return [
'stripe' => env('STRIPE_KEYS',''),
'paypal' => env('PAYPAL_KEYS', ''),
'travis' => env('TRAVIS', false),
'test_email' => env('TEST_EMAIL',''),
],
'contact' => [
'email' => env('MAIL_FROM_ADDRESS'),

View File

@ -0,0 +1,10 @@
@extends('email.template.master')
@section('title')
{{ $title }}
@endsection
@section('content')
{!! $body !!}
@endsection
@section('footer')
{!! $footer !!}
@endsection

View File

@ -0,0 +1,10 @@
@extends('email.template.master')
@section('title')
{{ $title }}
@endsection
@section('content')
{!! $body !!}
@endsection
@section('footer')
{!! $footer !!}
@endsection

View File

@ -0,0 +1,339 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>@yield('title')</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="">
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
@yield('content')
<!-- START FOOTER -->
<div class="footer">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
<span class="apple-link">Footerish stuff</span>
@yield('footer')
<br> Don't like these emails? <a href="#">Unsubscribe</a>.
</td>
</tr>
<tr>
<td class="content-block powered-by">
Powered by <a href="#">InvoiceNinja</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,4 @@
{{ $body }}
<br>
<br>
{{ $footer}}

View File

@ -32,7 +32,7 @@ Route::group(['api_secret_check','email_db'], function () {
});
Route::group(['middleware' => ['api_db','api_secret_check','token_auth'], 'prefix' =>'api/v1', 'as' => 'api.'], function () {
Route::group(['middleware' => ['api_db','api_secret_check','token_auth','locale'], 'prefix' =>'api/v1', 'as' => 'api.'], function () {
Route::resource('activities', 'ActivityController'); // name = (clients. index / create / show / update / destroy / edit
@ -111,6 +111,4 @@ Route::group(['middleware' => ['api_db','api_secret_check','token_auth'], 'prefi
Route::post('support/messages/send', 'Support\Messages\SendingController');
});
Route::get('test_email', '\App\Helpers\Mail\GmailTransportConfig@test');
Route::fallback('BaseController@notFound');
Route::fallback('BaseController@notFound');

View File

@ -2,16 +2,16 @@
Route::get('client', 'Auth\ContactLoginController@showLoginForm')->name('client.login'); //catch all
Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login');
Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login')->middleware('locale');
Route::post('client/login', 'Auth\ContactLoginController@login')->name('client.login.submit');
Route::get('client/password/reset', 'Auth\ContactForgotPasswordController@showLinkRequestForm')->name('client.password.request');
Route::post('client/password/email', 'Auth\ContactForgotPasswordController@sendResetLinkEmail')->name('client.password.email');
Route::get('client/password/reset/{token}', 'Auth\ContactResetPasswordController@showResetForm')->name('client.password.reset');
Route::post('client/password/reset', 'Auth\ContactResetPasswordController@reset')->name('client.password.update');
Route::get('client/password/reset', 'Auth\ContactForgotPasswordController@showLinkRequestForm')->name('client.password.request')->middleware('locale');
Route::post('client/password/email', 'Auth\ContactForgotPasswordController@sendResetLinkEmail')->name('client.password.email')->middleware('locale');
Route::get('client/password/reset/{token}', 'Auth\ContactResetPasswordController@showResetForm')->name('client.password.reset')->middleware('locale');
Route::post('client/password/reset', 'Auth\ContactResetPasswordController@reset')->name('client.password.update')->middleware('locale');
//todo implement domain DB
Route::group(['middleware' => ['auth:contact'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit