Skip to content

Commit 187fc37

Browse files
committed
File attachment at Book level #6042
1 parent ec3dd85 commit 187fc37

File tree

11 files changed

+388
-67
lines changed

11 files changed

+388
-67
lines changed

app/Entities/Models/Book.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\Entities\Tools\EntityCover;
66
use BookStack\Entities\Tools\EntityDefaultTemplate;
77
use BookStack\Sorting\SortRule;
8+
use BookStack\Uploads\Attachment;
89
use BookStack\Uploads\Image;
910
use Illuminate\Database\Eloquent\Factories\HasFactory;
1011
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -24,6 +25,7 @@
2425
* @property \Illuminate\Database\Eloquent\Collection $pages
2526
* @property \Illuminate\Database\Eloquent\Collection $directPages
2627
* @property \Illuminate\Database\Eloquent\Collection $shelves
28+
* @property \Illuminate\Database\Eloquent\Collection $attachments
2729
* @property ?SortRule $sortRule
2830
*/
2931
class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface
@@ -78,6 +80,16 @@ public function shelves(): BelongsToMany
7880
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
7981
}
8082

83+
/**
84+
* Get the attachments assigned to this book.
85+
*/
86+
public function attachments(): HasMany
87+
{
88+
return $this->hasMany(Attachment::class, 'attachable_id')
89+
->where('attachments.attachable_type', 'BookStack\\Entities\\Models\\Book')
90+
->orderBy('order', 'asc');
91+
}
92+
8193
/**
8294
* Get the direct child items within this book.
8395
*/

app/Uploads/Attachment.php

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\App\Model;
66
use BookStack\Entities\Models\Entity;
77
use BookStack\Entities\Models\Page;
8+
use BookStack\Entities\Models\Book;
89
use BookStack\Permissions\Models\JointPermission;
910
use BookStack\Permissions\PermissionApplicator;
1011
use BookStack\Users\Models\HasCreatorAndUpdater;
@@ -13,6 +14,7 @@
1314
use Illuminate\Database\Eloquent\Builder;
1415
use Illuminate\Database\Eloquent\Factories\HasFactory;
1516
use Illuminate\Database\Eloquent\Relations\BelongsTo;
17+
use Illuminate\Database\Eloquent\Relations\MorphTo;
1618
use Illuminate\Database\Eloquent\Relations\HasMany;
1719

1820
/**
@@ -21,6 +23,10 @@
2123
* @property string $path
2224
* @property string $extension
2325
* @property ?Page $page
26+
* @property ?Book $book
27+
* @property ?Entity $attachable
28+
* @property string $attachable_type
29+
* @property int $attachable_id
2430
* @property bool $external
2531
* @property int $uploaded_to
2632
* @property User $updatedBy
@@ -52,17 +58,40 @@ public function getFileName(): string
5258
}
5359

5460
/**
55-
* Get the page this file was uploaded to.
61+
* Get the polymorphic entity this file was uploaded to (page or book).
62+
*/
63+
public function attachable(): MorphTo
64+
{
65+
return $this->morphTo('attachable');
66+
}
67+
68+
/**
69+
* Get the page this file was uploaded to (backward compatibility).
5670
*/
5771
public function page(): BelongsTo
5872
{
5973
return $this->belongsTo(Page::class, 'uploaded_to');
6074
}
6175

76+
/**
77+
* Get the book this file was uploaded to.
78+
* Returns null if the attachment is not attached to a book.
79+
*/
80+
public function book(): ?Book
81+
{
82+
if ($this->attachable_type === 'BookStack\\Entities\\Models\\Book') {
83+
return $this->attachable;
84+
}
85+
return null;
86+
}
87+
6288
public function jointPermissions(): HasMany
6389
{
64-
return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to')
65-
->where('joint_permissions.entity_type', '=', 'page');
90+
return $this->hasMany(JointPermission::class, 'entity_id', 'attachable_id')
91+
->where(function($query) {
92+
$query->where('joint_permissions.entity_type', '=', 'page')
93+
->orWhere('joint_permissions.entity_type', '=', 'book');
94+
});
6695
}
6796

6897
/**
@@ -109,16 +138,18 @@ public function markdownLink(): string
109138
}
110139

111140
/**
112-
* Scope the query to those attachments that are visible based upon related page permissions.
141+
* Scope the query to those attachments that are visible based upon related entity permissions.
113142
*/
114143
public function scopeVisible(): Builder
115144
{
116145
$permissions = app()->make(PermissionApplicator::class);
117146

118-
return $permissions->restrictPageRelationQuery(
147+
// Use polymorphic relationship restriction for both pages and books
148+
return $permissions->restrictEntityRelationQuery(
119149
self::query(),
120150
'attachments',
121-
'uploaded_to'
151+
'attachable_id',
152+
'attachable_type'
122153
);
123154
}
124155
}

app/Uploads/AttachmentService.php

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,33 @@ public function getAttachmentFileSize(Attachment $attachment): int
3636
*
3737
* @throws FileUploadException
3838
*/
39-
public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment
39+
public function saveNewUpload(UploadedFile $uploadedFile, int $entityId, string $entityType = 'page'): Attachment
4040
{
4141
$attachmentName = $uploadedFile->getClientOriginalName();
4242
$attachmentPath = $this->putFileInStorage($uploadedFile);
43-
$largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $pageId)->max('order');
43+
44+
// Determine the model class
45+
$modelClass = $entityType === 'book'
46+
? 'BookStack\\Entities\\Models\\Book'
47+
: 'BookStack\\Entities\\Models\\Page';
48+
49+
// Get max order for the entity using polymorphic relationship
50+
$largestExistingOrder = Attachment::query()
51+
->where('attachable_type', $modelClass)
52+
->where('attachable_id', $entityId)
53+
->max('order') ?? 0;
4454

4555
/** @var Attachment $attachment */
4656
$attachment = Attachment::query()->forceCreate([
47-
'name' => $attachmentName,
48-
'path' => $attachmentPath,
49-
'extension' => $uploadedFile->getClientOriginalExtension(),
50-
'uploaded_to' => $pageId,
51-
'created_by' => user()->id,
52-
'updated_by' => user()->id,
53-
'order' => $largestExistingOrder + 1,
57+
'name' => $attachmentName,
58+
'path' => $attachmentPath,
59+
'extension' => $uploadedFile->getClientOriginalExtension(),
60+
'uploaded_to' => $entityType === 'page' ? $entityId : null, // Backward compatibility
61+
'attachable_type' => $modelClass,
62+
'attachable_id' => $entityId,
63+
'created_by' => user()->id,
64+
'updated_by' => user()->id,
65+
'order' => $largestExistingOrder + 1,
5466
]);
5567

5668
return $attachment;
@@ -83,19 +95,30 @@ public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attach
8395
/**
8496
* Save a new File attachment from a given link and name.
8597
*/
86-
public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
98+
public function saveNewFromLink(string $name, string $link, int $entityId, string $entityType = 'page'): Attachment
8799
{
88-
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
100+
// Determine the model class
101+
$modelClass = $entityType === 'book'
102+
? 'BookStack\\Entities\\Models\\Book'
103+
: 'BookStack\\Entities\\Models\\Page';
104+
105+
// Get max order for the entity using polymorphic relationship
106+
$largestExistingOrder = Attachment::query()
107+
->where('attachable_type', $modelClass)
108+
->where('attachable_id', $entityId)
109+
->max('order') ?? 0;
89110

90111
return Attachment::forceCreate([
91-
'name' => $name,
92-
'path' => $link,
93-
'external' => true,
94-
'extension' => '',
95-
'uploaded_to' => $page_id,
96-
'created_by' => user()->id,
97-
'updated_by' => user()->id,
98-
'order' => $largestExistingOrder + 1,
112+
'name' => $name,
113+
'path' => $link,
114+
'external' => true,
115+
'extension' => '',
116+
'uploaded_to' => $entityType === 'page' ? $entityId : null, // Backward compatibility
117+
'attachable_type' => $modelClass,
118+
'attachable_id' => $entityId,
119+
'created_by' => user()->id,
120+
'updated_by' => user()->id,
121+
'order' => $largestExistingOrder + 1,
99122
]);
100123
}
101124

@@ -104,8 +127,26 @@ public function saveNewFromLink(string $name, string $link, int $page_id): Attac
104127
*/
105128
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
106129
{
130+
$modelClass = 'BookStack\\Entities\\Models\\Page';
107131
foreach ($attachmentOrder as $index => $attachmentId) {
108-
Attachment::query()->where('uploaded_to', '=', $pageId)
132+
Attachment::query()
133+
->where('attachable_type', $modelClass)
134+
->where('attachable_id', '=', $pageId)
135+
->where('id', '=', $attachmentId)
136+
->update(['order' => $index]);
137+
}
138+
}
139+
140+
/**
141+
* Updates the ordering for a listing of attached files for a book.
142+
*/
143+
public function updateFileOrderWithinBook(array $attachmentOrder, string $bookId)
144+
{
145+
$modelClass = 'BookStack\\Entities\\Models\\Book';
146+
foreach ($attachmentOrder as $index => $attachmentId) {
147+
Attachment::query()
148+
->where('attachable_type', $modelClass)
149+
->where('attachable_id', '=', $bookId)
109150
->where('id', '=', $attachmentId)
110151
->update(['order' => $index]);
111152
}

0 commit comments

Comments
 (0)