Skip to content

Commit b49caf4

Browse files
authored
Merge pull request #1622 from shikorism/develop
Release 2025.11.1
2 parents 9795d64 + 5017bd8 commit b49caf4

File tree

5 files changed

+191
-0
lines changed

5 files changed

+191
-0
lines changed

app/Http/Controllers/SettingController.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Mail\PasswordChanged;
1010
use App\Services\CheckinCsvExporter;
1111
use App\Services\CheckinCsvImporter;
12+
use App\Services\LikedOkazuCsvExporter;
1213
use App\User;
1314
use Illuminate\Http\Request;
1415
use Illuminate\Support\Facades\Auth;
@@ -196,6 +197,34 @@ public function exportToCsv(Request $request)
196197
->deleteFileAfterSend(true);
197198
}
198199

200+
public function exportLikes(Request $request)
201+
{
202+
$validated = $request->validate([
203+
'charset' => ['required', Rule::in(['utf8', 'sjis'])]
204+
]);
205+
206+
$charsets = [
207+
'utf8' => 'UTF-8',
208+
'sjis' => 'SJIS-win'
209+
];
210+
211+
$filename = tempnam(sys_get_temp_dir(), 'tissue_export_tmp_');
212+
try {
213+
// 気休め
214+
set_time_limit(0);
215+
216+
$exporter = new LikedOkazuCsvExporter(Auth::user(), $filename, $charsets[$validated['charset']]);
217+
$exporter->execute();
218+
} catch (\Throwable $e) {
219+
unlink($filename);
220+
throw $e;
221+
}
222+
223+
return response()
224+
->download($filename, 'TissueLikes_' . date('Y-m-d_H-i-s') . '.csv')
225+
->deleteFileAfterSend(true);
226+
}
227+
199228
public function deactivate()
200229
{
201230
return view('setting.deactivate');
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use App\User;
6+
use Illuminate\Support\Facades\DB;
7+
use League\Csv\Writer;
8+
9+
class LikedOkazuCsvExporter
10+
{
11+
/** @var User Target user */
12+
private $user;
13+
/** @var string Output filename */
14+
private $filename;
15+
/** @var string Output charset */
16+
private $charset;
17+
18+
public function __construct(User $user, string $filename, string $charset)
19+
{
20+
$this->user = $user;
21+
$this->filename = $filename;
22+
$this->charset = $charset;
23+
}
24+
25+
public function execute()
26+
{
27+
$csv = Writer::createFromPath($this->filename, 'wb');
28+
$csv->setEndOfLine("\r\n");
29+
if ($this->charset === 'SJIS-win') {
30+
$csv->appendStreamFilterOnWrite('convert.mbstring.encoding.UTF-8:SJIS-win');
31+
}
32+
33+
$header = ['いいねした日時', 'チェックインURL', 'チェックインユーザーID', 'オカズリンク'];
34+
$csv->insertOne($header);
35+
36+
DB::transaction(function () use ($csv) {
37+
$query = $this->user->likes()
38+
->select('likes.*')
39+
->orderBy('likes.id')
40+
->with([
41+
'ejaculation' => function ($query) {
42+
$query->with('user', 'tags');
43+
}
44+
])
45+
->join('ejaculations', 'likes.ejaculation_id', '=', 'ejaculations.id')
46+
->join('users', 'ejaculations.user_id', '=', 'users.id')
47+
->where(function ($query) {
48+
$query->where(function ($query) {
49+
$query->where('ejaculations.user_id', $this->user->id)
50+
->orWhere(function ($query) {
51+
$query->where('ejaculations.is_private', false)->where('users.is_protected', false);
52+
});
53+
});
54+
});
55+
$query->chunk(1000, function ($likes) use ($csv) {
56+
foreach ($likes as $like) {
57+
$record = [
58+
$like->created_at->format('Y/m/d H:i:s'),
59+
route('checkin.show', ['id' => $like->ejaculation->id]),
60+
$like->ejaculation->user->name,
61+
$like->ejaculation->link,
62+
];
63+
$csv->insertOne($record);
64+
}
65+
});
66+
});
67+
}
68+
}

resources/views/setting/export.blade.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@
2121
</div>
2222
<button type="submit" class="btn btn-primary mt-3">ダウンロード</button>
2323
</form>
24+
25+
@unless (config('app.protected_only_mode'))
26+
<h3 class="mt-5">いいね履歴のエクスポート</h3>
27+
<hr>
28+
<p>いいねしたオカズの一覧をCSVファイルとしてダウンロードすることができます。</p>
29+
<p>本機能は、いいね履歴の公開終了まで期間限定で提供されます。</p>
30+
<form class="mt-4" action="{{ route('setting.export.likes') }}" method="get">
31+
{{ csrf_field() }}
32+
<div class="mb-2"><strong>文字コード</strong></div>
33+
<div class="form-group">
34+
<div class="custom-control custom-radio custom-control-inline">
35+
<input id="LcharsetUTF8" class="custom-control-input" type="radio" name="charset" value="utf8" checked>
36+
<label for="LcharsetUTF8" class="custom-control-label">UTF-8</label>
37+
</div>
38+
<div class="custom-control custom-radio custom-control-inline ml-3">
39+
<input id="LcharsetSJIS" class="custom-control-input" type="radio" name="charset" value="sjis">
40+
<label for="LcharsetSJIS" class="custom-control-label">Shift_JIS</label>
41+
</div>
42+
</div>
43+
<button type="submit" class="btn btn-primary mt-3">ダウンロード</button>
44+
</form>
45+
@endunless
2446
@endsection
2547

2648
@push('script')

routes/web.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
Route::delete('/setting/import', 'SettingController@destroyImport')->name('setting.import.destroy');
5858
Route::get('/setting/export', 'SettingController@export')->name('setting.export');
5959
Route::get('/setting/export/csv', 'SettingController@exportToCsv')->name('setting.export.csv');
60+
if (!config('app.protected_only_mode')) {
61+
Route::get('/setting/export/likes', 'SettingController@exportLikes')->name('setting.export.likes');
62+
}
6063
Route::get('/setting/deactivate', 'SettingController@deactivate')->name('setting.deactivate');
6164
Route::post('/setting/deactivate', 'SettingController@destroyUser')->name('setting.deactivate.destroy');
6265
Route::get('/setting/password', 'SettingController@password')->name('setting.password');
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Tests\Unit\Services;
5+
6+
use App\Ejaculation;
7+
use App\Services\LikedOkazuCsvExporter;
8+
use App\User;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use League\Csv\Reader;
11+
use Tests\TestCase;
12+
13+
class LikedOkazuCsvExporterTest extends TestCase
14+
{
15+
use RefreshDatabase;
16+
17+
private string $tmpFile;
18+
19+
protected function setUp(): void
20+
{
21+
parent::setUp();
22+
$this->seed();
23+
$this->tmpFile = tempnam(sys_get_temp_dir(), 'tissue_csv_export_test_') . '.csv';
24+
}
25+
26+
protected function tearDown(): void
27+
{
28+
if (file_exists($this->tmpFile)) {
29+
unlink($this->tmpFile);
30+
}
31+
parent::tearDown();
32+
}
33+
34+
public function testExportCsvWithValidData()
35+
{
36+
$user = User::factory()->create();
37+
$ownEjaculation1 = Ejaculation::factory()->create(['user_id' => $user->id, 'is_private' => false]);
38+
$ownEjaculation2 = Ejaculation::factory()->create(['user_id' => $user->id, 'is_private' => true]);
39+
40+
$anotherUser = User::factory()->create();
41+
$anotherUserEjaculation1 = Ejaculation::factory()->create(['user_id' => $anotherUser->id, 'is_private' => false]);
42+
$anotherUserEjaculation2 = Ejaculation::factory()->create(['user_id' => $anotherUser->id, 'is_private' => true]);
43+
44+
$protectedUser = User::factory()->protected()->create();
45+
$protectedUserEjaculation = Ejaculation::factory()->create(['user_id' => $protectedUser->id]);
46+
47+
$user->likes()->createMany([
48+
['ejaculation_id' => $ownEjaculation1->id],
49+
['ejaculation_id' => $ownEjaculation2->id],
50+
['ejaculation_id' => $anotherUserEjaculation1->id],
51+
['ejaculation_id' => $anotherUserEjaculation2->id],
52+
['ejaculation_id' => $protectedUserEjaculation->id],
53+
]);
54+
55+
$exporter = new LikedOkazuCsvExporter($user, $this->tmpFile, 'UTF-8');
56+
$exporter->execute();
57+
58+
$csv = Reader::createFromPath($this->tmpFile, 'r');
59+
$csv->setHeaderOffset(0);
60+
61+
$records = iterator_to_array($csv->getRecords(), false);
62+
$this->assertCount(3, $records);
63+
64+
// only includes public checkin or user's own checkin
65+
$this->assertStringEndsWith(route('checkin.show', ['id' => $ownEjaculation1->id]), $records[0]['チェックインURL']);
66+
$this->assertStringEndsWith(route('checkin.show', ['id' => $ownEjaculation2->id]), $records[1]['チェックインURL']);
67+
$this->assertStringEndsWith(route('checkin.show', ['id' => $anotherUserEjaculation1->id]), $records[2]['チェックインURL']);
68+
}
69+
}

0 commit comments

Comments
 (0)