Merge pull request #354 from shikorism/feature/319-csv-import
チェックインデータのインポート
This commit is contained in:
@@ -47,6 +47,12 @@ class Ejaculation extends Model
|
||||
return $this->hasMany(Like::class);
|
||||
}
|
||||
|
||||
public function scopeOnlyWebCheckin(Builder $query)
|
||||
{
|
||||
return $query->where('ejaculations.source', null)
|
||||
->orWhere('ejaculations.source', '<>', Ejaculation::SOURCE_CSV);
|
||||
}
|
||||
|
||||
public function scopeWithLikes(Builder $query)
|
||||
{
|
||||
if (Auth::check()) {
|
||||
|
@@ -71,6 +71,7 @@ SQL
|
||||
->select('ejaculations.*')
|
||||
->with('user', 'tags')
|
||||
->withLikes()
|
||||
->onlyWebCheckin()
|
||||
->take(21)
|
||||
->get();
|
||||
|
||||
|
@@ -3,7 +3,10 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\DeactivatedUser;
|
||||
use App\Ejaculation;
|
||||
use App\Exceptions\CsvImportException;
|
||||
use App\Services\CheckinCsvExporter;
|
||||
use App\Services\CheckinCsvImporter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -72,6 +75,46 @@ class SettingController extends Controller
|
||||
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
|
||||
}
|
||||
|
||||
public function import()
|
||||
{
|
||||
return view('setting.import');
|
||||
}
|
||||
|
||||
public function storeImport(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'file' => 'required|file'
|
||||
], [], [
|
||||
'file' => 'ファイル'
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
if (!$file->isValid()) {
|
||||
return redirect()->route('setting.import')->withErrors(['file' => 'ファイルのアップロードに失敗しました。']);
|
||||
}
|
||||
|
||||
try {
|
||||
set_time_limit(0);
|
||||
|
||||
$importer = new CheckinCsvImporter(Auth::user(), $file->path());
|
||||
$imported = $importer->execute();
|
||||
|
||||
return redirect()->route('setting.import')->with('status', "{$imported}件のインポートに性交しました。");
|
||||
} catch (CsvImportException $e) {
|
||||
return redirect()->route('setting.import')->with('import_errors', $e->getErrors());
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyImport()
|
||||
{
|
||||
Auth::user()
|
||||
->ejaculations()
|
||||
->where('ejaculations.source', Ejaculation::SOURCE_CSV)
|
||||
->delete();
|
||||
|
||||
return redirect()->route('setting.import')->with('status', '削除が完了しました。');
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
return view('setting.export');
|
||||
|
@@ -19,6 +19,7 @@ class TimelineController extends Controller
|
||||
->select('ejaculations.*')
|
||||
->with('user', 'tags')
|
||||
->withLikes()
|
||||
->onlyWebCheckin()
|
||||
->paginate(21);
|
||||
|
||||
return view('timeline.public')->with(compact('ejaculations'));
|
||||
|
@@ -32,6 +32,7 @@ note,
|
||||
is_private,
|
||||
is_too_sensitive,
|
||||
link,
|
||||
source,
|
||||
to_char(lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC), 'YYYY/MM/DD HH24:MI') AS before_date,
|
||||
to_char(ejaculated_date - (lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)), 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
|
||||
SQL
|
||||
@@ -154,6 +155,7 @@ note,
|
||||
is_private,
|
||||
is_too_sensitive,
|
||||
link,
|
||||
source,
|
||||
to_char(lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC), 'YYYY/MM/DD HH24:MI') AS before_date,
|
||||
to_char(ejaculated_date - (lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)), 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
|
||||
SQL
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
@@ -8,12 +9,17 @@ use App\Rules\CsvDateTime;
|
||||
use App\Tag;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use League\Csv\Reader;
|
||||
use Throwable;
|
||||
|
||||
class CheckinCsvImporter
|
||||
{
|
||||
/** @var int 取り込み件数の上限 */
|
||||
private const IMPORT_LIMIT = 5000;
|
||||
|
||||
/** @var User Target user */
|
||||
private $user;
|
||||
/** @var string CSV filename */
|
||||
@@ -25,7 +31,11 @@ class CheckinCsvImporter
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
public function execute()
|
||||
/**
|
||||
* インポート処理を実行します。
|
||||
* @return int 取り込んだ件数
|
||||
*/
|
||||
public function execute(): int
|
||||
{
|
||||
// Guess charset
|
||||
$charset = $this->guessCharset($this->filename);
|
||||
@@ -38,7 +48,8 @@ class CheckinCsvImporter
|
||||
}
|
||||
|
||||
// Import
|
||||
DB::transaction(function () use ($csv) {
|
||||
return DB::transaction(function () use ($csv) {
|
||||
$alreadyImportedCount = $this->user->ejaculations()->where('ejaculations.source', Ejaculation::SOURCE_CSV)->count();
|
||||
$errors = [];
|
||||
|
||||
if (!in_array('日時', $csv->getHeader(), true)) {
|
||||
@@ -49,8 +60,15 @@ class CheckinCsvImporter
|
||||
throw new CsvImportException(...$errors);
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
foreach ($csv->getRecords() as $offset => $record) {
|
||||
$line = $offset + 1;
|
||||
if (self::IMPORT_LIMIT <= $alreadyImportedCount + $imported) {
|
||||
$limit = self::IMPORT_LIMIT;
|
||||
$errors[] = "{$line} 行 : インポート機能で取り込めるデータは{$limit}件までに制限されています。これ以上取り込みできません。";
|
||||
throw new CsvImportException(...$errors);
|
||||
}
|
||||
|
||||
$ejaculation = new Ejaculation(['user_id' => $this->user->id]);
|
||||
|
||||
$validator = Validator::make($record, [
|
||||
@@ -78,15 +96,32 @@ class CheckinCsvImporter
|
||||
continue;
|
||||
}
|
||||
|
||||
$ejaculation->save();
|
||||
if (!empty($tags)) {
|
||||
$ejaculation->tags()->sync(collect($tags)->pluck('id'));
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$ejaculation->save();
|
||||
if (!empty($tags)) {
|
||||
$ejaculation->tags()->sync(collect($tags)->pluck('id'));
|
||||
}
|
||||
DB::commit();
|
||||
$imported++;
|
||||
} catch (QueryException $e) {
|
||||
DB::rollBack();
|
||||
if ($e->errorInfo[0] === '23505') {
|
||||
$errors[] = "{$line} 行 : すでにこの日時のチェックインデータが存在します。";
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new CsvImportException(...$errors);
|
||||
}
|
||||
|
||||
return $imported;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,6 +197,9 @@ class CheckinCsvImporter
|
||||
if (strpos($tag, "\n") !== false) {
|
||||
throw new CsvImportException("{$line} 行 : {$column}に改行を含めることはできません。");
|
||||
}
|
||||
if (strpos($tag, ' ') !== false) {
|
||||
throw new CsvImportException("{$line} 行 : {$column}にスペースを含めることはできません。");
|
||||
}
|
||||
|
||||
$tags[] = Tag::firstOrCreate(['name' => $tag]);
|
||||
if (count($tags) >= 32) {
|
||||
|
@@ -92,4 +92,43 @@ class Formatter
|
||||
|
||||
return implode(',', $srcset);
|
||||
}
|
||||
|
||||
/**
|
||||
* php.ini書式のデータサイズを正規化します。
|
||||
* @param mixed $val データサイズ
|
||||
* @return string
|
||||
*/
|
||||
public function normalizeIniBytes($val)
|
||||
{
|
||||
$val = trim($val);
|
||||
$last = strtolower(substr($val, -1, 1));
|
||||
if (ord($last) < 0x30 || ord($last) > 0x39) {
|
||||
$bytes = substr($val, 0, -1);
|
||||
switch ($last) {
|
||||
case 'g':
|
||||
$bytes *= 1024;
|
||||
// fall through
|
||||
// no break
|
||||
case 'm':
|
||||
$bytes *= 1024;
|
||||
// fall through
|
||||
// no break
|
||||
case 'k':
|
||||
$bytes *= 1024;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$bytes = $val;
|
||||
}
|
||||
|
||||
if ($bytes >= (1 << 30)) {
|
||||
return ($bytes >> 30) . 'GB';
|
||||
} elseif ($bytes >= (1 << 20)) {
|
||||
return ($bytes >> 20) . 'MB';
|
||||
} elseif ($bytes >= (1 << 10)) {
|
||||
return ($bytes >> 10) . 'KB';
|
||||
}
|
||||
|
||||
return $bytes . 'B';
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user