Add better timezone support.

This commit is contained in:
James Cole
2024-11-09 12:19:01 +01:00
parent b7d8daf013
commit 0d11769590
14 changed files with 197 additions and 30 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace FireflyIII\Casts;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
/**
* Class SeparateTimezoneCaster
*
* Checks if the object has a separate _tz value. If it does, it will use that timezone to parse the date.
* If it is NULL, it will use the system's timezone.
*
* At some point a user's database consists entirely of UTC dates, and we won't need this anymore. However,
* the completeness of this migration is not yet guaranteed.
*/
class SeparateTimezoneCaster implements CastsAttributes
{
/**
* Cast the given value.
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): ?Carbon
{
if('' === $value || null === $value) {
return null;
}
$timeZone = $attributes[sprintf('%s_tz', $key)] ?? config('app.timezone');
return Carbon::parse($value, $timeZone)->setTimezone(config('app.timezone'));
}
/**
* Prepare the given value for storage.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return $value;
}
}

View File

@@ -58,26 +58,27 @@ class AddTimezonesToDates extends Command
*/
protected $description = 'Make sure all dates have a timezone.';
public static array $models = [
AccountBalance::class => ['date'], // done
AvailableBudget::class => ['start_date', 'end_date'], // done
Bill::class => ['date', 'end_date', 'extension_date'], // done
BudgetLimit::class => ['start_date', 'end_date'], // done
CurrencyExchangeRate::class => ['date'], // done
InvitedUser::class => ['expires'],
PiggyBankEvent::class => ['date'],
PiggyBankRepetition::class => ['startdate', 'targetdate'],
PiggyBank::class => ['startdate', 'targetdate'], // done
Recurrence::class => ['first_date', 'repeat_until', 'latest_date'],
Tag::class => ['date'],
TransactionJournal::class => ['date'],
];
/**
* Execute the console command.
*/
public function handle(): void
{
$models = [
AccountBalance::class => ['date'], // done
AvailableBudget::class => ['start_date', 'end_date'], // done
Bill::class => ['date', 'end_date', 'extension_date'], // done
BudgetLimit::class => ['start_date', 'end_date'], // done
CurrencyExchangeRate::class => ['date'], // done
InvitedUser::class => ['expires'],
PiggyBankEvent::class => ['date'],
PiggyBankRepetition::class => ['startdate', 'targetdate'],
PiggyBank::class => ['startdate', 'targetdate'], // done
Recurrence::class => ['first_date', 'repeat_until', 'latest_date'],
Tag::class => ['date'],
TransactionJournal::class => ['date'],
];
foreach ($models as $model => $fields) {
foreach (self::$models as $model => $fields) {
$this->addTimezoneToModel($model, $fields);
}
}

View File

@@ -0,0 +1,102 @@
<?php
/*
* ConvertDatesToUTC.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
namespace FireflyIII\Console\Commands\Integrity;
use Carbon\Carbon;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use Illuminate\Console\Command;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class ConvertDatesToUTC extends Command
{
use ShowsFriendlyMessages;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:convert-dates-to-utc';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Convert stored dates to UTC.';
/**
* Execute the console command.
*/
public function handle(): int
{
/**
* @var string $model
* @var array $fields
*/
foreach (AddTimezonesToDates::$models as $model => $fields) {
$this->ConvertModeltoUTC($model, $fields);
}
return Command::SUCCESS;
}
private function ConvertModeltoUTC(string $model, array $fields): void
{
/** @var string $field */
foreach ($fields as $field) {
$this->convertFieldtoUTC($model, $field);
}
}
private function convertFieldtoUTC(string $model, string $field): void {
$this->info(sprintf('Converting %s.%s to UTC', $model, $field));
$shortModel = str_replace('FireflyIII\Models\\', '', $model);
$timezoneField = sprintf('%s_tz', $field);
$items = new Collection();
$timeZone = config('app.timezone');
try {
$items = $model::where($timezoneField, $timeZone)->get();
} catch (QueryException $e) {
$this->friendlyError(sprintf('Cannot find timezone information to field "%s" of model "%s". Field does not exist.', $field, $shortModel));
Log::error($e->getMessage());
}
if (0 === $items->count()) {
$this->friendlyPositive(sprintf('All timezone information is UTC in field "%s" of model "%s".', $field, $shortModel));
return;
}
$this->friendlyInfo(sprintf('Converting field "%s" of model "%s" to UTC.', $field, $shortModel));
$items->each(
function ($item) use ($field, $timezoneField, $timeZone) {
/** @var Carbon $date */
$date = Carbon::parse($item->$field, $item->$timezoneField);
$date->setTimezone('UTC');
$item->$field = $date->format('Y-m-d H:i:s');
$item->$timezoneField = 'UTC';
$item->save();
}
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -16,6 +17,13 @@ class AccountBalance extends Model
use HasFactory;
protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz'];
protected function casts(): array
{
return [
'date' => SeparateTimezoneCaster::class,
];
}
public function account(): BelongsTo
{
return $this->belongsTo(Account::class);

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -49,9 +50,9 @@ class Bill extends Model
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'date' => 'date',
'end_date' => 'date',
'extension_date' => 'date',
'date' => SeparateTimezoneCaster::class,
'end_date' => SeparateTimezoneCaster::class,
'extension_date' => SeparateTimezoneCaster::class,
'skip' => 'int',
'automatch' => 'boolean',
'active' => 'boolean',

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Events\Model\BudgetLimit\Created;
use FireflyIII\Events\Model\BudgetLimit\Deleted;
use FireflyIII\Events\Model\BudgetLimit\Updated;
@@ -43,8 +44,8 @@ class BudgetLimit extends Model
= [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'start_date' => 'date',
'end_date' => 'date',
'start_date' => SeparateTimezoneCaster::class,
'end_date' => SeparateTimezoneCaster::class,
'auto_budget' => 'boolean',
];
protected $dispatchesEvents

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -47,7 +48,7 @@ class CurrencyExchangeRate extends Model
'user_id' => 'int',
'from_currency_id' => 'int',
'to_currency_id' => 'int',
'date' => 'datetime',
'date' => SeparateTimezoneCaster::class,
];
protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate'];

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -41,7 +42,7 @@ class InvitedUser extends Model
protected $casts
= [
'expires' => 'datetime',
'expires' => SeparateTimezoneCaster::class,
'redeemed' => 'boolean',
];
protected $fillable = ['user_id', 'email', 'invite_code', 'expires', 'expires_tz', 'redeemed'];

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -39,7 +40,7 @@ class PiggyBankEvent extends Model
= [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'date' => 'date',
'date' => SeparateTimezoneCaster::class,
];
protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount'];

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -41,8 +42,8 @@ class PiggyBankRepetition extends Model
= [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'startdate' => 'date',
'targetdate' => 'date',
'startdate' => SeparateTimezoneCaster::class,
'targetdate' => SeparateTimezoneCaster::class,
];
protected $fillable = ['piggy_bank_id', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'currentamount'];

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -51,9 +52,9 @@ class Recurrence extends Model
'title' => 'string',
'id' => 'int',
'description' => 'string',
'first_date' => 'date',
'repeat_until' => 'date',
'latest_date' => 'date',
'first_date' => SeparateTimezoneCaster::class,
'repeat_until' => SeparateTimezoneCaster::class,
'latest_date' => SeparateTimezoneCaster::class,
'repetitions' => 'int',
'active' => 'bool',
'apply_rules' => 'bool',

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -47,7 +48,7 @@ class Tag extends Model
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'date' => 'date',
'date' => SeparateTimezoneCaster::class,
'zoomLevel' => 'int',
'latitude' => 'float',
'longitude' => 'float',

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
@@ -55,7 +56,7 @@ class TransactionJournal extends Model
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'date' => 'datetime',
'date' => SeparateTimezoneCaster::class,
'interest_date' => 'date',
'book_date' => 'date',
'process_date' => 'date',

View File

@@ -75,6 +75,9 @@
<td style="width:30%;">{{ trans('list.date') }}</td>
<td>
{{ first.date.isoFormat(dateTimeFormat) }}
{% if(first.date_tz != '') %}
({{ first.date_tz }})
{% endif %}
</td>
</tr>
</tbody>