Multiselect in mediafiles

Allows multiselect on mediafiles using the new folder structure
Add bulk delete on server
Add movement logic and path view
This commit is contained in:
Sean Engelhardt 2019-08-09 10:08:45 +02:00
parent f25a8aefb2
commit a97ca18c36
7 changed files with 241 additions and 97 deletions

View File

@ -135,4 +135,15 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
directory_id: directoryId directory_id: directoryId
}); });
} }
/**
* Deletes many files.
*
* @param mediafiles The users to delete
*/
public async bulkDelete(mediafiles: ViewMediafile[]): Promise<void> {
await this.httpService.post('/rest/mediafiles/mediafile/bulk_delete/', {
ids: mediafiles.map(mediafile => mediafile.id)
});
}
} }

View File

@ -11,7 +11,7 @@
type="button" type="button"
mat-icon-button mat-icon-button
(click)="openUploadDialog(uploadDialog)" (click)="openUploadDialog(uploadDialog)"
*osPerms="'mediafiles.can_upload'" *osPerms="'mediafiles.can_manage'"
> >
<mat-icon>cloud_upload</mat-icon> <mat-icon>cloud_upload</mat-icon>
</button> </button>

View File

@ -1,4 +1,4 @@
<os-head-bar [mainButton]="canUploadFiles" [multiSelectMode]="false" (mainEvent)="onMainEvent()"> <os-head-bar [mainButton]="canEdit" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2 translate>Files</h2> <h2 translate>Files</h2>
@ -6,35 +6,40 @@
<!-- Menu --> <!-- Menu -->
<div class="menu-slot" *osPerms="'mediafiles.can_manage'"> <div class="menu-slot" *osPerms="'mediafiles.can_manage'">
<button type="button" mat-icon-button (click)="createNewFolder(newFolderDialog)"> <button type="button" mat-icon-button (click)="createNewFolder(newFolderDialog)" *ngIf="!isMultiSelect">
<mat-icon>create_new_folder</mat-icon> <mat-icon>create_new_folder</mat-icon>
</button> </button>
<!--<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu"> <button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button>--> </button>
</div> </div>
<!-- Multiselect info --> <!-- Multiselect info -->
<!--<div *ngIf="this.isMultiSelect" class="central-info-slot"> <div *ngIf="isMultiSelect" class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button> <button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span> <span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>--> </div>
</os-head-bar> </os-head-bar>
<!-- Folder navigation bar --> <!-- Folder navigation bar -->
<div> <div>
<div class="custom-table-header folder-nav-bar"> <div class="custom-table-header folder-nav-bar">
<button class="folder" mat-button (click)="changeDirectory(null)"> <button class="folder" mat-button (click)="changeDirectory(null)" [disabled]="isMultiSelect">
<mat-icon class="file-icon">home</mat-icon> <mat-icon class="file-icon">home</mat-icon>
</button> </button>
<span *ngFor="let directory of directoryChain; let last = last"> <span *ngFor="let directory of directoryChain; let last = last">
<div class="arrow"> <div class="arrow">
<mat-icon>chevron_right</mat-icon> <mat-icon>chevron_right</mat-icon>
</div> </div>
<button
<button class="folder" mat-button (click)="changeDirectory(directory.id)" *ngIf="!last"> class="folder"
mat-button
(click)="changeDirectory(directory.id)"
[disabled]="isMultiSelect"
*ngIf="!last"
>
<span class="folder-text"> <span class="folder-text">
{{ directory.title }} {{ directory.filename }}
</span> </span>
</button> </button>
<button <button
@ -42,14 +47,15 @@
mat-button mat-button
[matMenuTriggerFor]="singleMediafileMenu" [matMenuTriggerFor]="singleMediafileMenu"
[matMenuTriggerData]="{ mediafile: directory }" [matMenuTriggerData]="{ mediafile: directory }"
[disabled]="isMultiSelect"
*ngIf="last && showFileMenu(directory)" *ngIf="last && showFileMenu(directory)"
> >
<os-icon-container icon="arrow_drop_down" swap="true" size="large"> <os-icon-container icon="arrow_drop_down" swap="true" size="large">
{{ directory.title }} {{ directory.filename }}
</os-icon-container> </os-icon-container>
</button> </button>
<span class="folder fake-folder folder-text" *ngIf="last && !showFileMenu(directory)"> <span class="folder fake-folder folder-text" *ngIf="last && !showFileMenu(directory)">
{{ directory.title }} {{ directory.filename }}
</span> </span>
</span> </span>
<span class="visibility" *ngIf="canEdit && directory && directory.has_inherited_access_groups"> <span class="visibility" *ngIf="canEdit && directory && directory.has_inherited_access_groups">
@ -83,23 +89,29 @@
showHeader="false" showHeader="false"
vScrollAuto vScrollAuto
[dataSource]="dataSource" [dataSource]="dataSource"
matCheckboxSelection="selection"
[columns]="columnSet" [columns]="columnSet"
[hideColumns]="hiddenColumns" [hideColumns]="hiddenColumns"
(rowClick)="onSelectRow($event)"
> >
<!-- Icon column --> <!-- Icon column -->
<div *pblNgridCellDef="'icon'; row as mediafile" class="fill clickable"> <div *pblNgridCellDef="'icon'; row as mediafile" class="fill clickable">
<a class="detail-link" target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file"> </a> <a class="detail-link" target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file && !isMultiSelect">
<a class="detail-link" (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory"> </a> </a>
<a class="detail-link" (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory && !isMultiSelect">
</a>
<mat-icon class="file-icon">{{ mediafile.getIcon() }}</mat-icon> <mat-icon class="file-icon">{{ mediafile.getIcon() }}</mat-icon>
</div> </div>
<!-- Title column --> <!-- Title column -->
<div *pblNgridCellDef="'title'; row as mediafile" class="fill clickable"> <div *pblNgridCellDef="'title'; row as mediafile" class="fill clickable">
<a class="detail-link" target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file"> </a> <a class="detail-link" target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file && !isMultiSelect">
<a class="detail-link" (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory"> </a> </a>
<a class="detail-link" (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory && !isMultiSelect">
</a>
<div class="innerTable"> <div class="innerTable">
<div class="file-title ellipsis-overflow"> <div class="file-title ellipsis-overflow">
{{ mediafile.title }} {{ mediafile.filename }}
</div> </div>
<div class="info-text" *ngIf="mediafile.is_file"> <div class="info-text" *ngIf="mediafile.is_file">
<span> {{ getDateFromTimestamp(mediafile.timestamp) }} · {{ mediafile.size }} </span> <span> {{ getDateFromTimestamp(mediafile.timestamp) }} · {{ mediafile.size }} </span>
@ -135,6 +147,7 @@
mat-icon-button mat-icon-button
[matMenuTriggerFor]="singleMediafileMenu" [matMenuTriggerFor]="singleMediafileMenu"
[matMenuTriggerData]="{ mediafile: mediafile }" [matMenuTriggerData]="{ mediafile: mediafile }"
[disabled]="isMultiSelect"
*ngIf="showFileMenu(mediafile)" *ngIf="showFileMenu(mediafile)"
> >
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
@ -191,7 +204,7 @@
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
<span translate>Edit</span> <span translate>Edit</span>
</button> </button>
<button mat-menu-item (click)="move(moveDialog, mediafile)"> <button mat-menu-item (click)="move(moveDialog, [mediafile])">
<mat-icon>near_me</mat-icon> <mat-icon>near_me</mat-icon>
<span translate>Move</span> <span translate>Move</span>
</button> </button>
@ -204,7 +217,7 @@
</mat-menu> </mat-menu>
<!-- Menu for Mediafiles --> <!-- Menu for Mediafiles -->
<!--<mat-menu #mediafilesMenu="matMenu"> <mat-menu #mediafilesMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()"> <button mat-menu-item *osPerms="'mediafiles.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
@ -212,6 +225,10 @@
</button> </button>
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<button mat-menu-item [disabled]="!selectedRows.length" (click)="move(moveDialog, selectedRows)">
<mat-icon>near_me</mat-icon>
<span translate>Move</span>
</button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
@ -228,11 +245,11 @@
[disabled]="!selectedRows.length" [disabled]="!selectedRows.length"
(click)="deleteSelected()" (click)="deleteSelected()"
> >
<mat-icon>delete</mat-icon> <mat-icon color="warn">delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
</button> </button>
</div> </div>
</mat-menu>--> </mat-menu>
<!-- File edit dialog --> <!-- File edit dialog -->
<ng-template #fileEditDialog> <ng-template #fileEditDialog>
@ -310,7 +327,7 @@
<h1 mat-dialog-title> <h1 mat-dialog-title>
<span translate>Move into directory</span> <span translate>Move into directory</span>
</h1> </h1>
<div mat-dialog-content> <div class="os-form-card-mobile" mat-dialog-content>
<p translate>Please select the directory:</p> <p translate>Please select the directory:</p>
<os-search-value-selector <os-search-value-selector
ngDefaultControl ngDefaultControl
@ -318,7 +335,7 @@
[includeNone]="true" [includeNone]="true"
[noneTitle]="'Base folder'" [noneTitle]="'Base folder'"
listname="{{ 'Parent directory' | translate }}" listname="{{ 'Parent directory' | translate }}"
[InputListValues]="directoryBehaviorSubject" [InputListValues]="filteredDirectoryBehaviorSubject"
></os-search-value-selector> ></os-search-value-selector>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>

View File

@ -15,8 +15,9 @@ import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { columnFactory, createDS, PblDataSource } from '@pebula/ngrid'; import { columnFactory, createDS, PblColumnDefinition } from '@pebula/ngrid';
import { BehaviorSubject, Subscription } from 'rxjs'; import { PblNgridDataMatrixRow } from '@pebula/ngrid/target-events';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service'; import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
@ -26,7 +27,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings'; import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseListViewComponent } from 'app/site/base/base-list-view';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewGroup } from 'app/site/users/models/view-group';
import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service'; import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service';
@ -41,12 +42,7 @@ import { MediafilesSortListService } from '../../services/mediafiles-sort-list.s
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class MediafileListComponent extends BaseViewComponent implements OnInit, OnDestroy { export class MediafileListComponent extends BaseListViewComponent<ViewMediafile> implements OnInit, OnDestroy {
/**
* Data source for the files
*/
public dataSource: PblDataSource<ViewMediafile>;
/** /**
* Holds the actions for logos. Updated via an observable * Holds the actions for logos. Updated via an observable
*/ */
@ -65,15 +61,11 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
public newDirectoryForm: FormGroup; public newDirectoryForm: FormGroup;
public moveForm: FormGroup; public moveForm: FormGroup;
public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>; public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>;
public filteredDirectoryBehaviorSubject: BehaviorSubject<ViewMediafile[]> = new BehaviorSubject<ViewMediafile[]>(
[]
);
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>; public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
/**
* @returns true if the user can manage media files
*/
public get canUploadFiles(): boolean {
return this.operator.hasPerms('mediafiles.can_see') && this.operator.hasPerms('mediafiles.can_upload');
}
/** /**
* @return true if the user can manage media files * @return true if the user can manage media files
*/ */
@ -113,6 +105,10 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
hidden.push('info'); hidden.push('info');
} }
if (!this.isMultiSelect) {
hidden.push('selection');
}
if (!this.canAccessFileMenu) { if (!this.canAccessFileMenu) {
hidden.push('menu'); hidden.push('menu');
} }
@ -121,10 +117,13 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
} }
/** /**
* Create the column set * Define the column definition
*/ */
public columnSet = columnFactory() public tableColumnDefinition: PblColumnDefinition[] = [
.table( {
prop: 'selection',
width: '40px'
},
{ {
prop: 'icon', prop: 'icon',
label: '', label: '',
@ -150,15 +149,22 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
label: '', label: '',
width: '40px' width: '40px'
} }
) ];
/**
* Create the column set
*/
public columnSet = columnFactory()
.table(...this.tableColumnDefinition)
.build(); .build();
public isMultiselect = false; // TODO
private folderSubscription: Subscription; private folderSubscription: Subscription;
private directorySubscription: Subscription; private directorySubscription: Subscription;
public directory: ViewMediafile | null; public directory: ViewMediafile | null;
public directoryChain: ViewMediafile[]; public directoryChain: ViewMediafile[];
private directoryObservable: Observable<ViewMediafile[]> = new Observable();
/** /**
* Constructs the component * Constructs the component
* *
@ -194,6 +200,7 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
this.canMultiSelect = true;
this.newDirectoryForm = this.formBuilder.group({ this.newDirectoryForm = this.formBuilder.group({
title: ['', Validators.required], title: ['', Validators.required],
@ -226,6 +233,8 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
this.mediaManage.getFontActions().subscribe(action => { this.mediaManage.getFontActions().subscribe(action => {
this.fontActions = action; this.fontActions = action;
}); });
this.createDataSource();
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
@ -250,25 +259,38 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
return new Date(timestamp).toLocaleString(this.translate.currentLang); return new Date(timestamp).toLocaleString(this.translate.currentLang);
} }
/**
* TODO: Swap logic to only create DS once and update on filder change
* @param mediafiles
*/
private createDataSource(): void {
this.dataSource = createDS<ViewMediafile>()
.onTrigger(() => this.directoryObservable)
.create();
this.dataSource.selection.changed.subscribe(selection => {
this.selectedRows = selection.source.selected;
});
}
public changeDirectory(directoryId: number | null): void { public changeDirectory(directoryId: number | null): void {
this.clearSubscriptions(); this.clearSubscriptions();
this.folderSubscription = this.repo.getListObservableDirectory(directoryId).subscribe(mediafiles => { this.directoryObservable = this.repo.getListObservableDirectory(directoryId);
this.folderSubscription = this.directoryObservable.subscribe(mediafiles => {
if (mediafiles) { if (mediafiles) {
this.dataSource = createDS<ViewMediafile>() this.dataSource.refresh();
.onTrigger(() => mediafiles) this.cd.markForCheck();
.create();
this.cd.detectChanges();
} }
}); });
if (directoryId) { if (directoryId) {
this.directorySubscription = this.repo.getViewModelObservable(directoryId).subscribe(d => { this.directorySubscription = this.repo.getViewModelObservable(directoryId).subscribe(newDirectory => {
this.directory = d; this.directory = newDirectory;
if (d) { if (newDirectory) {
this.directoryChain = d.getDirectoryChain(); this.directoryChain = newDirectory.getDirectoryChain();
// Update the URL. // Update the URL.
this.router.navigate(['/mediafiles/files/' + d.path], { this.router.navigate(['/mediafiles/files/' + newDirectory.path], {
replaceUrl: true replaceUrl: true
}); });
} else { } else {
@ -287,8 +309,6 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
} }
} }
/**
*/
public onMainEvent(): void { public onMainEvent(): void {
const path = '/mediafiles/upload/' + (this.directory ? this.directory.path : ''); const path = '/mediafiles/upload/' + (this.directory ? this.directory.path : '');
this.router.navigate([path]); this.router.navigate([path]);
@ -300,10 +320,11 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
* @param file the selected file * @param file the selected file
*/ */
public onEditFile(file: ViewMediafile): void { public onEditFile(file: ViewMediafile): void {
if (!this.isMultiSelect) {
this.fileToEdit = file; this.fileToEdit = file;
this.fileEditForm = this.fb.group({ this.fileEditForm = this.fb.group({
title: [file.title, Validators.required], title: [file.filename, Validators.required],
access_groups_id: [file.access_groups_id] access_groups_id: [file.access_groups_id]
}); });
@ -315,6 +336,7 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
} }
}); });
} }
}
/** /**
* Click on the save button in edit mode * Click on the save button in edit mode
@ -338,6 +360,14 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
} }
} }
public async deleteSelected(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete all selected files and folders?');
if (await this.promptService.open(title)) {
await this.repo.bulkDelete(this.selectedRows);
this.deselectAll();
}
}
/** /**
* Returns the display name of an action * Returns the display name of an action
* *
@ -415,22 +445,50 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
parent_id: this.directory ? this.directory.id : null, parent_id: this.directory ? this.directory.id : null,
is_directory: true is_directory: true
}); });
this.repo.create(mediafile).then(null, this.raiseError); this.repo.create(mediafile).catch(this.raiseError);
} }
}); });
} }
public move(templateRef: TemplateRef<string>, mediafile: ViewMediafile): void { public move(templateRef: TemplateRef<string>, mediafiles: ViewMediafile[]): void {
this.newDirectoryForm.reset(); this.moveForm.reset();
if (mediafiles.some(file => file.is_directory)) {
this.filteredDirectoryBehaviorSubject.next(
this.directoryBehaviorSubject.value.filter(
dir => !mediafiles.some(file => dir.path.startsWith(file.path))
)
);
} else {
this.filteredDirectoryBehaviorSubject.next(this.directoryBehaviorSubject.value);
}
const dialogRef = this.dialog.open(templateRef, infoDialogSettings); const dialogRef = this.dialog.open(templateRef, infoDialogSettings);
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
if (result) { if (result) {
this.repo.move([mediafile], this.moveForm.value.directory_id).then(null, this.raiseError); this.repo.move(mediafiles, this.moveForm.value.directory_id).then(() => {
this.dataSource.selection.clear();
this.cd.markForCheck();
}, this.raiseError);
} }
}); });
} }
/**
* TODO: This is basically a duplicate of onSelectRow of ListViewTableComponent
*/
public onSelectRow(event: PblNgridDataMatrixRow<ViewMediafile>): void {
if (this.isMultiSelect) {
const clickedModel: ViewMediafile = event.row;
const alreadySelected = this.dataSource.selection.isSelected(clickedModel);
if (alreadySelected) {
this.dataSource.selection.deselect(clickedModel);
} else {
this.dataSource.selection.select(clickedModel);
}
}
}
private clearSubscriptions(): void { private clearSubscriptions(): void {
if (this.folderSubscription) { if (this.folderSubscription) {
this.folderSubscription.unsubscribe(); this.folderSubscription.unsubscribe();

View File

@ -17,7 +17,7 @@ const routes: Routes = [
}, },
{ {
path: 'upload', path: 'upload',
data: { basePerm: 'mediafiles.can_upload' }, data: { basePerm: 'mediafiles.can_manage' },
children: [{ path: '**', component: MediaUploadComponent }], children: [{ path: '**', component: MediaUploadComponent }],
pathMatch: 'prefix' pathMatch: 'prefix'
} }

View File

@ -50,6 +50,14 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
} }
public get title(): string { public get title(): string {
if (this.is_directory) {
return this.mediafile.path;
} else {
return this.mediafile.title;
}
}
public get filename(): string {
return this.mediafile.title; return this.mediafile.title;
} }

View File

@ -32,7 +32,14 @@ class MediafileViewSet(ModelViewSet):
""" """
if self.action in ("list", "retrieve", "metadata"): if self.action in ("list", "retrieve", "metadata"):
result = self.get_access_permissions().check_permissions(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ("create", "partial_update", "update", "move", "destroy"): elif self.action in (
"create",
"partial_update",
"update",
"move",
"destroy",
"bulk_delete",
):
result = has_perm(self.request.user, "mediafiles.can_see") and has_perm( result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
self.request.user, "mediafiles.can_manage" self.request.user, "mediafiles.can_manage"
) )
@ -150,6 +157,49 @@ class MediafileViewSet(ModelViewSet):
return Response() return Response()
@list_route(methods=["post"])
def bulk_delete(self, request):
"""
Deletes mediafiles *from one directory*. Expected data:
{ ids: [<id>, <id>, ...] }
It is checked, that every id belongs to the same directory.
"""
# Validate data:
if not isinstance(request.data, dict):
raise ValidationError({"detail": "The data must be a dict"})
ids = request.data.get("ids")
if not isinstance(ids, list):
raise ValidationError({"detail": "The ids must be a list"})
for id in ids:
if not isinstance(id, int):
raise ValidationError({"detail": "All ids must be an int"})
# Get mediafiles
mediafiles = []
for id in ids:
try:
mediafiles.append(Mediafile.objects.get(pk=id))
except Mediafile.DoesNotExist:
raise ValidationError(
{"detail": f"The mediafile with id {id} does not exist"}
)
if not mediafiles:
return Response()
# Validate, that they are in the same directory:
directory_id = mediafiles[0].parent_id
for mediafile in mediafiles:
if mediafile.parent_id != directory_id:
raise ValidationError(
{"detail": "All mediafiles must be in the same directory."}
)
with watch_and_update_configs():
for mediafile in mediafiles:
mediafile.delete()
return Response()
def get_mediafile(request, path): def get_mediafile(request, path):
""" """