Directories and access permissions for mediafiles
This commit is contained in:
parent
3f6fe28f35
commit
56c1da352e
@ -5,6 +5,7 @@ import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model
|
|||||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||||
import { Deferred } from '../deferred';
|
import { Deferred } from '../deferred';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
import { BaseRepository } from '../repositories/base-repository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents information about a deleted model.
|
* Represents information about a deleted model.
|
||||||
@ -187,7 +188,7 @@ export class DataStoreUpdateManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commits the given update slot. THis slot must be the current one. If there are requests
|
* Commits the given update slot. This slot must be the current one. If there are requests
|
||||||
* for update slots queued, the next one will be served.
|
* for update slots queued, the next one will be served.
|
||||||
*
|
*
|
||||||
* Note: I added this param to make sure, that only the user of the slot
|
* Note: I added this param to make sure, that only the user of the slot
|
||||||
@ -203,18 +204,33 @@ export class DataStoreUpdateManagerService {
|
|||||||
|
|
||||||
// notify repositories in two phases
|
// notify repositories in two phases
|
||||||
const repositories = this.mapperService.getAllRepositories();
|
const repositories = this.mapperService.getAllRepositories();
|
||||||
|
// just commit the update in a repository, if something was changed. Save
|
||||||
|
// this information in this mapping. the boolean is not evaluated; if there is an
|
||||||
|
const affectedRepos: { [collection: string]: BaseRepository<any, any, any> } = {};
|
||||||
|
|
||||||
// Phase 1: deleting and creating of view models (in this order)
|
// Phase 1: deleting and creating of view models (in this order)
|
||||||
repositories.forEach(repo => {
|
repositories.forEach(repo => {
|
||||||
repo.deleteModels(slot.getDeletedModelIdsForCollection(repo.collectionString));
|
const deletedModelIds = slot.getDeletedModelIdsForCollection(repo.collectionString);
|
||||||
repo.changedModels(slot.getChangedModelIdsForCollection(repo.collectionString));
|
repo.deleteModels(deletedModelIds);
|
||||||
|
const changedModelIds = slot.getChangedModelIdsForCollection(repo.collectionString);
|
||||||
|
repo.changedModels(changedModelIds);
|
||||||
|
|
||||||
|
if (deletedModelIds.length || changedModelIds.length) {
|
||||||
|
affectedRepos[repo.collectionString] = repo;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Phase 2: updating dependencies
|
// Phase 2: updating dependencies
|
||||||
repositories.forEach(repo => {
|
repositories.forEach(repo => {
|
||||||
repo.updateDependencies(slot.getChangedModels());
|
if (repo.updateDependencies(slot.getChangedModels())) {
|
||||||
|
affectedRepos[repo.collectionString] = repo;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Phase 3: committing the update to all affected repos. This will trigger all
|
||||||
|
// list observables/subjects to emit the new list.
|
||||||
|
Object.values(affectedRepos).forEach(repo => repo.commitUpdate());
|
||||||
|
|
||||||
slot.DS.triggerModifiedObservable();
|
slot.DS.triggerModifiedObservable();
|
||||||
|
|
||||||
// serve next slot request
|
// serve next slot request
|
||||||
|
@ -52,7 +52,6 @@ export abstract class BaseHasContentObjectRepository<
|
|||||||
this.contentObjectMapping[contentObject.collection][contentObject.id] = v;
|
this.contentObjectMapping[contentObject.collection][contentObject.id] = v;
|
||||||
this.updateViewModelObservable(id);
|
this.updateViewModelObservable(id);
|
||||||
});
|
});
|
||||||
this.updateViewModelListObservable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,6 +69,5 @@ export abstract class BaseHasContentObjectRepository<
|
|||||||
delete this.viewModelStore[id];
|
delete this.viewModelStore[id];
|
||||||
this.updateViewModelObservable(id);
|
this.updateViewModelObservable(id);
|
||||||
});
|
});
|
||||||
this.updateViewModelListObservable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
*
|
*
|
||||||
* It's used to debounce messages on the sortedViewModelListSubject
|
* It's used to debounce messages on the sortedViewModelListSubject
|
||||||
*/
|
*/
|
||||||
private readonly viewModelListSubject: BehaviorSubject<V[]> = new BehaviorSubject<V[]>([]);
|
protected readonly unsafeViewModelListSubject: BehaviorSubject<V[]> = new BehaviorSubject<V[]>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable subject for the sorted view model list.
|
* Observable subject for the sorted view model list.
|
||||||
@ -39,7 +39,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
* updates, if e.g. an autoupdate with a lot motions come in. The result is just one
|
* updates, if e.g. an autoupdate with a lot motions come in. The result is just one
|
||||||
* update of the new list instead of many unnecessary updates.
|
* update of the new list instead of many unnecessary updates.
|
||||||
*/
|
*/
|
||||||
protected readonly sortedViewModelListSubject: BehaviorSubject<V[]> = new BehaviorSubject<V[]>([]);
|
protected readonly viewModelListSubject: BehaviorSubject<V[]> = new BehaviorSubject<V[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable subject for any changes of view models.
|
* Observable subject for any changes of view models.
|
||||||
@ -94,8 +94,10 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
// All data is piped through an auditTime of 1ms. This is to prevent massive
|
// All data is piped through an auditTime of 1ms. This is to prevent massive
|
||||||
// updates, if e.g. an autoupdate with a lot motions come in. The result is just one
|
// updates, if e.g. an autoupdate with a lot motions come in. The result is just one
|
||||||
// update of the new list instead of many unnecessary updates.
|
// update of the new list instead of many unnecessary updates.
|
||||||
this.viewModelListSubject.pipe(auditTime(1)).subscribe(models => {
|
this.unsafeViewModelListSubject.pipe(auditTime(1)).subscribe(models => {
|
||||||
this.sortedViewModelListSubject.next(models.sort(this.viewModelSortFn));
|
if (models) {
|
||||||
|
this.viewModelListSubject.next(models.sort(this.viewModelSortFn));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.languageCollator = new Intl.Collator(this.translate.currentLang);
|
this.languageCollator = new Intl.Collator(this.translate.currentLang);
|
||||||
@ -105,27 +107,15 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
this.DS.clearObservable.subscribe(() => this.clear());
|
this.DS.clearObservable.subscribe(() => this.clear());
|
||||||
this.translate.onLangChange.subscribe(change => {
|
this.translate.onLangChange.subscribe(change => {
|
||||||
this.languageCollator = new Intl.Collator(change.lang);
|
this.languageCollator = new Intl.Collator(change.lang);
|
||||||
this.updateViewModelListObservable();
|
if (this.unsafeViewModelListSubject.value) {
|
||||||
});
|
this.viewModelListSubject.next(this.unsafeViewModelListSubject.value.sort(this.viewModelSortFn));
|
||||||
|
|
||||||
this.loadInitialFromDS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadInitialFromDS(): void {
|
|
||||||
// Populate the local viewModelStore with ViewModel Objects.
|
|
||||||
this.DS.getAll(this.baseModelCtor).forEach((model: M) => {
|
|
||||||
this.viewModelStore[model.id] = this.createViewModelWithTitles(model);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the list and then all models on their own
|
|
||||||
this.updateViewModelListObservable();
|
|
||||||
this.DS.getAll(this.baseModelCtor).forEach((model: M) => {
|
|
||||||
this.updateViewModelObservable(model.id);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all models from the repository (internally, no requests). Informs all subjects.
|
* Deletes all models from the repository (internally, no requests). Changes need
|
||||||
|
* to be committed via `commitUpdate()`.
|
||||||
*
|
*
|
||||||
* @param ids All model ids
|
* @param ids All model ids
|
||||||
*/
|
*/
|
||||||
@ -134,12 +124,11 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
delete this.viewModelStore[id];
|
delete this.viewModelStore[id];
|
||||||
this.updateViewModelObservable(id);
|
this.updateViewModelObservable(id);
|
||||||
});
|
});
|
||||||
this.updateViewModelListObservable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates or creates all given models in the repository (internally, no requests).
|
* Updates or creates all given models in the repository (internally, no requests).
|
||||||
* Informs all subjects.
|
* Changes need to be committed via `commitUpdate()`.
|
||||||
*
|
*
|
||||||
* @param ids All model ids.
|
* @param ids All model ids.
|
||||||
*/
|
*/
|
||||||
@ -148,15 +137,15 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
this.viewModelStore[id] = this.createViewModelWithTitles(this.DS.get(this.collectionString, id));
|
this.viewModelStore[id] = this.createViewModelWithTitles(this.DS.get(this.collectionString, id));
|
||||||
this.updateViewModelObservable(id);
|
this.updateViewModelObservable(id);
|
||||||
});
|
});
|
||||||
this.updateViewModelListObservable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates all models in this repository with all changed models.
|
* Updates all models in this repository with all changed models.
|
||||||
*
|
*
|
||||||
* @param changedModels A mapping of collections to ids of all changed models.
|
* @param changedModels A mapping of collections to ids of all changed models.
|
||||||
|
* @returns if at least one model was affected.
|
||||||
*/
|
*/
|
||||||
public updateDependencies(changedModels: CollectionIds): void {
|
public updateDependencies(changedModels: CollectionIds): boolean {
|
||||||
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
|
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -184,8 +173,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
viewModels.forEach(ownViewModel => {
|
viewModels.forEach(ownViewModel => {
|
||||||
this.updateViewModelObservable(ownViewModel.id);
|
this.updateViewModelObservable(ownViewModel.id);
|
||||||
});
|
});
|
||||||
this.updateViewModelListObservable();
|
|
||||||
}
|
}
|
||||||
|
return somethingUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getListTitle: (titleInformation: T) => string = (titleInformation: T) => {
|
public getListTitle: (titleInformation: T) => string = (titleInformation: T) => {
|
||||||
@ -292,7 +281,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
*/
|
*/
|
||||||
public setSortFunction(fn: (a: V, b: V) => number): void {
|
public setSortFunction(fn: (a: V, b: V) => number): void {
|
||||||
this.viewModelSortFn = fn;
|
this.viewModelSortFn = fn;
|
||||||
this.updateViewModelListObservable();
|
this.commitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -316,7 +305,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
* @returns all sorted view models stored in this repository.
|
* @returns all sorted view models stored in this repository.
|
||||||
*/
|
*/
|
||||||
public getSortedViewModelList(): V[] {
|
public getSortedViewModelList(): V[] {
|
||||||
return this.sortedViewModelListSubject.getValue();
|
return this.viewModelListSubject.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -333,7 +322,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
* @returns the (sorted) Observable of the whole store.
|
* @returns the (sorted) Observable of the whole store.
|
||||||
*/
|
*/
|
||||||
public getViewModelListObservable(): Observable<V[]> {
|
public getViewModelListObservable(): Observable<V[]> {
|
||||||
return this.sortedViewModelListSubject.asObservable();
|
return this.viewModelListSubject.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -343,7 +332,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
* @returns A subject that holds the model list
|
* @returns A subject that holds the model list
|
||||||
*/
|
*/
|
||||||
public getViewModelListBehaviorSubject(): BehaviorSubject<V[]> {
|
public getViewModelListBehaviorSubject(): BehaviorSubject<V[]> {
|
||||||
return this.sortedViewModelListSubject;
|
return this.viewModelListSubject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -366,15 +355,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
/**
|
/**
|
||||||
* update the observable of the list. Also updates the sorting of the view model list.
|
* update the observable of the list. Also updates the sorting of the view model list.
|
||||||
*/
|
*/
|
||||||
protected updateViewModelListObservable(): void {
|
public commitUpdate(): void {
|
||||||
this.viewModelListSubject.next(this.getViewModelList());
|
this.unsafeViewModelListSubject.next(this.getViewModelList());
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers both the observable update routines
|
|
||||||
*/
|
|
||||||
protected updateAllObservables(id: number): void {
|
|
||||||
this.updateViewModelListObservable();
|
|
||||||
this.updateViewModelObservable(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,21 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { map, first } from 'rxjs/operators';
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { ViewMediafile, MediafileTitleInformation } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile, MediafileTitleInformation } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||||
import { User } from 'app/shared/models/users/user';
|
|
||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataSendService } from 'app/core/core-services/data-send.service';
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository';
|
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository';
|
||||||
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { Group } from 'app/shared/models/users/group';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository for MediaFiles
|
* Repository for MediaFiles
|
||||||
@ -27,6 +29,8 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
|
|||||||
Mediafile,
|
Mediafile,
|
||||||
MediafileTitleInformation
|
MediafileTitleInformation
|
||||||
> {
|
> {
|
||||||
|
private directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the mediafile repository
|
* Constructor for the mediafile repository
|
||||||
* @param DS Data store
|
* @param DS Data store
|
||||||
@ -42,8 +46,17 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
|
|||||||
dataSend: DataSendService,
|
dataSend: DataSendService,
|
||||||
private httpService: HttpService
|
private httpService: HttpService
|
||||||
) {
|
) {
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, Mediafile, [User]);
|
super(DS, dataSend, mapperService, viewModelStoreService, translate, Mediafile, [Mediafile, Group]);
|
||||||
this.initSorting();
|
this.directoryBehaviorSubject = new BehaviorSubject([]);
|
||||||
|
this.getViewModelListObservable().subscribe(mediafiles => {
|
||||||
|
if (mediafiles) {
|
||||||
|
this.directoryBehaviorSubject.next(mediafiles.filter(mediafile => mediafile.is_directory));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewModelSortFn = (a: ViewMediafile, b: ViewMediafile) => {
|
||||||
|
return this.languageCollator.compare(a.title, b.title);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTitle = (titleInformation: MediafileTitleInformation) => {
|
public getTitle = (titleInformation: MediafileTitleInformation) => {
|
||||||
@ -62,8 +75,40 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
|
|||||||
*/
|
*/
|
||||||
public createViewModel(file: Mediafile): ViewMediafile {
|
public createViewModel(file: Mediafile): ViewMediafile {
|
||||||
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, file.list_of_speakers_id);
|
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, file.list_of_speakers_id);
|
||||||
const uploader = this.viewModelStoreService.get(ViewUser, file.uploader_id);
|
const parent = this.viewModelStoreService.get(ViewMediafile, file.parent_id);
|
||||||
return new ViewMediafile(file, listOfSpeakers, uploader);
|
const accessGroups = this.viewModelStoreService.getMany(ViewGroup, file.access_groups_id);
|
||||||
|
let inheritedAccessGroups;
|
||||||
|
if (file.has_inherited_access_groups) {
|
||||||
|
inheritedAccessGroups = this.viewModelStoreService.getMany(ViewGroup, <number[]>(
|
||||||
|
file.inherited_access_groups_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return new ViewMediafile(file, listOfSpeakers, parent, accessGroups, inheritedAccessGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDirectoryIdByPath(pathSegments: string[]): Promise<number | null> {
|
||||||
|
let parentId = null;
|
||||||
|
|
||||||
|
const mediafiles = await this.unsafeViewModelListSubject.pipe(first(x => !!x)).toPromise();
|
||||||
|
|
||||||
|
pathSegments.forEach(segment => {
|
||||||
|
const mediafile = mediafiles.find(m => m.is_directory && m.title === segment && m.parent_id === parentId);
|
||||||
|
if (!mediafile) {
|
||||||
|
parentId = null;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
parentId = mediafile.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getListObservableDirectory(parentId: number | null): Observable<ViewMediafile[]> {
|
||||||
|
return this.getViewModelListObservable().pipe(
|
||||||
|
map(mediafiles => {
|
||||||
|
return mediafiles.filter(mediafile => mediafile.parent_id === parentId);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,17 +119,19 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
|
|||||||
* @param file created UploadData, containing a file
|
* @param file created UploadData, containing a file
|
||||||
* @returns the promise to a new mediafile.
|
* @returns the promise to a new mediafile.
|
||||||
*/
|
*/
|
||||||
public async uploadFile(file: FormData): Promise<Identifiable> {
|
public async uploadFile(file: any): Promise<Identifiable> {
|
||||||
const emptyHeader = new HttpHeaders();
|
const emptyHeader = new HttpHeaders();
|
||||||
return this.httpService.post<Identifiable>('/rest/mediafiles/mediafile/', file, {}, emptyHeader);
|
return this.httpService.post<Identifiable>('/rest/mediafiles/mediafile/', file, {}, emptyHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getDirectoryBehaviorSubject(): BehaviorSubject<ViewMediafile[]> {
|
||||||
* Sets the default sorting (e.g. in dropdowns and for new users) to 'title'
|
return this.directoryBehaviorSubject;
|
||||||
*/
|
}
|
||||||
private initSorting(): void {
|
|
||||||
this.setSortFunction((a: ViewMediafile, b: ViewMediafile) => {
|
public async move(mediafiles: ViewMediafile[], directoryId: number | null): Promise<void> {
|
||||||
return this.languageCollator.compare(a.title, b.title);
|
return await this.httpService.post('/rest/mediafiles/mediafile/move/', {
|
||||||
|
ids: mediafiles.map(mediafile => mediafile.id),
|
||||||
|
directory_id: directoryId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
* Special handling of updating personal notes.
|
* Special handling of updating personal notes.
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public updateDependencies(changedModels: CollectionIds): void {
|
public updateDependencies(changedModels: CollectionIds): boolean {
|
||||||
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
|
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -302,8 +302,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
viewModels.forEach(ownViewModel => {
|
viewModels.forEach(ownViewModel => {
|
||||||
this.updateViewModelObservable(ownViewModel.id);
|
this.updateViewModelObservable(ownViewModel.id);
|
||||||
});
|
});
|
||||||
this.updateViewModelListObservable();
|
|
||||||
}
|
}
|
||||||
|
return somethingUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +51,7 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
|
|||||||
) {
|
) {
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow);
|
super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow);
|
||||||
|
|
||||||
this.sortedViewModelListSubject.subscribe(models => {
|
this.viewModelListSubject.subscribe(models => {
|
||||||
if (models && models.length > 0) {
|
if (models && models.length > 0) {
|
||||||
this.initSorting(models);
|
this.initSorting(models);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
* @param DS The DataStore
|
* @param DS The DataStore
|
||||||
* @param mapperService Maps collection strings to classes
|
* @param mapperService Maps collection strings to classes
|
||||||
* @param dataSend sending changed objects
|
* @param dataSend sending changed objects
|
||||||
* @param constants reading out the OpenSlides constants
|
* @param constantsService reading out the OpenSlides constants
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
DS: DataStoreService,
|
DS: DataStoreService,
|
||||||
@ -55,7 +55,7 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
mapperService: CollectionStringMapperService,
|
mapperService: CollectionStringMapperService,
|
||||||
viewModelStoreService: ViewModelStoreService,
|
viewModelStoreService: ViewModelStoreService,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
private constants: ConstantsService,
|
private constantsService: ConstantsService,
|
||||||
private http: HttpService
|
private http: HttpService
|
||||||
) {
|
) {
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, Group);
|
super(DS, dataSend, mapperService, viewModelStoreService, translate, Group);
|
||||||
@ -109,7 +109,7 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
* read the constants, add them to an array of apps
|
* read the constants, add them to an array of apps
|
||||||
*/
|
*/
|
||||||
private sortPermsPerApp(): void {
|
private sortPermsPerApp(): void {
|
||||||
this.constants.get<any>('permissions').subscribe(perms => {
|
this.constantsService.get<any>('permissions').subscribe(perms => {
|
||||||
let pluginCounter = 0;
|
let pluginCounter = 0;
|
||||||
for (const perm of perms) {
|
for (const perm of perms) {
|
||||||
// extract the apps name
|
// extract the apps name
|
||||||
|
@ -63,7 +63,7 @@ export class MediaManageService {
|
|||||||
const restPath = `/rest/core/config/${action}/`;
|
const restPath = `/rest/core/config/${action}/`;
|
||||||
|
|
||||||
const config = this.getMediaConfig(action);
|
const config = this.getMediaConfig(action);
|
||||||
const path = config.path !== file.downloadUrl ? file.downloadUrl : '';
|
const path = config.path !== file.url ? file.url : '';
|
||||||
|
|
||||||
// Create the payload that the server requires to manage a mediafile
|
// Create the payload that the server requires to manage a mediafile
|
||||||
const payload: ManagementPayload = {
|
const payload: ManagementPayload = {
|
||||||
|
@ -12,6 +12,25 @@
|
|||||||
</ngx-file-drop>
|
</ngx-file-drop>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Directory selector, if no external directory is provided -->
|
||||||
|
<div *ngIf="showDirectorySelector">
|
||||||
|
<os-search-value-selector
|
||||||
|
ngDefaultControl
|
||||||
|
[formControl]="directorySelectionForm.get('parent_id')"
|
||||||
|
[multiple]="false"
|
||||||
|
[includeNone]="true"
|
||||||
|
[noneTitle]="'Base folder'"
|
||||||
|
listname="{{ 'Parent directory' | translate }}"
|
||||||
|
[InputListValues]="directoryBehaviorSubject"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Upload to:
|
||||||
|
<span *ngIf="selectedDirectoryId === null" translate>Base folder</span>
|
||||||
|
<span *ngIf="selectedDirectoryId !== null">{{ getDirectory(selectedDirectoryId).title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-container" *ngIf="uploadList.data.length > 0">
|
<div class="table-container" *ngIf="uploadList.data.length > 0">
|
||||||
<table mat-table [dataSource]="uploadList" class="mat-elevation-z8">
|
<table mat-table [dataSource]="uploadList" class="mat-elevation-z8">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
@ -44,14 +63,17 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Hidden -->
|
<!-- Access groups -->
|
||||||
<ng-container matColumnDef="hidden">
|
<ng-container matColumnDef="access_groups">
|
||||||
<th mat-header-cell *matHeaderCellDef><span translate>Hidden</span></th>
|
<th mat-header-cell *matHeaderCellDef><span translate>Access permissions</span></th>
|
||||||
<td mat-cell *matCellDef="let file">
|
<td mat-cell *matCellDef="let file">
|
||||||
<mat-checkbox
|
<os-search-value-selector
|
||||||
[checked]="file.hidden"
|
ngDefaultControl
|
||||||
(change)="onChangeHidden($event.checked, file)"
|
[formControl]="file.form.get('access_groups_id')"
|
||||||
></mat-checkbox>
|
[multiple]="true"
|
||||||
|
listname="{{ 'Access groups' | translate }}"
|
||||||
|
[InputListValues]="groupsBehaviorSubject"
|
||||||
|
></os-search-value-selector>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core';
|
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { MatTableDataSource, MatTable } from '@angular/material/table';
|
import { MatTableDataSource, MatTable } from '@angular/material/table';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
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';
|
||||||
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To hold the structure of files to upload
|
* To hold the structure of files to upload
|
||||||
@ -13,8 +17,7 @@ interface FileData {
|
|||||||
mediafile: File;
|
mediafile: File;
|
||||||
filename: string;
|
filename: string;
|
||||||
title: string;
|
title: string;
|
||||||
uploader_id: number;
|
form: FormGroup;
|
||||||
hidden: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -26,7 +29,7 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Columns to display in the upload-table
|
* Columns to display in the upload-table
|
||||||
*/
|
*/
|
||||||
public displayedColumns: string[] = ['title', 'filename', 'information', 'hidden', 'remove'];
|
public displayedColumns: string[] = ['title', 'filename', 'information', 'access_groups', 'remove'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine wether to show the progress bar
|
* Determine wether to show the progress bar
|
||||||
@ -50,6 +53,9 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
public parallel = true;
|
public parallel = true;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public directoryId: number | null | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set if an error was detected to prevent automatic navigation
|
* Set if an error was detected to prevent automatic navigation
|
||||||
*/
|
*/
|
||||||
@ -73,13 +79,40 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
@Output()
|
@Output()
|
||||||
public errorEvent = new EventEmitter<string>();
|
public errorEvent = new EventEmitter<string>();
|
||||||
|
|
||||||
|
public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>;
|
||||||
|
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
|
||||||
|
|
||||||
|
public directorySelectionForm: FormGroup;
|
||||||
|
|
||||||
|
public get showDirectorySelector(): boolean {
|
||||||
|
return this.directoryId === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selectedDirectoryId(): number | null {
|
||||||
|
if (this.showDirectorySelector) {
|
||||||
|
return this.directorySelectionForm.controls.parent_id.value;
|
||||||
|
} else {
|
||||||
|
return this.directoryId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the media upload page
|
* Constructor for the media upload page
|
||||||
*
|
*
|
||||||
* @param repo the mediafile repository
|
* @param repo the mediafile repository
|
||||||
* @param op the operator, to check who was the uploader
|
* @param op the operator, to check who was the uploader
|
||||||
*/
|
*/
|
||||||
public constructor(private repo: MediafileRepositoryService, private op: OperatorService) {}
|
public constructor(
|
||||||
|
private repo: MediafileRepositoryService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private groupRepo: GroupRepositoryService
|
||||||
|
) {
|
||||||
|
this.directoryBehaviorSubject = this.repo.getDirectoryBehaviorSubject();
|
||||||
|
this.groupsBehaviorSubject = this.groupRepo.getViewModelListBehaviorSubject();
|
||||||
|
this.directorySelectionForm = this.formBuilder.group({
|
||||||
|
parent_id: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init
|
* Init
|
||||||
@ -89,6 +122,10 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
this.uploadList = new MatTableDataSource<FileData>();
|
this.uploadList = new MatTableDataSource<FileData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDirectory(directoryId: number): ViewMediafile {
|
||||||
|
return this.repo.getViewModel(directoryId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts given FileData into FormData format and hands it over to the repository
|
* Converts given FileData into FormData format and hands it over to the repository
|
||||||
* to upload
|
* to upload
|
||||||
@ -99,8 +136,13 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
const input = new FormData();
|
const input = new FormData();
|
||||||
input.set('mediafile', fileData.mediafile);
|
input.set('mediafile', fileData.mediafile);
|
||||||
input.set('title', fileData.title);
|
input.set('title', fileData.title);
|
||||||
input.set('uploader_id', '' + fileData.uploader_id);
|
const access_groups_id = fileData.form.value.access_groups_id || [];
|
||||||
input.set('hidden', '' + fileData.hidden);
|
if (access_groups_id.length > 0) {
|
||||||
|
input.set('access_groups_id', '' + access_groups_id);
|
||||||
|
}
|
||||||
|
if (this.selectedDirectoryId) {
|
||||||
|
input.set('parent_id', '' + this.selectedDirectoryId);
|
||||||
|
}
|
||||||
|
|
||||||
// raiseError will automatically ignore existing files
|
// raiseError will automatically ignore existing files
|
||||||
await this.repo.uploadFile(input).then(
|
await this.repo.uploadFile(input).then(
|
||||||
@ -127,16 +169,6 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
return `${bytes} ${['B', 'kB', 'MB', 'GB', 'TB'][unitLevel]}`;
|
return `${bytes} ${['B', 'kB', 'MB', 'GB', 'TB'][unitLevel]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Change event to set a file to hidden or not
|
|
||||||
*
|
|
||||||
* @param hidden whether the file should be hidden
|
|
||||||
* @param file the given file
|
|
||||||
*/
|
|
||||||
public onChangeHidden(hidden: boolean, file: FileData): void {
|
|
||||||
file.hidden = hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change event to adjust the title
|
* Change event to adjust the title
|
||||||
*
|
*
|
||||||
@ -157,8 +189,9 @@ export class MediaUploadContentComponent implements OnInit {
|
|||||||
mediafile: file,
|
mediafile: file,
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
title: file.name,
|
title: file.name,
|
||||||
uploader_id: this.op.user.id,
|
form: this.formBuilder.group({
|
||||||
hidden: false
|
access_groups_id: [[]]
|
||||||
|
})
|
||||||
};
|
};
|
||||||
this.uploadList.data.push(newFile);
|
this.uploadList.data.push(newFile);
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<ngx-mat-select-search ngModel (ngModelChange)="onSearch($event)"></ngx-mat-select-search>
|
<ngx-mat-select-search ngModel (ngModelChange)="onSearch($event)"></ngx-mat-select-search>
|
||||||
<div *ngIf="!multiple && includeNone">
|
<div *ngIf="!multiple && includeNone">
|
||||||
<mat-option [value]="null">
|
<mat-option [value]="null">
|
||||||
<span>–</span>
|
{{ noneTitle | translate }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,7 @@ import { Selectable } from '../selectable';
|
|||||||
/**
|
/**
|
||||||
* Reusable Searchable Value Selector
|
* Reusable Searchable Value Selector
|
||||||
*
|
*
|
||||||
* Use `multiple="true"`, `[InputListValues]=myValues`,`[formControl]="myformcontrol"`, `[form]="myform_name"` and `placeholder={{listname}}` to pass the Values and Listname
|
* Use `multiple="true"`, `[InputListValues]=myValues`,`[formControl]="myformcontrol"` and `placeholder={{listname}}` to pass the Values and Listname
|
||||||
*
|
*
|
||||||
* ## Examples:
|
* ## Examples:
|
||||||
*
|
*
|
||||||
@ -64,6 +64,9 @@ export class SearchValueSelectorComponent implements OnDestroy {
|
|||||||
@Input()
|
@Input()
|
||||||
public includeNone = false;
|
public includeNone = false;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public noneTitle = '–';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean, whether the component should be rendered with full width.
|
* Boolean, whether the component should be rendered with full width.
|
||||||
*/
|
*/
|
||||||
|
@ -17,15 +17,24 @@ export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
|
|||||||
public static COLLECTIONSTRING = 'mediafiles/mediafile';
|
public static COLLECTIONSTRING = 'mediafiles/mediafile';
|
||||||
public id: number;
|
public id: number;
|
||||||
public title: string;
|
public title: string;
|
||||||
public mediafile: FileMetadata;
|
public mediafile?: FileMetadata;
|
||||||
public media_url_prefix: string;
|
public media_url_prefix: string;
|
||||||
public uploader_id: number;
|
|
||||||
public filesize: string;
|
public filesize: string;
|
||||||
public hidden: boolean;
|
public access_groups_id: number[];
|
||||||
public timestamp: string;
|
public create_timestamp: string;
|
||||||
|
public parent_id: number | null;
|
||||||
|
public is_directory: boolean;
|
||||||
|
public path: string;
|
||||||
|
public inherited_access_groups_id: boolean | number[];
|
||||||
|
|
||||||
|
public get has_inherited_access_groups(): boolean {
|
||||||
|
return typeof this.inherited_access_groups_id !== 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public constructor(input?: any) {
|
||||||
super(Mediafile.COLLECTIONSTRING, input);
|
super(Mediafile.COLLECTIONSTRING);
|
||||||
|
// Do not change null to undefined...
|
||||||
|
this.deserialize(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +42,7 @@ export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
|
|||||||
*
|
*
|
||||||
* @returns the download URL for the specific file as string
|
* @returns the download URL for the specific file as string
|
||||||
*/
|
*/
|
||||||
public get downloadUrl(): string {
|
public get url(): string {
|
||||||
return `${this.media_url_prefix}${this.mediafile.name}`;
|
return `${this.media_url_prefix}${this.path}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
|||||||
: true;
|
: true;
|
||||||
|
|
||||||
if (this.isCurrentListOfSpeakers) {
|
if (this.isCurrentListOfSpeakers) {
|
||||||
this.projectors = this.projectorRepo.getSortedViewModelList();
|
this.projectors = this.projectorRepo.getViewModelList();
|
||||||
this.updateClosProjector();
|
this.updateClosProjector();
|
||||||
this.projectorRepo.getViewModelListObservable().subscribe(newProjectors => {
|
this.projectorRepo.getViewModelListObservable().subscribe(newProjectors => {
|
||||||
this.projectors = newProjectors;
|
this.projectors = newProjectors;
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
<h4 translate>Election documents</h4>
|
<h4 translate>Election documents</h4>
|
||||||
<mat-list dense class="election-document-list">
|
<mat-list dense class="election-document-list">
|
||||||
<mat-list-item *ngFor="let file of assignment.attachments">
|
<mat-list-item *ngFor="let file of assignment.attachments">
|
||||||
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.getTitle() }}</a>
|
<a [routerLink]="file.url" target="_blank">{{ file.getTitle() }}</a>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,7 +110,7 @@ export class AssignmentListComponent extends BaseListViewComponent<ViewAssignmen
|
|||||||
* otherwise the whole list of assignments is exported.
|
* otherwise the whole list of assignments is exported.
|
||||||
*/
|
*/
|
||||||
public downloadAssignmentButton(assignments?: ViewAssignment[]): void {
|
public downloadAssignmentButton(assignments?: ViewAssignment[]): void {
|
||||||
this.pdfService.exportMultipleAssignments(assignments ? assignments : this.repo.getSortedViewModelList());
|
this.pdfService.exportMultipleAssignments(assignments ? assignments : this.repo.getViewModelList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<mat-card class="os-card">
|
<mat-card class="os-card">
|
||||||
<os-media-upload-content
|
<os-media-upload-content
|
||||||
[parallel]="parallel"
|
[parallel]="parallel"
|
||||||
|
[directoryId]="directoryId"
|
||||||
(uploadSuccessEvent)="uploadSuccess()"
|
(uploadSuccessEvent)="uploadSuccess()"
|
||||||
(errorEvent)="showError($event)"
|
(errorEvent)="showError($event)"
|
||||||
></os-media-upload-content>
|
></os-media-upload-content>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle file uploads from user
|
* Handle file uploads from user
|
||||||
@ -15,13 +16,15 @@ import { Router, ActivatedRoute } from '@angular/router';
|
|||||||
templateUrl: './media-upload.component.html',
|
templateUrl: './media-upload.component.html',
|
||||||
styleUrls: ['./media-upload.component.scss']
|
styleUrls: ['./media-upload.component.scss']
|
||||||
})
|
})
|
||||||
export class MediaUploadComponent extends BaseViewComponent {
|
export class MediaUploadComponent extends BaseViewComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Determine if uploading should happen parallel or synchronously.
|
* Determine if uploading should happen parallel or synchronously.
|
||||||
* Synchronous uploading might be necessary if we see that stuff breaks
|
* Synchronous uploading might be necessary if we see that stuff breaks
|
||||||
*/
|
*/
|
||||||
public parallel = true;
|
public parallel = true;
|
||||||
|
|
||||||
|
public directoryId: number | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the media upload page
|
* Constructor for the media upload page
|
||||||
*
|
*
|
||||||
@ -36,11 +39,18 @@ export class MediaUploadComponent extends BaseViewComponent {
|
|||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
private repo: MediafileRepositoryService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.repo.getDirectoryIdByPath(this.route.snapshot.url.map(x => x.path)).then(directoryId => {
|
||||||
|
this.directoryId = directoryId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for successful uploads
|
* Handler for successful uploads
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<os-head-bar [mainButton]="canUploadFiles" [multiSelectMode]="isMultiSelect" (mainEvent)="onMainEvent()">
|
<os-head-bar [mainButton]="canUploadFiles" [multiSelectMode]="false" (mainEvent)="onMainEvent()">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 translate>Files</h2>
|
<h2 translate>Files</h2>
|
||||||
@ -6,110 +6,151 @@
|
|||||||
|
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<div class="menu-slot" *osPerms="'mediafiles.can_manage'">
|
<div class="menu-slot" *osPerms="'mediafiles.can_manage'">
|
||||||
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
|
<button type="button" mat-icon-button (click)="createNewFolder(newFolderDialog)">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>create_new_folder</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<!--<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multiselect info -->
|
<!-- Multiselect info -->
|
||||||
<div *ngIf="this.isMultiSelect" class="central-info-slot">
|
<!--<div *ngIf="this.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 }} </span><span translate>selected</span>
|
<span>{{ selectedRows.length }} </span><span translate>selected</span>
|
||||||
</div>
|
</div>-->
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<os-list-view-table
|
<!-- TODO: Sort bar -->
|
||||||
[repo]="repo"
|
|
||||||
[filterService]="filterService"
|
|
||||||
[sortService]="sortService"
|
|
||||||
[columns]="tableColumnDefinition"
|
|
||||||
[multiSelect]="isMultiSelect"
|
|
||||||
[restricted]="restrictedColumns"
|
|
||||||
[filterProps]="filterProps"
|
|
||||||
scrollKey="user"
|
|
||||||
[(selectedRows)]="selectedRows"
|
|
||||||
(dataSourceChange)="onDataSourceChange($event)"
|
|
||||||
>
|
|
||||||
<!-- File title column -->
|
|
||||||
<div *pblNgridCellDef="'title'; row as file" class="cell-slot fill">
|
|
||||||
<a class="detail-link" [routerLink]="file.downloadUrl" target="_blank" *ngIf="!isMultiSelect"></a>
|
|
||||||
<span *ngIf="file.is_hidden">
|
|
||||||
<mat-icon matTooltip="{{ 'is hidden' | translate }}">lock</mat-icon>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
<div>
|
<div>
|
||||||
{{ file.title }}
|
<button mat-button (click)="changeDirectory(null)">
|
||||||
</div>
|
<span translate>Base folder</span>
|
||||||
</div>
|
</button>
|
||||||
|
<span *ngFor="let directory of directoryChain; let last=last">
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
<button *ngIf="!last" mat-button (click)="changeDirectory(directory.id)">
|
||||||
|
{{ directory.title }}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="last" mat-button (click)="onEditFile(directory)">
|
||||||
|
{{ directory.title }}
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Info column -->
|
<button mat-icon-button *ngIf="directory" (click)="changeDirectory(directory.parent_id)">
|
||||||
<div *pblNgridCellDef="'info'; row as file" class="cell-slot fill">
|
<mat-icon>arrow_upward</mat-icon>
|
||||||
<div class="file-info-cell">
|
|
||||||
<os-icon-container icon="insert_drive_file">{{ file.type }}</os-icon-container>
|
|
||||||
<os-icon-container icon="data_usage">{{ file.size }}</os-icon-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Indicator column -->
|
|
||||||
<div *pblNgridCellDef="'indicator'; row as file" class="cell-slot fill">
|
|
||||||
<div
|
|
||||||
*ngIf="getFileSettings(file).length > 0"
|
|
||||||
[matMenuTriggerFor]="singleFileMenu"
|
|
||||||
[matMenuTriggerData]="{ file: file }"
|
|
||||||
[matTooltip]="formatIndicatorTooltip(file)"
|
|
||||||
>
|
|
||||||
<mat-icon *ngIf="file.isFont()">text_fields</mat-icon>
|
|
||||||
<mat-icon *ngIf="file.isImage()">insert_photo</mat-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Menu column -->
|
|
||||||
<div *pblNgridCellDef="'menu'; row as file" class="cell-slot fill">
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
[matMenuTriggerFor]="singleFileMenu"
|
|
||||||
[matMenuTriggerData]="{ file: file }"
|
|
||||||
[disabled]="isMultiSelect"
|
|
||||||
>
|
|
||||||
<mat-icon>more_vert</mat-icon>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</os-list-view-table>
|
|
||||||
|
<div *ngIf="directory && directory.inherited_access_groups_id !== true">
|
||||||
|
<span translate>Visibility of this directory:</span>
|
||||||
|
<span *ngIf="directory.inherited_access_groups_id === false" translate>No one</span>
|
||||||
|
<span *ngIf="directory.has_inherited_access_groups" translate>
|
||||||
|
<os-icon-container icon="group">{{ directory.inherited_access_groups }}</os-icon-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-table [dataSource]="dataSource" class="os-listview-table">
|
||||||
|
<!-- Projector button -->
|
||||||
|
<ng-container matColumnDef="projector">
|
||||||
|
<td mat-cell *matCellDef="let mediafile">
|
||||||
|
<os-projector-button *ngIf="mediafile.isProjectable()" class="projector-button" [object]="mediafile"></os-projector-button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="icon">
|
||||||
|
<td mat-cell *matCellDef="let mediafile">
|
||||||
|
<mat-icon>{{ mediafile.getIcon() }}</mat-icon>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="title">
|
||||||
|
<td mat-cell *matCellDef="let mediafile">
|
||||||
|
<a target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file">
|
||||||
|
{{ mediafile.title }}
|
||||||
|
</a>
|
||||||
|
<a (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory">
|
||||||
|
{{ mediafile.title }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="info">
|
||||||
|
<td mat-cell *matCellDef="let mediafile">
|
||||||
|
<os-icon-container *ngIf="mediafile.is_file" icon="data_usage">{{ mediafile.size }}</os-icon-container>
|
||||||
|
<os-icon-container *ngIf="mediafile.access_groups.length" icon="group">{{ mediafile.access_groups }}</os-icon-container>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="indicator">
|
||||||
|
<td mat-cell *matCellDef="let mediafile">
|
||||||
|
<div
|
||||||
|
*ngIf="getFileSettings(mediafile).length > 0"
|
||||||
|
[matMenuTriggerFor]="singleMediafileMenu"
|
||||||
|
[matMenuTriggerData]="{ file: file }"
|
||||||
|
[matTooltip]="formatIndicatorTooltip(mediafile)"
|
||||||
|
>
|
||||||
|
<mat-icon *ngIf="mediafile.isFont()">text_fields</mat-icon>
|
||||||
|
<mat-icon *ngIf="mediafile.isImage()">insert_photo</mat-icon>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="menu">
|
||||||
|
<td mat-cell *matCellDef="let mediafile">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[matMenuTriggerFor]="singleMediafileMenu"
|
||||||
|
[matMenuTriggerData]="{ mediafile: mediafile }"
|
||||||
|
>
|
||||||
|
<!-- TODO: [disabled]="isMultiSelect" -->
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
<!-- Template for the managing buttons -->
|
<!-- Template for the managing buttons -->
|
||||||
<ng-template #manageButton let-file="file" let-action="action">
|
<ng-template #manageButton let-mediafile="mediafile" let-action="action">
|
||||||
<button mat-menu-item (click)="onManageButton($event, file, action)">
|
<button mat-menu-item (click)="onManageButton($event, mediafile, action)">
|
||||||
<mat-icon color="accent"> {{ isUsedAs(file, action) ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
<mat-icon color="accent"> {{ isUsedAs(mediafile, action) ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
||||||
<span>{{ getNameOfAction(action) }}</span>
|
<span>{{ getNameOfAction(action) }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Menu for single files in the list -->
|
<!-- Menu for single files in the list -->
|
||||||
<mat-menu #singleFileMenu="matMenu">
|
<mat-menu #singleMediafileMenu="matMenu">
|
||||||
<ng-template matMenuContent let-file="file">
|
<ng-template matMenuContent let-mediafile="mediafile">
|
||||||
<!-- Exclusive for images -->
|
<!-- Exclusive for images -->
|
||||||
<div *ngIf="file.isImage()">
|
<div *ngIf="mediafile.isImage()">
|
||||||
<div *ngFor="let action of logoActions">
|
<div *ngFor="let action of logoActions">
|
||||||
<ng-container *ngTemplateOutlet="manageButton; context: { file: file, action: action }"></ng-container>
|
<ng-container *ngTemplateOutlet="manageButton; context: { mediafile: mediafile, action: action }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Exclusive for fonts -->
|
<!-- Exclusive for fonts -->
|
||||||
<div *ngIf="file.isFont()">
|
<div *ngIf="mediafile.isFont()">
|
||||||
<div *ngFor="let action of fontActions">
|
<div *ngFor="let action of fontActions">
|
||||||
<ng-container *ngTemplateOutlet="manageButton; context: { file: file, action: action }"></ng-container>
|
<ng-container *ngTemplateOutlet="manageButton; context: { mediafile: mediafile, action: action }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit and delete for all images -->
|
<!-- Edit and delete for all images -->
|
||||||
<mat-divider></mat-divider>
|
<mat-divider *ngIf="mediafile.isFont() || mediafile.isImage()"></mat-divider>
|
||||||
|
|
||||||
<os-speaker-button [object]="file" [menuItem]="true"></os-speaker-button>
|
<os-speaker-button [object]="mediafile" [menuItem]="true"></os-speaker-button>
|
||||||
<button mat-menu-item (click)="onEditFile(file)">
|
<button mat-menu-item (click)="onEditFile(mediafile)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Edit</span>
|
<span translate>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item class="red-warning-text" (click)="onDelete(file)">
|
<button mat-menu-item (click)="move(moveDialog, mediafile)">
|
||||||
|
<mat-icon>near_me</mat-icon>
|
||||||
|
<span translate>Move</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="red-warning-text" (click)="onDelete(mediafile)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
@ -117,7 +158,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>
|
||||||
@ -145,7 +186,7 @@
|
|||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-menu>
|
</mat-menu>-->
|
||||||
|
|
||||||
<ng-template #fileEditDialog>
|
<ng-template #fileEditDialog>
|
||||||
<h1 mat-dialog-title>{{ 'Edit details for' | translate }}</h1>
|
<h1 mat-dialog-title>{{ 'Edit details for' | translate }}</h1>
|
||||||
@ -163,12 +204,13 @@
|
|||||||
<mat-error *ngIf="fileEditForm.invalid" translate>Required</mat-error>
|
<mat-error *ngIf="fileEditForm.invalid" translate>Required</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field>
|
<os-search-value-selector
|
||||||
<mat-select formControlName="hidden" placeholder="{{ 'Visibility' | translate }}">
|
ngDefaultControl
|
||||||
<mat-option [value]="true"> <span translate>Hidden</span> </mat-option>
|
[formControl]="fileEditForm.get('access_groups_id')"
|
||||||
<mat-option [value]="false"><span translate>Visible</span></mat-option>
|
[multiple]="true"
|
||||||
</mat-select>
|
listname="{{ 'Access groups' | translate }}"
|
||||||
</mat-form-field>
|
[InputListValues]="groupsBehaviorSubject"
|
||||||
|
></os-search-value-selector>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions>
|
<div mat-dialog-actions>
|
||||||
@ -186,3 +228,58 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- New folder dialog -->
|
||||||
|
<ng-template #newFolderDialog>
|
||||||
|
<h1 mat-dialog-title>
|
||||||
|
<span translate>Create new directory</span>
|
||||||
|
</h1>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<p translate>Please enter a name for the new directory:</p>
|
||||||
|
<mat-form-field [formGroup]="newDirectoryForm">
|
||||||
|
<input matInput osAutofocus formControlName="title" required/>
|
||||||
|
</mat-form-field>
|
||||||
|
<os-search-value-selector
|
||||||
|
ngDefaultControl
|
||||||
|
[formControl]="newDirectoryForm.get('access_groups_id')"
|
||||||
|
[multiple]="true"
|
||||||
|
listname="{{ 'Access groups' | translate }}"
|
||||||
|
[InputListValues]="groupsBehaviorSubject"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button type="submit" mat-button color="primary" [mat-dialog-close]="true">
|
||||||
|
<span translate>Save</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" mat-button [mat-dialog-close]="null">
|
||||||
|
<span translate>Cancel</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Move dialog -->
|
||||||
|
<ng-template #moveDialog>
|
||||||
|
<h1 mat-dialog-title>
|
||||||
|
<span translate>Move to directory</span>
|
||||||
|
</h1>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<p translate>Please select the directory to move to:</p>
|
||||||
|
<os-search-value-selector
|
||||||
|
ngDefaultControl
|
||||||
|
[formControl]="moveForm.get('directory_id')"
|
||||||
|
[multiple]="false"
|
||||||
|
[includeNone]="true"
|
||||||
|
[noneTitle]="'Base folder'"
|
||||||
|
listname="{{ 'Parent directory' | translate }}"
|
||||||
|
[InputListValues]="directoryBehaviorSubject"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button type="submit" mat-button color="primary" [mat-dialog-close]="true">
|
||||||
|
<span translate>Move</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" mat-button [mat-dialog-close]="null">
|
||||||
|
<span translate>Cancel</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
@ -4,15 +4,3 @@
|
|||||||
::ng-deep .mat-tooltip {
|
::ng-deep .mat-tooltip {
|
||||||
white-space: pre-line !important;
|
white-space: pre-line !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// duplicate. Put into own file
|
|
||||||
.file-info-cell {
|
|
||||||
display: grid;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
span {
|
|
||||||
.mat-icon {
|
|
||||||
font-size: 130%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
import { Component, OnInit, ViewChild, TemplateRef } from '@angular/core';
|
import { Component, OnInit, ViewChild, TemplateRef, OnDestroy } from '@angular/core';
|
||||||
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
|
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { MatTableDataSource } from '@angular/material';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { PblColumnDefinition } from '@pebula/ngrid';
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
|
|
||||||
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 { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
|
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
|
||||||
import { MediaManageService } from 'app/core/ui-services/media-manage.service';
|
import { MediaManageService } from 'app/core/ui-services/media-manage.service';
|
||||||
import { MediafileFilterListService } from '../../services/mediafile-filter.service';
|
|
||||||
import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service';
|
import { MediafilesSortListService } from '../../services/mediafiles-sort-list.service';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { StorageService } from 'app/core/core-services/storage.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 { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all the uploaded files.
|
* Lists all the uploaded files.
|
||||||
@ -28,7 +29,9 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
|
|||||||
templateUrl: './mediafile-list.component.html',
|
templateUrl: './mediafile-list.component.html',
|
||||||
styleUrls: ['./mediafile-list.component.scss']
|
styleUrls: ['./mediafile-list.component.scss']
|
||||||
})
|
})
|
||||||
export class MediafileListComponent extends BaseListViewComponent<ViewMediafile> implements OnInit {
|
export class MediafileListComponent extends BaseViewComponent implements OnInit, OnDestroy {
|
||||||
|
public readonly dataSource: MatTableDataSource<ViewMediafile> = new MatTableDataSource<ViewMediafile>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the actions for logos. Updated via an observable
|
* Holds the actions for logos. Updated via an observable
|
||||||
*/
|
*/
|
||||||
@ -39,16 +42,18 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
*/
|
*/
|
||||||
public fontActions: string[];
|
public fontActions: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Show or hide the edit mode
|
|
||||||
*/
|
|
||||||
public editFile = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the file to edit
|
* Holds the file to edit
|
||||||
*/
|
*/
|
||||||
public fileToEdit: ViewMediafile;
|
public fileToEdit: ViewMediafile;
|
||||||
|
|
||||||
|
public newDirectoryForm: FormGroup;
|
||||||
|
|
||||||
|
public moveForm: FormGroup;
|
||||||
|
|
||||||
|
public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>;
|
||||||
|
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if the user can manage media files
|
* @returns true if the user can manage media files
|
||||||
*/
|
*/
|
||||||
@ -75,46 +80,14 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
@ViewChild('fileEditDialog', { static: true })
|
@ViewChild('fileEditDialog', { static: true })
|
||||||
public fileEditDialog: TemplateRef<string>;
|
public fileEditDialog: TemplateRef<string>;
|
||||||
|
|
||||||
/**
|
public displayedColumns = ['projector', 'icon', 'title', 'info', 'indicator', 'menu'];
|
||||||
* Define the columns to show
|
|
||||||
*/
|
|
||||||
public tableColumnDefinition: PblColumnDefinition[] = [
|
|
||||||
{
|
|
||||||
prop: 'title',
|
|
||||||
width: 'auto'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'info',
|
|
||||||
width: '20%'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'indicator',
|
|
||||||
width: this.singleButtonWidth
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'menu',
|
|
||||||
width: this.singleButtonWidth
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
public isMultiselect = false; // TODO
|
||||||
* Restricted Columns
|
|
||||||
*/
|
|
||||||
public restrictedColumns: ColumnRestriction[] = [
|
|
||||||
{
|
|
||||||
columnName: 'indicator',
|
|
||||||
permission: 'mediafiles.can_manage'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
columnName: 'menu',
|
|
||||||
permission: 'mediafiles.can_manage'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
private folderSubscription: Subscription;
|
||||||
* Define extra filter properties
|
private directorySubscription: Subscription;
|
||||||
*/
|
public directory: ViewMediafile | null;
|
||||||
public filterProps = ['title', 'type'];
|
public directoryChain: ViewMediafile[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the component
|
* Constructs the component
|
||||||
@ -137,20 +110,29 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
storage: StorageService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
public repo: MediafileRepositoryService,
|
public repo: MediafileRepositoryService,
|
||||||
private mediaManage: MediaManageService,
|
private mediaManage: MediaManageService,
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
public vp: ViewportService,
|
public vp: ViewportService,
|
||||||
public filterService: MediafileFilterListService,
|
|
||||||
public sortService: MediafilesSortListService,
|
public sortService: MediafilesSortListService,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private fb: FormBuilder
|
private fb: FormBuilder,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private groupRepo: GroupRepositoryService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar, storage);
|
super(titleService, translate, matSnackBar);
|
||||||
this.canMultiSelect = true;
|
|
||||||
|
this.newDirectoryForm = this.formBuilder.group({
|
||||||
|
title: ['', Validators.required],
|
||||||
|
access_groups_id: []
|
||||||
|
});
|
||||||
|
this.moveForm = this.formBuilder.group({
|
||||||
|
directory_id: []
|
||||||
|
});
|
||||||
|
this.directoryBehaviorSubject = this.repo.getDirectoryBehaviorSubject();
|
||||||
|
this.groupsBehaviorSubject = this.groupRepo.getViewModelListBehaviorSubject();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -160,6 +142,10 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
super.setTitle('Files');
|
super.setTitle('Files');
|
||||||
|
|
||||||
|
this.repo.getDirectoryIdByPath(this.route.snapshot.url.map(x => x.path)).then(directoryId => {
|
||||||
|
this.changeDirectory(directoryId);
|
||||||
|
});
|
||||||
|
|
||||||
// Observe the logo actions
|
// Observe the logo actions
|
||||||
this.mediaManage.getLogoActions().subscribe(action => {
|
this.mediaManage.getLogoActions().subscribe(action => {
|
||||||
this.logoActions = action;
|
this.logoActions = action;
|
||||||
@ -171,17 +157,44 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public changeDirectory(directoryId: number | null): void {
|
||||||
|
this.clearSubscriptions();
|
||||||
|
|
||||||
|
this.folderSubscription = this.repo.getListObservableDirectory(directoryId).subscribe(mediafiles => {
|
||||||
|
this.dataSource.data = [];
|
||||||
|
this.dataSource.data = mediafiles;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (directoryId) {
|
||||||
|
this.directorySubscription = this.repo.getViewModelObservable(directoryId).subscribe(d => {
|
||||||
|
this.directory = d;
|
||||||
|
if (d) {
|
||||||
|
this.directoryChain = d.getDirectoryChain();
|
||||||
|
// Update the URL.
|
||||||
|
this.router.navigate(['/mediafiles/files/' + d.path], {
|
||||||
|
replaceUrl: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.directoryChain = [];
|
||||||
|
this.router.navigate(['/mediafiles/files/'], {
|
||||||
|
replaceUrl: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.directory = null;
|
||||||
|
this.directoryChain = [];
|
||||||
|
this.router.navigate(['/mediafiles/files/'], {
|
||||||
|
replaceUrl: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for the main Event.
|
|
||||||
* In edit mode, this abandons the changes
|
|
||||||
* Without edit mode, this will navigate to the upload page
|
|
||||||
*/
|
*/
|
||||||
public onMainEvent(): void {
|
public onMainEvent(): void {
|
||||||
if (!this.editFile) {
|
const path = '/mediafiles/upload/' + (this.directory ? this.directory.path : '');
|
||||||
this.router.navigate(['./upload'], { relativeTo: this.route });
|
this.router.navigate([path]);
|
||||||
} else {
|
|
||||||
this.editFile = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,7 +207,7 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
|
|
||||||
this.fileEditForm = this.fb.group({
|
this.fileEditForm = this.fb.group({
|
||||||
title: [file.title, Validators.required],
|
title: [file.title, Validators.required],
|
||||||
hidden: [file.hidden]
|
access_groups_id: [file.access_groups_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(this.fileEditDialog, {
|
const dialogRef = this.dialog.open(this.fileEditDialog, {
|
||||||
@ -214,7 +227,7 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
/**
|
/**
|
||||||
* Click on the save button in edit mode
|
* Click on the save button in edit mode
|
||||||
*/
|
*/
|
||||||
public onSaveEditedFile(value: { title: string; hidden: any }): void {
|
public onSaveEditedFile(value: Partial<Mediafile>): void {
|
||||||
this.repo.update(value, this.fileToEdit).then(() => {
|
this.repo.update(value, this.fileToEdit).then(() => {
|
||||||
this.dialog.closeAll();
|
this.dialog.closeAll();
|
||||||
}, this.raiseError);
|
}, this.raiseError);
|
||||||
@ -233,19 +246,6 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to delete several files at once. Requires data in selectedRows, which
|
|
||||||
* will be made available in multiSelect mode
|
|
||||||
*/
|
|
||||||
public async deleteSelected(): Promise<void> {
|
|
||||||
const title = this.translate.instant('Are you sure you want to delete all selected files?');
|
|
||||||
if (await this.promptService.open(title)) {
|
|
||||||
for (const mediafile of this.selectedRows) {
|
|
||||||
await this.repo.delete(mediafile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the display name of an action
|
* Returns the display name of an action
|
||||||
*
|
*
|
||||||
@ -278,7 +278,7 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
*/
|
*/
|
||||||
public isUsedAs(file: ViewMediafile, mediaFileAction: string): boolean {
|
public isUsedAs(file: ViewMediafile, mediaFileAction: string): boolean {
|
||||||
const config = this.mediaManage.getMediaConfig(mediaFileAction);
|
const config = this.mediaManage.getMediaConfig(mediaFileAction);
|
||||||
return config ? config.path === file.downloadUrl : false;
|
return config ? config.path === file.url : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -312,14 +312,50 @@ export class MediafileListComponent extends BaseListViewComponent<ViewMediafile>
|
|||||||
this.mediaManage.setAs(file, action);
|
this.mediaManage.setAs(file, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public createNewFolder(templateRef: TemplateRef<string>): void {
|
||||||
* Clicking escape while in editFileForm should deactivate edit mode.
|
this.newDirectoryForm.reset();
|
||||||
*
|
const dialogRef = this.dialog.open(templateRef, {
|
||||||
* @param event The key that was pressed
|
width: '400px'
|
||||||
*/
|
});
|
||||||
public keyDownFunction(event: KeyboardEvent): void {
|
|
||||||
if (event.key === 'Escape') {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
this.editFile = false;
|
if (result) {
|
||||||
|
const mediafile = new Mediafile({
|
||||||
|
...this.newDirectoryForm.value,
|
||||||
|
parent_id: this.directory ? this.directory.id : null,
|
||||||
|
is_directory: true
|
||||||
|
});
|
||||||
|
this.repo.create(mediafile).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public move(templateRef: TemplateRef<string>, mediafile: ViewMediafile): void {
|
||||||
|
this.newDirectoryForm.reset();
|
||||||
|
const dialogRef = this.dialog.open(templateRef, {
|
||||||
|
width: '400px'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.repo.move([mediafile], this.moveForm.value.directory_id).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSubscriptions(): void {
|
||||||
|
if (this.folderSubscription) {
|
||||||
|
this.folderSubscription.unsubscribe();
|
||||||
|
this.folderSubscription = null;
|
||||||
|
}
|
||||||
|
if (this.directorySubscription) {
|
||||||
|
this.directorySubscription.unsubscribe();
|
||||||
|
this.directorySubscription = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.clearSubscriptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export const MediafileAppConfig: AppConfig = {
|
|||||||
],
|
],
|
||||||
mainMenuEntries: [
|
mainMenuEntries: [
|
||||||
{
|
{
|
||||||
route: '/mediafiles',
|
route: '/mediafiles/files',
|
||||||
displayName: 'Files',
|
displayName: 'Files',
|
||||||
icon: 'attach_file',
|
icon: 'attach_file',
|
||||||
weight: 600,
|
weight: 600,
|
||||||
|
@ -4,8 +4,17 @@ import { MediafileListComponent } from './components/mediafile-list/mediafile-li
|
|||||||
import { MediaUploadComponent } from './components/media-upload/media-upload.component';
|
import { MediaUploadComponent } from './components/media-upload/media-upload.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: MediafileListComponent, pathMatch: 'full' },
|
{
|
||||||
{ path: 'upload', component: MediaUploadComponent, data: { basePerm: 'mediafiles.can_upload' } }
|
path: 'files',
|
||||||
|
children: [{ path: '**', component: MediafileListComponent }],
|
||||||
|
pathMatch: 'prefix'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'upload',
|
||||||
|
data: { basePerm: 'mediafiles.can_upload' },
|
||||||
|
children: [{ path: '**', component: MediaUploadComponent }],
|
||||||
|
pathMatch: 'prefix'
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -2,10 +2,10 @@ import { BaseViewModel } from '../../base/base-view-model';
|
|||||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||||
import { Searchable } from 'app/site/base/searchable';
|
import { Searchable } from 'app/site/base/searchable';
|
||||||
import { SearchRepresentation } from 'app/core/ui-services/search.service';
|
import { SearchRepresentation } from 'app/core/ui-services/search.service';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
|
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
|
||||||
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
|
||||||
export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||||
export const FONT_MIMETYPES = ['font/ttf', 'font/woff', 'application/font-woff', 'application/font-sfnt'];
|
export const FONT_MIMETYPES = ['font/ttf', 'font/woff', 'application/font-woff', 'application/font-sfnt'];
|
||||||
@ -19,76 +19,97 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
implements MediafileTitleInformation, Searchable {
|
implements MediafileTitleInformation, Searchable {
|
||||||
public static COLLECTIONSTRING = Mediafile.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = Mediafile.COLLECTIONSTRING;
|
||||||
|
|
||||||
private _uploader: ViewUser;
|
private _parent?: ViewMediafile;
|
||||||
|
private _access_groups?: ViewGroup[];
|
||||||
|
private _inherited_access_groups?: ViewGroup[];
|
||||||
|
|
||||||
public get mediafile(): Mediafile {
|
public get mediafile(): Mediafile {
|
||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get uploader(): ViewUser {
|
public get parent(): ViewMediafile | null {
|
||||||
return this._uploader;
|
return this._parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get uploader_id(): number {
|
public get access_groups(): ViewGroup[] {
|
||||||
return this.mediafile.uploader_id;
|
return this._access_groups || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get access_groups_id(): number[] {
|
||||||
|
return this.mediafile.access_groups_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get inherited_access_groups(): ViewGroup[] | null {
|
||||||
|
return this._inherited_access_groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get inherited_access_groups_id(): boolean | number[] {
|
||||||
|
return this.mediafile.inherited_access_groups_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get has_inherited_access_groups(): boolean {
|
||||||
|
return this.mediafile.has_inherited_access_groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get title(): string {
|
public get title(): string {
|
||||||
return this.mediafile.title;
|
return this.mediafile.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get size(): string {
|
public get path(): string {
|
||||||
return this.mediafile.filesize;
|
return this.mediafile.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get type(): string {
|
public get parent_id(): number {
|
||||||
return this.mediafile.mediafile.type;
|
return this.mediafile.parent_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get is_directory(): boolean {
|
||||||
|
return this.mediafile.is_directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get is_file(): boolean {
|
||||||
|
return !this.is_directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get size(): string {
|
||||||
|
return this.mediafile.filesize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get prefix(): string {
|
public get prefix(): string {
|
||||||
return this.mediafile.media_url_prefix;
|
return this.mediafile.media_url_prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hidden(): boolean {
|
public get url(): string {
|
||||||
return this.mediafile.hidden;
|
return this.mediafile.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get fileName(): string {
|
public get type(): string {
|
||||||
return this.mediafile.mediafile.name;
|
return this.mediafile.mediafile ? this.mediafile.mediafile.type : '';
|
||||||
}
|
|
||||||
|
|
||||||
public get downloadUrl(): string {
|
|
||||||
return this.mediafile.downloadUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get pages(): number | null {
|
public get pages(): number | null {
|
||||||
return this.mediafile.mediafile.pages;
|
return this.mediafile.mediafile ? this.mediafile.mediafile.pages : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public constructor(
|
||||||
* Determines if the file has the 'hidden' attribute
|
mediafile: Mediafile,
|
||||||
* @returns the hidden attribute, also 'hidden' if there is no file
|
listOfSpeakers?: ViewListOfSpeakers,
|
||||||
* TODO Which is the expected behavior for 'no file'?
|
parent?: ViewMediafile,
|
||||||
*/
|
access_groups?: ViewGroup[],
|
||||||
public get is_hidden(): boolean {
|
inherited_access_groups?: ViewGroup[]
|
||||||
return this.mediafile.hidden;
|
) {
|
||||||
}
|
|
||||||
|
|
||||||
public constructor(mediafile: Mediafile, listOfSpeakers?: ViewListOfSpeakers, uploader?: ViewUser) {
|
|
||||||
super(Mediafile.COLLECTIONSTRING, mediafile, listOfSpeakers);
|
super(Mediafile.COLLECTIONSTRING, mediafile, listOfSpeakers);
|
||||||
this._uploader = uploader;
|
this._parent = parent;
|
||||||
|
this._access_groups = access_groups;
|
||||||
|
this._inherited_access_groups = inherited_access_groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public formatForSearch(): SearchRepresentation {
|
public formatForSearch(): SearchRepresentation {
|
||||||
const searchValues = [this.title];
|
return [this.title, this.path];
|
||||||
if (this.uploader) {
|
|
||||||
searchValues.push(this.uploader.full_name);
|
|
||||||
}
|
|
||||||
return searchValues;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDetailStateURL(): string {
|
public getDetailStateURL(): string {
|
||||||
return this.downloadUrl;
|
return this.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
@ -104,6 +125,12 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDirectoryChain(): ViewMediafile[] {
|
||||||
|
const parentChain = this.parent ? this.parent.getDirectoryChain() : [];
|
||||||
|
parentChain.push(this);
|
||||||
|
return parentChain;
|
||||||
|
}
|
||||||
|
|
||||||
public isProjectable(): boolean {
|
public isProjectable(): boolean {
|
||||||
return this.isImage() || this.isPdf();
|
return this.isImage() || this.isPdf();
|
||||||
}
|
}
|
||||||
@ -156,19 +183,44 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
].includes(this.type);
|
].includes(this.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getIcon(): string {
|
||||||
* Determine if the file is presentable
|
if (this.is_directory) {
|
||||||
*
|
return 'folder';
|
||||||
* @returns true or false
|
} else if (this.isPdf()) {
|
||||||
*/
|
return 'picture_as_pdf';
|
||||||
public isPresentable(): boolean {
|
} else if (this.isImage()) {
|
||||||
return this.isPdf() || this.isImage() || this.isVideo();
|
return 'insert_photo';
|
||||||
|
} else if (this.isFont()) {
|
||||||
|
return 'text_fields';
|
||||||
|
} else if (this.isVideo()) {
|
||||||
|
return 'movie';
|
||||||
|
} else {
|
||||||
|
return 'insert_drive_file';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateDependencies(update: BaseViewModel): void {
|
public updateDependencies(update: BaseViewModel): void {
|
||||||
super.updateDependencies(update);
|
super.updateDependencies(update);
|
||||||
if (update instanceof ViewUser && this.uploader_id === update.id) {
|
if (update instanceof ViewMediafile && update.id === this.parent_id) {
|
||||||
this._uploader = update;
|
this._parent = update;
|
||||||
|
} else if (update instanceof ViewGroup) {
|
||||||
|
if (this.access_groups_id.includes(update.id)) {
|
||||||
|
const groupIndex = this.access_groups.findIndex(group => group.id === update.id);
|
||||||
|
if (groupIndex < 0) {
|
||||||
|
this.access_groups.push(update);
|
||||||
|
} else {
|
||||||
|
this.access_groups[groupIndex] = update;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.has_inherited_access_groups && (<number[]>this.inherited_access_groups_id).includes(update.id)) {
|
||||||
|
const groupIndex = this.inherited_access_groups.findIndex(group => group.id === update.id);
|
||||||
|
if (groupIndex < 0) {
|
||||||
|
this.inherited_access_groups.push(update);
|
||||||
|
} else {
|
||||||
|
this.inherited_access_groups[groupIndex] = update;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
|
||||||
import { StorageService } from 'app/core/core-services/storage.service';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { ViewMediafile } from '../models/view-mediafile';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter service for media files
|
|
||||||
*/
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class MediafileFilterListService extends BaseFilterListService<ViewMediafile> {
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
* Sets the filter options according to permissions
|
|
||||||
*
|
|
||||||
* @param store
|
|
||||||
* @param operator
|
|
||||||
* @param translate
|
|
||||||
*/
|
|
||||||
public constructor(store: StorageService, private operator: OperatorService, private translate: TranslateService) {
|
|
||||||
super('Mediafiles', store);
|
|
||||||
|
|
||||||
this.operator.getUserObservable().subscribe(() => {
|
|
||||||
this.setFilterDefinitions();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns the filter definition
|
|
||||||
*/
|
|
||||||
protected getFilterDefinitions(): OsFilter[] {
|
|
||||||
const pdfOption: OsFilter = {
|
|
||||||
property: 'type',
|
|
||||||
label: 'PDF',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
condition: 'application/pdf',
|
|
||||||
label: this.translate.instant('Is PDF file')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: null,
|
|
||||||
label: this.translate.instant('Is no PDF file')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const hiddenOptions: OsFilter = {
|
|
||||||
property: 'is_hidden',
|
|
||||||
label: this.translate.instant('Visibility'),
|
|
||||||
options: [
|
|
||||||
{ condition: true, label: this.translate.instant('is hidden') },
|
|
||||||
{ condition: false, label: this.translate.instant('is not hidden') }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.operator.hasPerms('mediafiles.can_see_hidden') ? [hiddenOptions, pdfOption] : [pdfOption];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1361,7 +1361,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
|
|||||||
* @param attachment the selected file
|
* @param attachment the selected file
|
||||||
*/
|
*/
|
||||||
public onClickAttachment(attachment: Mediafile): void {
|
public onClickAttachment(attachment: Mediafile): void {
|
||||||
window.open(attachment.downloadUrl);
|
window.open(attachment.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,7 +82,7 @@ export class MotionExportDialogComponent implements OnInit {
|
|||||||
* @returns a list of availavble commentSections
|
* @returns a list of availavble commentSections
|
||||||
*/
|
*/
|
||||||
public get commentsToExport(): ViewMotionCommentSection[] {
|
public get commentsToExport(): ViewMotionCommentSection[] {
|
||||||
return this.commentRepo.getSortedViewModelList();
|
return this.commentRepo.getViewModelList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,7 +93,7 @@ export class MotionMultiselectService {
|
|||||||
*/
|
*/
|
||||||
public async moveToItem(motions: ViewMotion[]): Promise<void> {
|
public async moveToItem(motions: ViewMotion[]): Promise<void> {
|
||||||
const title = this.translate.instant('This will move all selected motions as childs to:');
|
const title = this.translate.instant('This will move all selected motions as childs to:');
|
||||||
const choices: (Displayable & Identifiable)[] = this.agendaRepo.getSortedViewModelList();
|
const choices: (Displayable & Identifiable)[] = this.agendaRepo.getViewModelList();
|
||||||
const selectedChoice = await this.choiceService.open(title, choices);
|
const selectedChoice = await this.choiceService.open(title, choices);
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
const requestData = {
|
const requestData = {
|
||||||
@ -173,7 +173,7 @@ export class MotionMultiselectService {
|
|||||||
const clearChoice = this.translate.instant('No category');
|
const clearChoice = this.translate.instant('No category');
|
||||||
const selectedChoice = await this.choiceService.open(
|
const selectedChoice = await this.choiceService.open(
|
||||||
title,
|
title,
|
||||||
this.categoryRepo.getSortedViewModelList(),
|
this.categoryRepo.getViewModelList(),
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
clearChoice
|
clearChoice
|
||||||
@ -199,12 +199,7 @@ export class MotionMultiselectService {
|
|||||||
'This will add or remove the following submitters for all selected motions:'
|
'This will add or remove the following submitters for all selected motions:'
|
||||||
);
|
);
|
||||||
const choices = [this.translate.instant('Add'), this.translate.instant('Remove')];
|
const choices = [this.translate.instant('Add'), this.translate.instant('Remove')];
|
||||||
const selectedChoice = await this.choiceService.open(
|
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true, choices);
|
||||||
title,
|
|
||||||
this.userRepo.getSortedViewModelList(),
|
|
||||||
true,
|
|
||||||
choices
|
|
||||||
);
|
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
let requestData = null;
|
let requestData = null;
|
||||||
if (selectedChoice.action === choices[0]) {
|
if (selectedChoice.action === choices[0]) {
|
||||||
@ -247,12 +242,7 @@ export class MotionMultiselectService {
|
|||||||
this.translate.instant('Remove'),
|
this.translate.instant('Remove'),
|
||||||
this.translate.instant('Clear tags')
|
this.translate.instant('Clear tags')
|
||||||
];
|
];
|
||||||
const selectedChoice = await this.choiceService.open(
|
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true, choices);
|
||||||
title,
|
|
||||||
this.tagRepo.getSortedViewModelList(),
|
|
||||||
true,
|
|
||||||
choices
|
|
||||||
);
|
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
let requestData = null;
|
let requestData = null;
|
||||||
if (selectedChoice.action === choices[0]) {
|
if (selectedChoice.action === choices[0]) {
|
||||||
@ -301,7 +291,7 @@ export class MotionMultiselectService {
|
|||||||
const clearChoice = this.translate.instant('Clear motion block');
|
const clearChoice = this.translate.instant('Clear motion block');
|
||||||
const selectedChoice = await this.choiceService.open(
|
const selectedChoice = await this.choiceService.open(
|
||||||
title,
|
title,
|
||||||
this.motionBlockRepo.getSortedViewModelList(),
|
this.motionBlockRepo.getViewModelList(),
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
clearChoice
|
clearChoice
|
||||||
|
@ -154,7 +154,7 @@ export class MotionPdfService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (infoToExport && infoToExport.includes('allcomments')) {
|
if (infoToExport && infoToExport.includes('allcomments')) {
|
||||||
commentsToExport = this.commentRepo.getSortedViewModelList().map(vm => vm.id);
|
commentsToExport = this.commentRepo.getViewModelList().map(vm => vm.id);
|
||||||
}
|
}
|
||||||
if (commentsToExport) {
|
if (commentsToExport) {
|
||||||
motionPdfContent.push(this.createComments(motion, commentsToExport));
|
motionPdfContent.push(this.createComments(motion, commentsToExport));
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<span translate>Attachments</span>:
|
<span translate>Attachments</span>:
|
||||||
<mat-list dense>
|
<mat-list dense>
|
||||||
<mat-list-item *ngFor="let file of topic.attachments">
|
<mat-list-item *ngFor="let file of topic.attachments">
|
||||||
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.getTitle() }}</a>
|
<a [routerLink]="file.url" target="_blank">{{ file.getTitle() }}</a>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -141,7 +141,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.createForm();
|
this.createForm();
|
||||||
|
|
||||||
this.groups = this.groupRepo.getSortedViewModelList().filter(group => group.id !== 1);
|
this.groups = this.groupRepo.getViewModelList().filter(group => group.id !== 1);
|
||||||
this.groupRepo
|
this.groupRepo
|
||||||
.getViewModelListObservable()
|
.getViewModelListObservable()
|
||||||
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
|
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
|
||||||
|
@ -186,7 +186,7 @@ export class UserListComponent extends BaseListViewComponent<ViewUser> implement
|
|||||||
super.setTitle('Participants');
|
super.setTitle('Participants');
|
||||||
|
|
||||||
// Initialize the groups
|
// Initialize the groups
|
||||||
this.groups = this.groupRepo.getSortedViewModelList().filter(group => group.id !== 1);
|
this.groups = this.groupRepo.getViewModelList().filter(group => group.id !== 1);
|
||||||
this.groupRepo
|
this.groupRepo
|
||||||
.getViewModelListObservable()
|
.getViewModelListObservable()
|
||||||
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
|
.subscribe(groups => (this.groups = groups.filter(group => group.id !== 1)));
|
||||||
|
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="speaker",
|
model_name="speaker",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -15,7 +15,7 @@ from openslides.utils.exceptions import OpenSlidesError
|
|||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.utils import to_roman
|
from openslides.utils.utils import to_roman
|
||||||
|
|
||||||
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import ItemAccessPermissions, ListOfSpeakersAccessPermissions
|
from .access_permissions import ItemAccessPermissions, ListOfSpeakersAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -445,7 +445,7 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
objects = SpeakerManager()
|
objects = SpeakerManager()
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUPDATE)
|
||||||
"""
|
"""
|
||||||
ForeinKey to the user who speaks.
|
ForeinKey to the user who speaks.
|
||||||
"""
|
"""
|
||||||
|
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="assignmentrelateduser",
|
model_name="assignmentrelateduser",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -22,7 +22,7 @@ from openslides.utils.autoupdate import inform_changed_data
|
|||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
|
|
||||||
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import AssignmentAccessPermissions
|
from .access_permissions import AssignmentAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
|||||||
ForeinKey to the assignment.
|
ForeinKey to the assignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUPDATE)
|
||||||
"""
|
"""
|
||||||
ForeinKey to the user who is related to the assignment.
|
ForeinKey to the user who is related to the assignment.
|
||||||
"""
|
"""
|
||||||
|
@ -16,7 +16,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="chatmessage",
|
model_name="chatmessage",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, cast
|
||||||
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
from ..utils.auth import async_has_perm
|
from ..utils.auth import async_has_perm, async_in_some_groups
|
||||||
|
|
||||||
|
|
||||||
class MediafileAccessPermissions(BaseAccessPermissions):
|
class MediafileAccessPermissions(BaseAccessPermissions):
|
||||||
@ -18,15 +18,15 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
|||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Removes hidden mediafiles for some users.
|
for the user. Removes hidden mediafiles for some users.
|
||||||
"""
|
"""
|
||||||
# Parse data.
|
if not await async_has_perm(user_id, "mediafiles.can_see"):
|
||||||
if await async_has_perm(user_id, "mediafiles.can_see") and await async_has_perm(
|
return []
|
||||||
user_id, "mediafiles.can_see_hidden"
|
|
||||||
):
|
|
||||||
data = full_data
|
|
||||||
elif await async_has_perm(user_id, "mediafiles.can_see"):
|
|
||||||
# Exclude hidden mediafiles.
|
|
||||||
data = [full for full in full_data if not full["hidden"]]
|
|
||||||
else:
|
|
||||||
data = []
|
data = []
|
||||||
|
for full in full_data:
|
||||||
|
access_groups = full["inherited_access_groups_id"]
|
||||||
|
if (
|
||||||
|
isinstance(access_groups, bool) and access_groups
|
||||||
|
) or await async_in_some_groups(user_id, cast(List[int], access_groups)):
|
||||||
|
data.append(full)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from typing import Any, Dict, Set
|
from typing import Any, Dict, Set
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
class MediafilesAppConfig(AppConfig):
|
class MediafilesAppConfig(AppConfig):
|
||||||
@ -17,6 +19,14 @@ class MediafilesAppConfig(AppConfig):
|
|||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from ..utils.access_permissions import required_user
|
from ..utils.access_permissions import required_user
|
||||||
|
|
||||||
|
# Validate, that the media_url is correct formatted:
|
||||||
|
# Must begin and end with a slash. It has to be at least "/".
|
||||||
|
media_url = settings.MEDIA_URL
|
||||||
|
if not media_url.startswith("/") or not media_url.endswith("/"):
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"The MEDIA_URL setting must start and end with a slash"
|
||||||
|
)
|
||||||
|
|
||||||
# Define projector elements.
|
# Define projector elements.
|
||||||
register_projector_slides()
|
register_projector_slides()
|
||||||
|
|
||||||
|
54
openslides/mediafiles/config.py
Normal file
54
openslides/mediafiles/config.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from ..core.config import config
|
||||||
|
from .models import Mediafile
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def watch_and_update_configs():
|
||||||
|
"""
|
||||||
|
Watches each font and logo config for changes. If some mediafiles were updated
|
||||||
|
(also their parents, so some path changes) or were deleted, all affected configs
|
||||||
|
are updated.
|
||||||
|
"""
|
||||||
|
# 1) map logo and font config keys to mediafile ids
|
||||||
|
mediafiles = Mediafile.objects.get_full_queryset().all()
|
||||||
|
logos = build_mapping("logos_available", mediafiles)
|
||||||
|
fonts = build_mapping("fonts_available", mediafiles)
|
||||||
|
yield
|
||||||
|
# 2) update changed paths/urls
|
||||||
|
mediafiles = Mediafile.objects.get_full_queryset().all()
|
||||||
|
update_mapping(logos, mediafiles)
|
||||||
|
update_mapping(fonts, mediafiles)
|
||||||
|
|
||||||
|
|
||||||
|
def build_mapping(base_config_key, mediafiles):
|
||||||
|
""" Returns a map of config keys to medaifile ids """
|
||||||
|
logos = {}
|
||||||
|
for key in config[base_config_key]:
|
||||||
|
url = config[key]["path"]
|
||||||
|
|
||||||
|
for mediafile in mediafiles:
|
||||||
|
if mediafile.url == url:
|
||||||
|
logos[key] = mediafile.id
|
||||||
|
break
|
||||||
|
return logos
|
||||||
|
|
||||||
|
|
||||||
|
def update_mapping(mapping, mediafiles):
|
||||||
|
"""
|
||||||
|
Tries to get the mediafile from the id for a specific config field.
|
||||||
|
If the file was found and the path changed, the config is updated. If the
|
||||||
|
mediafile cound not be found, the config is cleared (mediafile deleted).
|
||||||
|
"""
|
||||||
|
for key, id in mapping.items():
|
||||||
|
try:
|
||||||
|
mediafile = mediafiles.filter(pk=id)[0]
|
||||||
|
print(config[key]["path"], mediafile.url)
|
||||||
|
if config[key]["path"] != mediafile.url:
|
||||||
|
config[key] = {
|
||||||
|
"display_name": config[key]["display_name"],
|
||||||
|
"path": mediafile.url,
|
||||||
|
}
|
||||||
|
except IndexError:
|
||||||
|
config[key] = {"display_name": config[key]["display_name"], "path": ""}
|
@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 2.2.2 on 2019-06-28 06:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.mediafiles.models
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("mediafiles", "0003_auto_20190119_1425")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="mediafile",
|
||||||
|
options={
|
||||||
|
"default_permissions": (),
|
||||||
|
"ordering": ("title",),
|
||||||
|
"permissions": (
|
||||||
|
("can_see", "Can see the list of files"),
|
||||||
|
("can_manage", "Can manage files"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="mediafile", old_name="timestamp", new_name="create_timestamp"
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="mediafile",
|
||||||
|
name="access_groups",
|
||||||
|
field=models.ManyToManyField(blank=True, to="users.Group"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="mediafile",
|
||||||
|
name="is_directory",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="mediafile",
|
||||||
|
name="parent",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.deletion.SET_NULL,
|
||||||
|
related_name="children",
|
||||||
|
to="mediafiles.Mediafile",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="mediafile",
|
||||||
|
name="original_filename",
|
||||||
|
field=models.CharField(default="", max_length=255),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mediafile",
|
||||||
|
name="mediafile",
|
||||||
|
field=models.FileField(
|
||||||
|
null=True, upload_to=openslides.mediafiles.models.get_file_path
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="mediafile", name="title", field=models.CharField(max_length=255)
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 2.2.2 on 2019-06-28 06:09
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def copy_filename(apps, schema_editor):
|
||||||
|
Mediafile = apps.get_model("mediafiles", "Mediafile")
|
||||||
|
for mediafile in Mediafile.objects.all():
|
||||||
|
filename = os.path.basename(mediafile.mediafile.name)
|
||||||
|
mediafile.original_filename = filename
|
||||||
|
mediafile.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
|
||||||
|
def set_groups_and_delete_old_permissions(apps, schema_editor):
|
||||||
|
Mediafile = apps.get_model("mediafiles", "Mediafile")
|
||||||
|
mediafile_content_type = ContentType.objects.get(model="mediafile")
|
||||||
|
try:
|
||||||
|
can_see_hidden = Permission.objects.get(
|
||||||
|
codename="can_see_hidden", content_type=mediafile_content_type
|
||||||
|
)
|
||||||
|
group_ids = [group.id for group in can_see_hidden.group_set.all()]
|
||||||
|
for mediafile in Mediafile.objects.filter(hidden=True):
|
||||||
|
mediafile.access_groups.set(group_ids)
|
||||||
|
mediafile.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
# Delete permissions
|
||||||
|
can_see_hidden.delete()
|
||||||
|
Permission.objects.filter(
|
||||||
|
codename="can_upload", content_type=mediafile_content_type
|
||||||
|
).delete()
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("mediafiles", "0004_directories_and_permissions_1")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(copy_filename),
|
||||||
|
migrations.RunPython(set_groups_and_delete_old_permissions),
|
||||||
|
]
|
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 2.2.2 on 2019-06-28 06:06
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0010_auto_20190119_1447"),
|
||||||
|
("mediafiles", "0005_directories_and_permissions_2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="mediafile", name="hidden"),
|
||||||
|
migrations.RemoveField(model_name="mediafile", name="uploader"),
|
||||||
|
]
|
@ -1,10 +1,14 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from typing import List, cast
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from ..agenda.mixins import ListOfSpeakersMixin
|
from ..agenda.mixins import ListOfSpeakersMixin
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.models import RESTModelMixin
|
||||||
from ..utils.models import SET_NULL_AND_AUTOUPDATE, RESTModelMixin
|
from ..utils.rest_api import ValidationError
|
||||||
from .access_permissions import MediafileAccessPermissions
|
from .access_permissions import MediafileAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +22,21 @@ class MediafileManager(models.Manager):
|
|||||||
Returns the normal queryset with all mediafiles. In the background
|
Returns the normal queryset with all mediafiles. In the background
|
||||||
all related list of speakers are prefetched from the database.
|
all related list of speakers are prefetched from the database.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().prefetch_related("lists_of_speakers")
|
return self.get_queryset().prefetch_related(
|
||||||
|
"lists_of_speakers", "parent", "access_groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Do not use the querysets delete function. Please delete every mediafile on it's own."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_path(mediafile, filename):
|
||||||
|
mediafile.original_filename = filename
|
||||||
|
ext = filename.split(".")[-1]
|
||||||
|
filename = "%s.%s" % (uuid.uuid4(), ext)
|
||||||
|
return os.path.join("file", filename)
|
||||||
|
|
||||||
|
|
||||||
class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
||||||
@ -30,55 +48,140 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
|||||||
access_permissions = MediafileAccessPermissions()
|
access_permissions = MediafileAccessPermissions()
|
||||||
can_see_permission = "mediafiles.can_see"
|
can_see_permission = "mediafiles.can_see"
|
||||||
|
|
||||||
mediafile = models.FileField(upload_to="file")
|
mediafile = models.FileField(upload_to=get_file_path, null=True)
|
||||||
"""
|
"""
|
||||||
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
||||||
for more information.
|
for more information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title = models.CharField(max_length=255, unique=True)
|
title = models.CharField(max_length=255)
|
||||||
"""A string representing the title of the file."""
|
"""A string representing the title of the file."""
|
||||||
|
|
||||||
uploader = models.ForeignKey(
|
original_filename = models.CharField(max_length=255)
|
||||||
settings.AUTH_USER_MODEL, on_delete=SET_NULL_AND_AUTOUPDATE, null=True
|
|
||||||
)
|
|
||||||
"""A user – the uploader of a file."""
|
|
||||||
|
|
||||||
hidden = models.BooleanField(default=False)
|
create_timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
"""Whether or not this mediafile should be marked as hidden"""
|
|
||||||
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
|
||||||
"""A DateTimeField to save the upload date and time."""
|
"""A DateTimeField to save the upload date and time."""
|
||||||
|
|
||||||
|
is_directory = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
"self",
|
||||||
|
# The on_delete should be CASCADE_AND_AUTOUPDATE, but we do
|
||||||
|
# have to delete the actual file from every mediafile to ensure
|
||||||
|
# cleaning up the server files. This is ensured by the custom delete
|
||||||
|
# method of every mediafile. Do not use the delete method of the
|
||||||
|
# mediafile manager.
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="children",
|
||||||
|
)
|
||||||
|
|
||||||
|
access_groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""
|
"""
|
||||||
Meta class for the mediafile model.
|
Meta class for the mediafile model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ordering = ["title"]
|
ordering = ("title",)
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
("can_see", "Can see the list of files"),
|
("can_see", "Can see the list of files"),
|
||||||
("can_see_hidden", "Can see hidden files"),
|
|
||||||
("can_upload", "Can upload files"),
|
|
||||||
("can_manage", "Can manage files"),
|
("can_manage", "Can manage files"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def create(self, *args, **kwargs):
|
||||||
|
self.validate_unique()
|
||||||
|
return super().create(*args, **kwargs)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.validate_unique()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate_unique(self):
|
||||||
|
"""
|
||||||
|
`unique_together` is not working with foreign keys with possible `null` values.
|
||||||
|
So we do need to check this here.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
Mediafile.objects.exclude(pk=self.pk)
|
||||||
|
.filter(title=self.title, parent=self.parent)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "A mediafile with this title already exists in this folder."}
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
Method for representation.
|
Method for representation.
|
||||||
"""
|
"""
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def delete(self, skip_autoupdate=False):
|
||||||
|
mediafiles_to_delete = self.get_children_deep()
|
||||||
|
mediafiles_to_delete.append(self)
|
||||||
|
for mediafile in mediafiles_to_delete:
|
||||||
|
if mediafile.is_file:
|
||||||
|
# To avoid Django calling save() and triggering autoupdate we do not
|
||||||
|
# use the builtin method mediafile.mediafile.delete() but call
|
||||||
|
# mediafile.mediafile.storage.delete(...) directly. This may have
|
||||||
|
# unattended side effects so be careful especially when accessing files
|
||||||
|
# on server via Django methods (file, open(), save(), ...).
|
||||||
|
mediafile.mediafile.storage.delete(mediafile.mediafile.name)
|
||||||
|
mediafile._db_delete(skip_autoupdate=skip_autoupdate)
|
||||||
|
|
||||||
|
def _db_delete(self, *args, **kwargs):
|
||||||
|
""" Captures the original .delete() method. """
|
||||||
|
return super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_children_deep(self):
|
||||||
|
""" Returns all children and all children of childrens and so forth. """
|
||||||
|
children = []
|
||||||
|
for child in self.children.all():
|
||||||
|
children.append(child)
|
||||||
|
children.extend(child.get_children_deep())
|
||||||
|
return children
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
name = (self.title + "/") if self.is_directory else self.original_filename
|
||||||
|
if self.parent:
|
||||||
|
return self.parent.path + name
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return settings.MEDIA_URL + self.path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inherited_access_groups_id(self):
|
||||||
"""
|
"""
|
||||||
Saves mediafile (mainly on create and update requests).
|
True: all groups
|
||||||
|
False: no permissions
|
||||||
|
List[int]: Groups with permissions
|
||||||
"""
|
"""
|
||||||
result = super().save(*args, **kwargs)
|
own_access_groups = [group.id for group in self.access_groups.all()]
|
||||||
# Send uploader via autoupdate because users without permission
|
if not self.parent:
|
||||||
# to see users may not have it but can get it now.
|
return own_access_groups or True # either some groups or all
|
||||||
inform_changed_data(self.uploader)
|
|
||||||
return result
|
access_groups = self.parent.inherited_access_groups_id
|
||||||
|
if len(own_access_groups) > 0:
|
||||||
|
if isinstance(access_groups, bool) and access_groups:
|
||||||
|
return own_access_groups
|
||||||
|
elif isinstance(access_groups, bool) and not access_groups:
|
||||||
|
return False
|
||||||
|
else: # List[int]
|
||||||
|
access_groups = [
|
||||||
|
id
|
||||||
|
for id in cast(List[int], access_groups)
|
||||||
|
if id in own_access_groups
|
||||||
|
]
|
||||||
|
return access_groups or False
|
||||||
|
else:
|
||||||
|
return access_groups # We do not have restrictions, copy from parent.
|
||||||
|
|
||||||
def get_filesize(self):
|
def get_filesize(self):
|
||||||
"""
|
"""
|
||||||
@ -89,6 +192,9 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
|||||||
size = self.mediafile.size
|
size = self.mediafile.size
|
||||||
except OSError:
|
except OSError:
|
||||||
size_string = "unknown"
|
size_string = "unknown"
|
||||||
|
except ValueError:
|
||||||
|
# happens, if this is a directory and no file exists
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
if size < 1024:
|
if size < 1024:
|
||||||
size_string = "< 1 kB"
|
size_string = "< 1 kB"
|
||||||
@ -100,17 +206,31 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
|||||||
size_string = "%d kB" % kB
|
size_string = "%d kB" % kB
|
||||||
return size_string
|
return size_string
|
||||||
|
|
||||||
|
@property
|
||||||
def is_logo(self):
|
def is_logo(self):
|
||||||
|
if self.is_directory:
|
||||||
|
return False
|
||||||
for key in config["logos_available"]:
|
for key in config["logos_available"]:
|
||||||
if config[key]["path"] == self.mediafile.url:
|
if config[key]["path"] == self.url:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
def is_font(self):
|
def is_font(self):
|
||||||
|
if self.is_directory:
|
||||||
|
return False
|
||||||
for key in config["fonts_available"]:
|
for key in config["fonts_available"]:
|
||||||
if config[key]["path"] == self.mediafile.url:
|
if config[key]["path"] == self.url:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_special_file(self):
|
||||||
|
return self.is_logo or self.is_font
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_file(self):
|
||||||
|
return not self.is_directory
|
||||||
|
|
||||||
def get_list_of_speakers_title_information(self):
|
def get_list_of_speakers_title_information(self):
|
||||||
return {"title": self.title}
|
return {"title": self.title}
|
||||||
|
@ -31,7 +31,7 @@ async def mediafile_slide(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"path": mediafile["mediafile"]["name"],
|
"path": mediafile["path"],
|
||||||
"type": mediafile["mediafile"]["type"],
|
"type": mediafile["mediafile"]["type"],
|
||||||
"media_url_prefix": mediafile["media_url_prefix"],
|
"media_url_prefix": mediafile["media_url_prefix"],
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,14 @@ from django.db import models as dbmodels
|
|||||||
from PyPDF2 import PdfFileReader
|
from PyPDF2 import PdfFileReader
|
||||||
from PyPDF2.utils import PdfReadError
|
from PyPDF2.utils import PdfReadError
|
||||||
|
|
||||||
from ..utils.rest_api import FileField, ModelSerializer, SerializerMethodField
|
from ..utils.auth import get_group_model
|
||||||
|
from ..utils.rest_api import (
|
||||||
|
FileField,
|
||||||
|
IdPrimaryKeyRelatedField,
|
||||||
|
ModelSerializer,
|
||||||
|
SerializerMethodField,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
|
|
||||||
|
|
||||||
@ -16,13 +23,22 @@ class AngularCompatibleFileField(FileField):
|
|||||||
return super(AngularCompatibleFileField, self).to_internal_value(data)
|
return super(AngularCompatibleFileField, self).to_internal_value(data)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
if value is None or value.name is None:
|
||||||
return None
|
return None
|
||||||
filetype = mimetypes.guess_type(value.path)[0]
|
filetype = mimetypes.guess_type(value.name)[0]
|
||||||
result = {"name": value.name, "type": filetype}
|
result = {"name": value.name, "type": filetype}
|
||||||
if filetype == "application/pdf":
|
if filetype == "application/pdf":
|
||||||
try:
|
try:
|
||||||
result["pages"] = PdfFileReader(open(value.path, "rb")).getNumPages()
|
if (
|
||||||
|
settings.DEFAULT_FILE_STORAGE
|
||||||
|
== "storages.backends.sftpstorage.SFTPStorage"
|
||||||
|
):
|
||||||
|
remote_path = value.storage._remote_path(value.name)
|
||||||
|
file_handle = value.storage.sftp.open(remote_path, mode="rb")
|
||||||
|
else:
|
||||||
|
file_handle = open(value.path, "rb")
|
||||||
|
|
||||||
|
result["pages"] = PdfFileReader(file_handle).getNumPages()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# File was deleted from server. Set 'pages' to 0.
|
# File was deleted from server. Set 'pages' to 0.
|
||||||
result["pages"] = 0
|
result["pages"] = 0
|
||||||
@ -40,6 +56,9 @@ class MediafileSerializer(ModelSerializer):
|
|||||||
|
|
||||||
media_url_prefix = SerializerMethodField()
|
media_url_prefix = SerializerMethodField()
|
||||||
filesize = SerializerMethodField()
|
filesize = SerializerMethodField()
|
||||||
|
access_groups = IdPrimaryKeyRelatedField(
|
||||||
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -48,6 +67,8 @@ class MediafileSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
super(MediafileSerializer, self).__init__(*args, **kwargs)
|
super(MediafileSerializer, self).__init__(*args, **kwargs)
|
||||||
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
|
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
|
||||||
|
|
||||||
|
# Make some fields read-oinly for updates (not creation)
|
||||||
if self.instance is not None:
|
if self.instance is not None:
|
||||||
self.fields["mediafile"].read_only = True
|
self.fields["mediafile"].read_only = True
|
||||||
|
|
||||||
@ -58,13 +79,49 @@ class MediafileSerializer(ModelSerializer):
|
|||||||
"title",
|
"title",
|
||||||
"mediafile",
|
"mediafile",
|
||||||
"media_url_prefix",
|
"media_url_prefix",
|
||||||
"uploader",
|
|
||||||
"filesize",
|
"filesize",
|
||||||
"hidden",
|
"access_groups",
|
||||||
"timestamp",
|
"create_timestamp",
|
||||||
|
"is_directory",
|
||||||
|
"path",
|
||||||
|
"parent",
|
||||||
"list_of_speakers_id",
|
"list_of_speakers_id",
|
||||||
|
"inherited_access_groups_id",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
read_only_fields = ("path",)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
title = data.get("title")
|
||||||
|
if title is not None and not title:
|
||||||
|
raise ValidationError({"detail": "The title must not be empty"})
|
||||||
|
|
||||||
|
parent = data.get("parent")
|
||||||
|
if parent and not parent.is_directory:
|
||||||
|
raise ValidationError({"detail": "parent must be a directory."})
|
||||||
|
|
||||||
|
if data.get("is_directory") and "/" in data.get("title", ""):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": 'The name contains invalid characters: "/"'}
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().validate(data)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
access_groups = validated_data.pop("access_groups", [])
|
||||||
|
mediafile = super().create(validated_data)
|
||||||
|
mediafile.access_groups.set(access_groups)
|
||||||
|
mediafile.save()
|
||||||
|
return mediafile
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
# remove is_directory, create_timestamp and parent from validated_data
|
||||||
|
# to prevent updating them (mediafile is ensured in the constructor)
|
||||||
|
validated_data.pop("is_directory", None)
|
||||||
|
validated_data.pop("create_timestamp", None)
|
||||||
|
validated_data.pop("parent", None)
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
def get_filesize(self, mediafile):
|
def get_filesize(self, mediafile):
|
||||||
return mediafile.get_filesize()
|
return mediafile.get_filesize()
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
from django.http import HttpResponseForbidden, HttpResponseNotFound
|
from django.http import HttpResponseForbidden, HttpResponseNotFound
|
||||||
|
from django.http.request import QueryDict
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
|
|
||||||
from ..core.config import config
|
from openslides.core.models import Projector
|
||||||
from ..utils.auth import has_perm
|
|
||||||
from ..utils.rest_api import ModelViewSet, ValidationError
|
from ..utils.auth import has_perm, in_some_groups
|
||||||
|
from ..utils.autoupdate import inform_changed_data
|
||||||
|
from ..utils.rest_api import ModelViewSet, Response, ValidationError, list_route
|
||||||
from .access_permissions import MediafileAccessPermissions
|
from .access_permissions import MediafileAccessPermissions
|
||||||
|
from .config import watch_and_update_configs
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
|
|
||||||
|
|
||||||
@ -26,21 +30,9 @@ class MediafileViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
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 == "metadata":
|
elif self.action in ("create", "partial_update", "update", "move", "destroy"):
|
||||||
result = has_perm(self.request.user, "mediafiles.can_see")
|
|
||||||
elif self.action == "create":
|
|
||||||
result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
|
|
||||||
self.request.user, "mediafiles.can_upload"
|
|
||||||
)
|
|
||||||
elif self.action in ("partial_update", "update"):
|
|
||||||
result = (
|
|
||||||
has_perm(self.request.user, "mediafiles.can_see")
|
|
||||||
and has_perm(self.request.user, "mediafiles.can_upload")
|
|
||||||
and has_perm(self.request.user, "mediafiles.can_manage")
|
|
||||||
)
|
|
||||||
elif self.action == "destroy":
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
@ -52,62 +44,166 @@ class MediafileViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
Customized view endpoint to upload a new file.
|
Customized view endpoint to upload a new file.
|
||||||
"""
|
"""
|
||||||
# Check permission to check if the uploader has to be changed.
|
# The form data may send the groups_id
|
||||||
uploader_id = self.request.data.get("uploader_id")
|
if isinstance(request.data, QueryDict):
|
||||||
if (
|
request.data._mutable = True
|
||||||
uploader_id
|
|
||||||
and not has_perm(request.user, "mediafiles.can_manage")
|
# convert formdata string "<id, <id>, id>" to a list of numbers.
|
||||||
and str(self.request.user.pk) != str(uploader_id)
|
if "access_groups_id" in request.data and isinstance(request.data, QueryDict):
|
||||||
):
|
access_groups_id = request.data.get("access_groups_id")
|
||||||
self.permission_denied(request)
|
if access_groups_id:
|
||||||
if not self.request.data.get("mediafile"):
|
request.data.setlist(
|
||||||
|
"access_groups_id", [int(x) for x in access_groups_id.split(", ")]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
del request.data["access_groups_id"]
|
||||||
|
|
||||||
|
is_directory = bool(request.data.get("is_directory", False))
|
||||||
|
if is_directory and request.data.get("mediafile"):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Either create a path or a file, but not both"}
|
||||||
|
)
|
||||||
|
if not request.data.get("mediafile") and not is_directory:
|
||||||
raise ValidationError({"detail": "You forgot to provide a file."})
|
raise ValidationError({"detail": "You forgot to provide a file."})
|
||||||
|
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, *args, **kwargs):
|
||||||
"""
|
with watch_and_update_configs():
|
||||||
Customized view endpoint to delete uploaded files.
|
response = super().destroy(*args, **kwargs)
|
||||||
|
return response
|
||||||
|
|
||||||
Does also delete the file from filesystem.
|
def update(self, *args, **kwargs):
|
||||||
"""
|
with watch_and_update_configs():
|
||||||
# To avoid Django calling save() and triggering autoupdate we do not
|
response = super().update(*args, **kwargs)
|
||||||
# use the builtin method mediafile.mediafile.delete() but call
|
inform_changed_data(self.get_object().get_children_deep())
|
||||||
# mediafile.mediafile.storage.delete(...) directly. This may have
|
return response
|
||||||
# unattended side effects so be careful especially when accessing files
|
|
||||||
# on server via Django methods (file, open(), save(), ...).
|
|
||||||
mediafile = self.get_object()
|
|
||||||
mediafile.mediafile.storage.delete(mediafile.mediafile.name)
|
|
||||||
|
|
||||||
# check if the file was used as a logo or font
|
@list_route(methods=["post"])
|
||||||
for logo in config["logos_available"]:
|
def move(self, request):
|
||||||
if config[logo]["path"] == mediafile.mediafile.url:
|
"""
|
||||||
config[logo] = {
|
{
|
||||||
"display_name": config[logo]["display_name"],
|
ids: [<id>, <id>, ...],
|
||||||
"path": "",
|
directory_id: <id>
|
||||||
}
|
}
|
||||||
for font in config["fonts_available"]:
|
Move <ids> to the given directory_id. This will raise an error, if
|
||||||
if config[font]["path"] == mediafile.mediafile.url:
|
the move would be recursive.
|
||||||
config[font] = {
|
"""
|
||||||
"display_name": config[font]["display_name"],
|
|
||||||
"default": config[font]["default"],
|
# Validate data:
|
||||||
"path": "",
|
if not isinstance(request.data, dict):
|
||||||
}
|
raise ValidationError({"detail": "The data must be a dict"})
|
||||||
return super().destroy(request, *args, **kwargs)
|
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"})
|
||||||
|
directory_id = request.data.get("directory_id")
|
||||||
|
if directory_id is not None and not isinstance(directory_id, int):
|
||||||
|
raise ValidationError({"detail": "The directory_id must be an int"})
|
||||||
|
if directory_id is None:
|
||||||
|
directory = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
directory = Mediafile.objects.get(pk=directory_id, is_directory=True)
|
||||||
|
except Mediafile.DoesNotExist:
|
||||||
|
raise ValidationError({"detail": "The directory does not exist"})
|
||||||
|
|
||||||
|
ids_set = set(ids) # keep them in a set for fast lookup
|
||||||
|
ids = list(ids_set) # make ids unique
|
||||||
|
|
||||||
|
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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search for valid parents (None is not included, but also safe!)
|
||||||
|
if directory is not None:
|
||||||
|
valid_parent_ids = set()
|
||||||
|
|
||||||
|
queue = list(Mediafile.objects.filter(parent=None, is_directory=True))
|
||||||
|
for mediafile in queue:
|
||||||
|
if mediafile.pk in ids_set:
|
||||||
|
continue # not valid, because this is in the input data
|
||||||
|
valid_parent_ids.add(mediafile.pk)
|
||||||
|
queue.extend(
|
||||||
|
list(Mediafile.objects.filter(parent=mediafile, is_directory=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
if directory_id not in valid_parent_ids:
|
||||||
|
raise ValidationError({"detail": "The directory is not valid"})
|
||||||
|
|
||||||
|
# Ok, update all mediafiles
|
||||||
|
with watch_and_update_configs():
|
||||||
|
for mediafile in mediafiles:
|
||||||
|
mediafile.parent = directory
|
||||||
|
mediafile.save(skip_autoupdate=True)
|
||||||
|
if directory is None:
|
||||||
|
inform_changed_data(Mediafile.objects.all())
|
||||||
|
else:
|
||||||
|
inform_changed_data(directory.get_children_deep())
|
||||||
|
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
def get_mediafile(request, path):
|
||||||
|
"""
|
||||||
|
returnes the mediafile for the requested path and checks, if the user is
|
||||||
|
valid to retrieve the mediafile. If not, None will be returned.
|
||||||
|
A user must have all access permissions for all folders the the file itself,
|
||||||
|
or the file is a special file (logo or font), then it is always returned.
|
||||||
|
|
||||||
|
If the mediafile cannot be found, a Mediafile.DoesNotExist will be raised.
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
raise Mediafile.DoesNotExist()
|
||||||
|
parts = path.split("/")
|
||||||
|
parent = None
|
||||||
|
can_see = has_perm(request.user, "mediafiles.can_see")
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
is_directory = i < len(parts) - 1
|
||||||
|
if is_directory:
|
||||||
|
mediafile = Mediafile.objects.get(
|
||||||
|
parent=parent, is_directory=is_directory, title=part
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mediafile = Mediafile.objects.get(
|
||||||
|
parent=parent, is_directory=is_directory, original_filename=part
|
||||||
|
)
|
||||||
|
if mediafile.access_groups.exists() and not in_some_groups(
|
||||||
|
request.user.id, [group.id for group in mediafile.access_groups.all()]
|
||||||
|
):
|
||||||
|
can_see = False
|
||||||
|
parent = mediafile
|
||||||
|
|
||||||
|
# Check, if this file is projected
|
||||||
|
is_projected = False
|
||||||
|
for projector in Projector.objects.all():
|
||||||
|
for element in projector.elements:
|
||||||
|
name = element.get("name")
|
||||||
|
id = element.get("id")
|
||||||
|
if name == "mediafiles/mediafile" and id == mediafile.id:
|
||||||
|
is_projected = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not can_see and not mediafile.is_special_file and not is_projected:
|
||||||
|
mediafile = None
|
||||||
|
|
||||||
|
return mediafile
|
||||||
|
|
||||||
|
|
||||||
def protected_serve(request, path, document_root=None, show_indexes=False):
|
def protected_serve(request, path, document_root=None, show_indexes=False):
|
||||||
try:
|
try:
|
||||||
mediafile = Mediafile.objects.get(mediafile=path)
|
mediafile = get_mediafile(request, path)
|
||||||
except Mediafile.DoesNotExist:
|
except Mediafile.DoesNotExist:
|
||||||
return HttpResponseNotFound(content="Not found.")
|
return HttpResponseNotFound(content="Not found.")
|
||||||
|
|
||||||
can_see = has_perm(request.user, "mediafiles.can_see")
|
if mediafile:
|
||||||
is_special_file = mediafile.is_logo() or mediafile.is_font()
|
return serve(request, mediafile.mediafile.name, document_root, show_indexes)
|
||||||
is_hidden_but_no_perms = mediafile.hidden and not has_perm(
|
|
||||||
request.user, "mediafiles.can_see_hidden"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_special_file and (not can_see or is_hidden_but_no_perms):
|
|
||||||
return HttpResponseForbidden(content="Forbidden.")
|
|
||||||
else:
|
else:
|
||||||
return serve(request, path, document_root, show_indexes)
|
return HttpResponseForbidden(content="Forbidden.")
|
||||||
|
@ -88,7 +88,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="motionchangerecommendation",
|
model_name="motionchangerecommendation",
|
||||||
name="motion",
|
name="motion",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
related_name="change_recommendations",
|
related_name="change_recommendations",
|
||||||
to="motions.Motion",
|
to="motions.Motion",
|
||||||
),
|
),
|
||||||
@ -106,7 +106,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="submitter",
|
model_name="submitter",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -19,7 +19,7 @@ from openslides.utils.autoupdate import inform_changed_data
|
|||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
|
|
||||||
from ..utils.models import CASCADE_AND_AUTOUODATE, SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
@ -657,7 +657,7 @@ class Submitter(RESTModelMixin, models.Model):
|
|||||||
Use custom Manager.
|
Use custom Manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUODATE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE_AND_AUTOUPDATE)
|
||||||
"""
|
"""
|
||||||
ForeignKey to the user who is the submitter.
|
ForeignKey to the user who is the submitter.
|
||||||
"""
|
"""
|
||||||
@ -707,7 +707,7 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
|||||||
objects = MotionChangeRecommendationManager()
|
objects = MotionChangeRecommendationManager()
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion, on_delete=CASCADE_AND_AUTOUODATE, related_name="change_recommendations"
|
Motion, on_delete=CASCADE_AND_AUTOUPDATE, related_name="change_recommendations"
|
||||||
)
|
)
|
||||||
"""The motion to which the change recommendation belongs."""
|
"""The motion to which the change recommendation belongs."""
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="personalnote",
|
model_name="personalnote",
|
||||||
name="user",
|
name="user",
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
on_delete=openslides.utils.models.CASCADE_AND_AUTOUODATE,
|
on_delete=openslides.utils.models.CASCADE_AND_AUTOUPDATE,
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -20,7 +20,7 @@ from jsonfield import JSONField
|
|||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.auth import GROUP_ADMIN_PK
|
from ..utils.auth import GROUP_ADMIN_PK
|
||||||
from ..utils.models import CASCADE_AND_AUTOUODATE, RESTModelMixin
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
GroupAccessPermissions,
|
GroupAccessPermissions,
|
||||||
PersonalNoteAccessPermissions,
|
PersonalNoteAccessPermissions,
|
||||||
@ -351,7 +351,7 @@ class PersonalNote(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
objects = PersonalNoteManager()
|
objects = PersonalNoteManager()
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUODATE)
|
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUPDATE)
|
||||||
notes = JSONField()
|
notes = JSONField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -52,8 +52,6 @@ def create_builtin_groups_and_admin(**kwargs):
|
|||||||
"core.can_see_projector",
|
"core.can_see_projector",
|
||||||
"mediafiles.can_manage",
|
"mediafiles.can_manage",
|
||||||
"mediafiles.can_see",
|
"mediafiles.can_see",
|
||||||
"mediafiles.can_see_hidden",
|
|
||||||
"mediafiles.can_upload",
|
|
||||||
"motions.can_create",
|
"motions.can_create",
|
||||||
"motions.can_create_amendments",
|
"motions.can_create_amendments",
|
||||||
"motions.can_manage",
|
"motions.can_manage",
|
||||||
@ -145,8 +143,6 @@ def create_builtin_groups_and_admin(**kwargs):
|
|||||||
permission_dict["core.can_manage_tags"],
|
permission_dict["core.can_manage_tags"],
|
||||||
permission_dict["mediafiles.can_see"],
|
permission_dict["mediafiles.can_see"],
|
||||||
permission_dict["mediafiles.can_manage"],
|
permission_dict["mediafiles.can_manage"],
|
||||||
permission_dict["mediafiles.can_upload"],
|
|
||||||
permission_dict["mediafiles.can_see_hidden"],
|
|
||||||
permission_dict["motions.can_see"],
|
permission_dict["motions.can_see"],
|
||||||
permission_dict["motions.can_see_internal"],
|
permission_dict["motions.can_see_internal"],
|
||||||
permission_dict["motions.can_create"],
|
permission_dict["motions.can_create"],
|
||||||
|
@ -191,7 +191,7 @@ def SET_NULL_AND_AUTOUPDATE(
|
|||||||
models.SET_NULL(collector, field, sub_objs, using)
|
models.SET_NULL(collector, field, sub_objs, using)
|
||||||
|
|
||||||
|
|
||||||
def CASCADE_AND_AUTOUODATE(
|
def CASCADE_AND_AUTOUPDATE(
|
||||||
collector: Any, field: Any, sub_objs: Any, using: Any
|
collector: Any, field: Any, sub_objs: Any, using: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
from ..helpers import count_queries
|
from ..helpers import count_queries
|
||||||
|
|
||||||
@ -12,6 +16,8 @@ def test_mediafiles_db_queries():
|
|||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 1 requests to get the list of all files
|
* 1 requests to get the list of all files
|
||||||
* 1 request to get all lists of speakers.
|
* 1 request to get all lists of speakers.
|
||||||
|
* 1 request to get all groups
|
||||||
|
* 1 request to prefetch parents
|
||||||
"""
|
"""
|
||||||
for index in range(10):
|
for index in range(10):
|
||||||
Mediafile.objects.create(
|
Mediafile.objects.create(
|
||||||
@ -19,4 +25,190 @@ def test_mediafiles_db_queries():
|
|||||||
mediafile=SimpleUploadedFile(f"some_file{index}", b"some content."),
|
mediafile=SimpleUploadedFile(f"some_file{index}", b"some content."),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert count_queries(Mediafile.get_elements) == 2
|
assert count_queries(Mediafile.get_elements) == 4
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreation(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.file = SimpleUploadedFile("some_file.ext", b"some content.")
|
||||||
|
|
||||||
|
def test_simple_file(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{"title": "test_title_ahyo1uifoo9Aiph2av5a", "mediafile": self.file},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
mediafile = Mediafile.objects.get()
|
||||||
|
self.assertEqual(mediafile.title, "test_title_ahyo1uifoo9Aiph2av5a")
|
||||||
|
self.assertFalse(mediafile.is_directory)
|
||||||
|
self.assertTrue(mediafile.mediafile.name)
|
||||||
|
self.assertEqual(mediafile.path, mediafile.original_filename)
|
||||||
|
|
||||||
|
def test_simple_directory(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{"title": "test_title_ahyo1uifoo9Aiph2av5a", "is_directory": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
mediafile = Mediafile.objects.get()
|
||||||
|
self.assertEqual(mediafile.title, "test_title_ahyo1uifoo9Aiph2av5a")
|
||||||
|
self.assertTrue(mediafile.is_directory)
|
||||||
|
self.assertEqual(mediafile.mediafile.name, "")
|
||||||
|
self.assertEqual(mediafile.path, mediafile.title + "/")
|
||||||
|
|
||||||
|
def test_file_and_directory(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_ahyo1uifoo9Aiph2av5a",
|
||||||
|
"is_directory": True,
|
||||||
|
"mediafile": self.file,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(Mediafile.objects.exists())
|
||||||
|
|
||||||
|
def test_mediafile_twice(self):
|
||||||
|
title = "test_title_kFJq83fjmqo2babfqk3f"
|
||||||
|
Mediafile.objects.create(is_directory=True, title=title)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"), {"title": title, "is_directory": True}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(Mediafile.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_without_mediafile(self):
|
||||||
|
response = self.client.post(reverse("mediafile-list"), {})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(Mediafile.objects.exists())
|
||||||
|
|
||||||
|
def test_without_title(self):
|
||||||
|
response = self.client.post(reverse("mediafile-list"), {"is_directory": True})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(Mediafile.objects.exists())
|
||||||
|
|
||||||
|
def test_with_empty_title(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"), {"is_directory": True, "title": ""}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(Mediafile.objects.exists())
|
||||||
|
|
||||||
|
def test_directory_with_slash(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{"title": "test_title_with_/", "is_directory": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(Mediafile.objects.exists())
|
||||||
|
|
||||||
|
def test_with_parent(self):
|
||||||
|
parent_title = "test_title_3q0cqghZRFewocjwferT"
|
||||||
|
title = "test_title_gF3if8jmvrbnwdksg4je"
|
||||||
|
Mediafile.objects.create(is_directory=True, title=parent_title)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{"title": title, "is_directory": True, "parent_id": 1},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Mediafile.objects.count(), 2)
|
||||||
|
mediafile = Mediafile.objects.get(title="test_title_gF3if8jmvrbnwdksg4je")
|
||||||
|
self.assertEqual(mediafile.parent.title, "test_title_3q0cqghZRFewocjwferT")
|
||||||
|
self.assertEqual(mediafile.path, parent_title + "/" + title + "/")
|
||||||
|
|
||||||
|
def test_with_file_as_parent(self):
|
||||||
|
Mediafile.objects.create(
|
||||||
|
title="test_title_qejOVM84gw8ghwpKnqeg", mediafile=self.file
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_poejvvlmmorsgeroemr9",
|
||||||
|
"is_directory": True,
|
||||||
|
"parent_id": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(Mediafile.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_with_access_groups(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("mediafile-list"),
|
||||||
|
{
|
||||||
|
"title": "test_title_dggjwevBnUngelkdviom",
|
||||||
|
"is_directory": True,
|
||||||
|
# This is the format, if it would be provided by JS `FormData`.
|
||||||
|
"access_groups_id": "2, 4",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertTrue(Mediafile.objects.exists())
|
||||||
|
mediafile = Mediafile.objects.get()
|
||||||
|
self.assertEqual(
|
||||||
|
sorted([group.id for group in mediafile.access_groups.all()]), [2, 4]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: List and retrieve
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdate(TestCase):
|
||||||
|
"""
|
||||||
|
Tree:
|
||||||
|
-dir
|
||||||
|
-mediafileA
|
||||||
|
-mediafileB
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username="admin", password="admin")
|
||||||
|
self.dir = Mediafile.objects.create(title="dir", is_directory=True)
|
||||||
|
self.fileA = SimpleUploadedFile("some_fileA.ext", b"some content.")
|
||||||
|
self.mediafileA = Mediafile.objects.create(
|
||||||
|
title="mediafileA", mediafile=self.fileA, parent=self.dir
|
||||||
|
)
|
||||||
|
self.fileB = SimpleUploadedFile("some_fileB.ext", b"some content.")
|
||||||
|
self.mediafileB = Mediafile.objects.create(
|
||||||
|
title="mediafileB", mediafile=self.fileB
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("mediafile-detail", args=[self.mediafileA.pk]),
|
||||||
|
{"title": "test_title_gpasgrmg*miGUM)EAyGO", "access_groups_id": [2, 4]},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
mediafile = Mediafile.objects.get(pk=self.mediafileA.pk)
|
||||||
|
self.assertEqual(mediafile.title, "test_title_gpasgrmg*miGUM)EAyGO")
|
||||||
|
self.assertEqual(mediafile.path, "dir/some_fileA.ext")
|
||||||
|
self.assertEqual(
|
||||||
|
sorted([group.id for group in mediafile.access_groups.all()]), [2, 4]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_directory(self):
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("mediafile-detail", args=[self.dir.pk]),
|
||||||
|
{"title": "test_title_seklMOIGGihdjJBNaflkklnlg"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
dir = Mediafile.objects.get(pk=self.dir.pk)
|
||||||
|
self.assertEqual(dir.title, "test_title_seklMOIGGihdjJBNaflkklnlg")
|
||||||
|
mediafile = Mediafile.objects.get(pk=self.mediafileA.pk)
|
||||||
|
self.assertEqual(
|
||||||
|
mediafile.path, "test_title_seklMOIGGihdjJBNaflkklnlg/some_fileA.ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_parent_id(self):
|
||||||
|
""" Assert, that the parent id does not change """
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("mediafile-detail", args=[self.mediafileA.pk]),
|
||||||
|
{"title": self.mediafileA.title, "parent_id": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
mediafile = Mediafile.objects.get(pk=self.mediafileA.pk)
|
||||||
|
self.assertTrue(mediafile.parent)
|
||||||
|
self.assertEqual(mediafile.parent.pk, self.dir.pk)
|
||||||
|
@ -514,8 +514,6 @@ class GroupUpdate(TestCase):
|
|||||||
"core.can_see_projector",
|
"core.can_see_projector",
|
||||||
"mediafiles.can_manage",
|
"mediafiles.can_manage",
|
||||||
"mediafiles.can_see",
|
"mediafiles.can_see",
|
||||||
"mediafiles.can_see_hidden",
|
|
||||||
"mediafiles.can_upload",
|
|
||||||
"motions.can_create",
|
"motions.can_create",
|
||||||
"motions.can_manage",
|
"motions.can_manage",
|
||||||
"motions.can_see",
|
"motions.can_see",
|
||||||
|
Loading…
Reference in New Issue
Block a user