Merge pull request #4767 from normanjaeckel/HistoryRebuild
Refactored OpenSlides history (HistoryInformation is not a root rest …
This commit is contained in:
commit
face97a2a2
@ -21,7 +21,7 @@ Core:
|
|||||||
- Add a change-id system to get only new elements [#3938].
|
- Add a change-id system to get only new elements [#3938].
|
||||||
- Switch from Yarn back to npm [#3964].
|
- Switch from Yarn back to npm [#3964].
|
||||||
- Added password reset link (password reset via email) [#3914, #4199].
|
- Added password reset link (password reset via email) [#3914, #4199].
|
||||||
- Added global history mode [#3977, #4141, #4369, #4373].
|
- Added global history mode [#3977, #4141, #4369, #4373, #4767].
|
||||||
- Projector refactoring [4119, #4130].
|
- Projector refactoring [4119, #4130].
|
||||||
- Fixed logo configuration if logo file is deleted [#4374].
|
- Fixed logo configuration if logo file is deleted [#4374].
|
||||||
|
|
||||||
|
@ -358,6 +358,10 @@ export class OperatorService implements OnAfterAppsLoaded {
|
|||||||
return groupIds.some(id => this.user.groups_id.includes(id));
|
return groupIds.some(id => this.user.groups_id.includes(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isSuperAdmin(): boolean {
|
||||||
|
return this.isInGroupIdsNonAdminCheck(2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the operators permissions and publish the operator afterwards.
|
* Update the operators permissions and publish the operator afterwards.
|
||||||
* Saves the current WhoAmI to storage with the updated permissions
|
* Saves the current WhoAmI to storage with the updated permissions
|
||||||
|
@ -9,6 +9,7 @@ import { BaseModel } from 'app/shared/models/base/base-model';
|
|||||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||||
import { OpenSlidesService } from './openslides.service';
|
import { OpenSlidesService } from './openslides.service';
|
||||||
import { HttpService } from './http.service';
|
import { HttpService } from './http.service';
|
||||||
|
import { DataStoreUpdateManagerService } from './data-store-update-manager.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for full history data objects.
|
* Interface for full history data objects.
|
||||||
@ -51,7 +52,8 @@ export class TimeTravelService {
|
|||||||
private modelMapperService: CollectionStringMapperService,
|
private modelMapperService: CollectionStringMapperService,
|
||||||
private DS: DataStoreService,
|
private DS: DataStoreService,
|
||||||
private OSStatus: OpenSlidesStatusService,
|
private OSStatus: OpenSlidesStatusService,
|
||||||
private OpenSlides: OpenSlidesService
|
private OpenSlides: OpenSlidesService,
|
||||||
|
private DSUpdateManager: DataStoreUpdateManagerService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,6 +62,8 @@ export class TimeTravelService {
|
|||||||
* @param history the desired point in the history of OpenSlides
|
* @param history the desired point in the history of OpenSlides
|
||||||
*/
|
*/
|
||||||
public async loadHistoryPoint(history: History): Promise<void> {
|
public async loadHistoryPoint(history: History): Promise<void> {
|
||||||
|
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
||||||
|
|
||||||
await this.stopTime(history);
|
await this.stopTime(history);
|
||||||
const fullDataHistory: HistoryData[] = await this.getHistoryData(history);
|
const fullDataHistory: HistoryData[] = await this.getHistoryData(history);
|
||||||
for (const historyObject of fullDataHistory) {
|
for (const historyObject of fullDataHistory) {
|
||||||
@ -74,6 +78,8 @@ export class TimeTravelService {
|
|||||||
await this.DS.remove(collectionString, [+id]);
|
await this.DS.remove(collectionString, [+id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.DSUpdateManager.commit(updateSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,25 +100,16 @@ export class TimeTravelService {
|
|||||||
* @returns the full history on the given date
|
* @returns the full history on the given date
|
||||||
*/
|
*/
|
||||||
private async getHistoryData(history: History): Promise<HistoryData[]> {
|
private async getHistoryData(history: History): Promise<HistoryData[]> {
|
||||||
const queryParams = { timestamp: Math.ceil(+history.unixtime) };
|
const queryParams = { timestamp: Math.ceil(history.timestamp) };
|
||||||
return this.httpService.get<HistoryData[]>(`${environment.urlPrefix}/core/history/`, null, queryParams);
|
return this.httpService.get<HistoryData[]>(`${environment.urlPrefix}/core/history/data/`, null, queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the DataStore and stops the WebSocket connection
|
* Clears the DataStore and stops the WebSocket connection
|
||||||
*/
|
*/
|
||||||
private async stopTime(history: History): Promise<void> {
|
private async stopTime(history: History): Promise<void> {
|
||||||
this.webSocketService.close();
|
await this.webSocketService.close();
|
||||||
await this.cleanDataStore();
|
await this.DS.set(); // Same as clear, but not persistent.
|
||||||
this.OSStatus.enterHistoryMode(history);
|
this.OSStatus.enterHistoryMode(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean the DataStore to inject old Data.
|
|
||||||
* Remove everything "but" the history.
|
|
||||||
*/
|
|
||||||
private async cleanDataStore(): Promise<void> {
|
|
||||||
const historyArchive = this.DS.getAll(History);
|
|
||||||
await this.DS.set(historyArchive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ export class WebsocketService {
|
|||||||
if (data instanceof ArrayBuffer) {
|
if (data instanceof ArrayBuffer) {
|
||||||
const compressedSize = data.byteLength;
|
const compressedSize = data.byteLength;
|
||||||
const decompressedBuffer: Uint8Array = decompress(new Uint8Array(data));
|
const decompressedBuffer: Uint8Array = decompress(new Uint8Array(data));
|
||||||
console.log(
|
console.debug(
|
||||||
`Recieved ${compressedSize / 1024} KB (${decompressedBuffer.byteLength /
|
`Recieved ${compressedSize / 1024} KB (${decompressedBuffer.byteLength /
|
||||||
1024} KB uncompressed), ratio ${decompressedBuffer.byteLength / compressedSize}`
|
1024} KB uncompressed), ratio ${decompressedBuffer.byteLength / compressedSize}`
|
||||||
);
|
);
|
||||||
@ -285,7 +285,7 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message: IncommingWebsocketMessage = JSON.parse(data);
|
const message: IncommingWebsocketMessage = JSON.parse(data);
|
||||||
console.log('Received', message);
|
console.debug('Received', message);
|
||||||
const type = message.type;
|
const type = message.type;
|
||||||
const inResponse = message.in_response;
|
const inResponse = message.in_response;
|
||||||
const callbacks = this.responseCallbacks[inResponse];
|
const callbacks = this.responseCallbacks[inResponse];
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { HistoryRepositoryService } from './history-repository.service';
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
|
||||||
|
|
||||||
describe('HistoryRepositoryService', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [E2EImportsModule],
|
|
||||||
providers: [HistoryRepositoryService]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
const service = TestBed.get(HistoryRepositoryService);
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,145 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
|
||||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
|
||||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
|
||||||
import { History } from 'app/shared/models/core/history';
|
|
||||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
|
||||||
import { ViewHistory, ProxyHistory, HistoryTitleInformation } from 'app/site/history/models/view-history';
|
|
||||||
import { TimeTravelService } from 'app/core/core-services/time-travel.service';
|
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
import { DataSendService } from 'app/core/core-services/data-send.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository for the history.
|
|
||||||
*
|
|
||||||
* Gets new history objects/entries and provides them for the view.
|
|
||||||
*/
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class HistoryRepositoryService extends BaseRepository<ViewHistory, History, HistoryTitleInformation> {
|
|
||||||
/**
|
|
||||||
* Constructs the history repository
|
|
||||||
*
|
|
||||||
* @param DS The DataStore
|
|
||||||
* @param mapperService mapps the models to the collection string
|
|
||||||
* @param httpService OpenSlides own HTTP service
|
|
||||||
* @param timeTravel To change the time
|
|
||||||
*/
|
|
||||||
public constructor(
|
|
||||||
DS: DataStoreService,
|
|
||||||
dataSend: DataSendService,
|
|
||||||
mapperService: CollectionStringMapperService,
|
|
||||||
viewModelStoreService: ViewModelStoreService,
|
|
||||||
translate: TranslateService,
|
|
||||||
private httpService: HttpService,
|
|
||||||
private timeTravel: TimeTravelService
|
|
||||||
) {
|
|
||||||
super(DS, dataSend, mapperService, viewModelStoreService, translate, History);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVerboseName = (plural: boolean = false) => {
|
|
||||||
return this.translate.instant(plural ? 'Histories' : 'History');
|
|
||||||
};
|
|
||||||
|
|
||||||
public getTitle = (titleInformation: HistoryTitleInformation) => {
|
|
||||||
return titleInformation.element_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ViewHistory objects out of a historyObject
|
|
||||||
*
|
|
||||||
* @param history the source history object
|
|
||||||
* @return a new ViewHistory object
|
|
||||||
*/
|
|
||||||
public createViewModel(history: History): ViewHistory {
|
|
||||||
return new ViewHistory(this.createProxyHistory(history));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a ProxyHistory from a History by wrapping it and give access to the user.
|
|
||||||
*
|
|
||||||
* @param history The History object
|
|
||||||
* @returns the ProxyHistory
|
|
||||||
*/
|
|
||||||
private createProxyHistory(history: History): ProxyHistory {
|
|
||||||
return new Proxy(history, {
|
|
||||||
get: (instance, property) => {
|
|
||||||
if (property === 'user') {
|
|
||||||
return this.viewModelStoreService.get(ViewUser, instance.user_id);
|
|
||||||
} else {
|
|
||||||
return instance[property];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overwrites the default procedure
|
|
||||||
*
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
public async create(): Promise<Identifiable> {
|
|
||||||
throw new Error('You cannot create a history object');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overwrites the default procedure
|
|
||||||
*
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
public async update(): Promise<void> {
|
|
||||||
throw new Error('You cannot update a history object');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overwrites the default procedure
|
|
||||||
*
|
|
||||||
* @ignore
|
|
||||||
*/
|
|
||||||
public async patch(): Promise<void> {
|
|
||||||
throw new Error('You cannot patch a history object');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overwrites the default procedure
|
|
||||||
*
|
|
||||||
* Sends a post-request to delete history objects
|
|
||||||
*/
|
|
||||||
public async delete(): Promise<void> {
|
|
||||||
const restPath = '/rest/core/history/clear_history/';
|
|
||||||
await this.httpService.post(restPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ListTitle of a history Element from the dataStore
|
|
||||||
* using the collection string and the ID.
|
|
||||||
*
|
|
||||||
* @param collectionString the models collection string
|
|
||||||
* @param id the models id
|
|
||||||
* @returns the ListTitle or null if the model was deleted already
|
|
||||||
*/
|
|
||||||
public getOldModelInfo(collectionString: string, id: number): string {
|
|
||||||
const model = this.viewModelStoreService.get(collectionString, id);
|
|
||||||
if (model) {
|
|
||||||
return model.getListTitle();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the full data on the given date and use the
|
|
||||||
* TimeTravelService to browse the history on the
|
|
||||||
* given date
|
|
||||||
*
|
|
||||||
* @param viewHistory determines to point to travel back to
|
|
||||||
*/
|
|
||||||
public async browseHistory(viewHistory: ViewHistory): Promise<void> {
|
|
||||||
return this.timeTravel.loadHistoryPoint(viewHistory.history);
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,7 +16,7 @@ export class PromptService {
|
|||||||
* @param title The title to display in the dialog
|
* @param title The title to display in the dialog
|
||||||
* @param content The content in the dialog
|
* @param content The content in the dialog
|
||||||
*/
|
*/
|
||||||
public async open(title: string, content: string): Promise<any> {
|
public async open(title: string, content: string = ''): Promise<any> {
|
||||||
const dialogRef = this.dialog.open(PromptDialogComponent, {
|
const dialogRef = this.dialog.open(PromptDialogComponent, {
|
||||||
width: '250px',
|
width: '250px',
|
||||||
data: { title: title, content: content }
|
data: { title: title, content: content }
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { BaseModel } from '../base/base-model';
|
import { Deserializable } from '../base/deserializable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a history object.
|
* Representation of a history object.
|
||||||
*
|
*
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
export class History extends BaseModel {
|
export class History implements Deserializable {
|
||||||
public static COLLECTIONSTRING = 'core/history';
|
|
||||||
public id: number;
|
|
||||||
public element_id: string;
|
public element_id: string;
|
||||||
public now: string;
|
public timestamp: number;
|
||||||
public information: string;
|
public information: string;
|
||||||
|
public restricted: boolean;
|
||||||
public user_id: number;
|
public user_id: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,18 +18,21 @@ export class History extends BaseModel {
|
|||||||
* @returns a Data object
|
* @returns a Data object
|
||||||
*/
|
*/
|
||||||
public get date(): Date {
|
public get date(): Date {
|
||||||
return new Date(this.now);
|
return new Date(this.timestamp * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public get collectionString(): string {
|
||||||
* Converts the timestamp to unix time
|
return this.element_id.split(':')[0];
|
||||||
*/
|
|
||||||
public get unixtime(): number {
|
|
||||||
return Date.parse(this.now) / 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(input?: any) {
|
public get modelId(): number {
|
||||||
super(History.COLLECTIONSTRING, input);
|
return +this.element_id.split(':')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(input: History) {
|
||||||
|
if (input) {
|
||||||
|
this.deserialize(input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,4 +44,8 @@ export class History extends BaseModel {
|
|||||||
public getLocaleString(locale: string): string {
|
public getLocaleString(locale: string): string {
|
||||||
return this.date.toLocaleString(locale);
|
return this.date.toLocaleString(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deserialize(input: any): void {
|
||||||
|
Object.assign(this, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,41 @@
|
|||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<div class="menu-slot">
|
<div class="menu-slot">
|
||||||
<!-- Hidden for everyone but the superadmin -->
|
<!-- Hidden for everyone but the superadmin -->
|
||||||
<button *osPerms="'superadmin'" type="button" mat-icon-button [matMenuTriggerFor]="historyMenu">
|
<button *ngIf="isSuperAdmin" type="button" mat-icon-button [matMenuTriggerFor]="historyMenu">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<div class="custom-table-header">
|
<div class="custom-table-header">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<os-search-value-selector
|
||||||
|
ngDefaultControl
|
||||||
|
[form]="modelSelectForm"
|
||||||
|
[formControl]="modelSelectForm.get('model')"
|
||||||
|
[multiple]="false"
|
||||||
|
[includeNone]="false"
|
||||||
|
listname="{{ 'Motion' | translate }}"
|
||||||
|
[InputListValues]="collectionObserver"
|
||||||
|
></os-search-value-selector>
|
||||||
|
</span>
|
||||||
|
<span class="spacer-left-20">
|
||||||
|
<button mat-button (click)="refresh()" *ngIf="currentModelId">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
<span translate>Refresh</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput (keyup)="applySearch($event.target.value)" placeholder="{{ 'Search' | translate }}" />
|
<input matInput (keyup)="applySearch($event.target.value)" placeholder="{{ 'Search' | translate }}" />
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-table class="os-headed-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
<mat-table class="on-transition-fade" [dataSource]="dataSource" matSort>
|
||||||
<!-- Timestamp -->
|
<!-- Timestamp -->
|
||||||
<ng-container matColumnDef="time">
|
<ng-container matColumnDef="time">
|
||||||
<mat-header-cell *matHeaderCellDef translate>Timestamp</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef translate>Timestamp</mat-header-cell>
|
||||||
@ -43,23 +64,23 @@
|
|||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<ng-container matColumnDef="info">
|
<ng-container matColumnDef="info">
|
||||||
<mat-header-cell *matHeaderCellDef translate>Comment</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef translate>Comment</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let history">{{ parseInformation(history.information) }}</mat-cell>
|
<mat-cell *matCellDef="let history">{{ parseInformation(history) }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- User -->
|
<!-- User -->
|
||||||
<ng-container matColumnDef="user">
|
<ng-container matColumnDef="user">
|
||||||
<mat-header-cell *matHeaderCellDef translate>Changed by</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef translate>Changed by</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let history">{{ history.user_full_name }}</mat-cell>
|
<mat-cell *matCellDef="let history">{{ getUserName(history) }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="getRowDef()"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="getRowDef()"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: getRowDef()" (click)="onClickRow(row)"></mat-row>
|
<mat-row *matRowDef="let row; columns: getRowDef()" (click)="onClickRow(row)"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
|
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSizes"></mat-paginator>
|
||||||
|
|
||||||
<mat-menu #historyMenu="matMenu">
|
<mat-menu #historyMenu="matMenu">
|
||||||
<button mat-menu-item class="red-warning-text" (click)="onDeleteAllButton()">
|
<button mat-menu-item class="red-warning-text" (click)="clearHistory()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete whole history</span>
|
<span translate>Delete whole history</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.os-listview-table {
|
.mat-table {
|
||||||
/** Time */
|
/** Time */
|
||||||
.mat-column-time {
|
.mat-column-time {
|
||||||
flex: 1 0 50px;
|
flex: 1 0 50px;
|
||||||
@ -24,3 +24,12 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: slategray; // TODO: Colors per theme
|
color: slategray; // TODO: Colors per theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-table-header {
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar, MatTableDataSource } from '@angular/material';
|
||||||
import { Router } from '@angular/router';
|
import { Router } 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 { Subject } from 'rxjs';
|
import { Subject, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { History } from 'app/shared/models/core/history';
|
import { environment } from 'environments/environment';
|
||||||
import { HistoryRepositoryService } from 'app/core/repositories/history/history-repository.service';
|
|
||||||
import { isDetailNavigable } from 'app/shared/models/base/detail-navigable';
|
import { isDetailNavigable } from 'app/shared/models/base/detail-navigable';
|
||||||
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { ViewHistory } from '../../models/view-history';
|
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
||||||
|
import { TimeTravelService } from 'app/core/core-services/time-travel.service';
|
||||||
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
|
import { History } from 'app/shared/models/core/history';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||||
|
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
||||||
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
|
import { Motion } from 'app/shared/models/motions/motion';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list view for the history.
|
* A list view for the history.
|
||||||
@ -25,13 +32,41 @@ import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
|||||||
templateUrl: './history-list.component.html',
|
templateUrl: './history-list.component.html',
|
||||||
styleUrls: ['./history-list.component.scss']
|
styleUrls: ['./history-list.component.scss']
|
||||||
})
|
})
|
||||||
export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, History, HistoryRepositoryService>
|
export class HistoryListComponent extends BaseViewComponent implements OnInit {
|
||||||
implements OnInit {
|
|
||||||
/**
|
/**
|
||||||
* Subject determine when the custom timestamp subject changes
|
* Subject determine when the custom timestamp subject changes
|
||||||
*/
|
*/
|
||||||
public customTimestampChanged: Subject<number> = new Subject<number>();
|
public customTimestampChanged: Subject<number> = new Subject<number>();
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<History> = new MatTableDataSource<History>();
|
||||||
|
|
||||||
|
public get isSuperAdmin(): boolean {
|
||||||
|
return this.operator.isSuperAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public pageSizes = [50, 100, 150, 200, 250];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form for the selection of the model
|
||||||
|
* When more models are supproted, add a "collection"-dropdown
|
||||||
|
*/
|
||||||
|
public modelSelectForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The observer for the selected collection, which is currently hardcoded
|
||||||
|
* to motions.
|
||||||
|
*/
|
||||||
|
public collectionObserver: BehaviorSubject<BaseViewModel[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current selected collection. THis may move to `modelSelectForm`, if this can be choosen.
|
||||||
|
*/
|
||||||
|
private currentCollection = Motion.COLLECTIONSTRING;
|
||||||
|
|
||||||
|
public get currentModelId(): number | null {
|
||||||
|
return this.modelSelectForm.controls.model.value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the history list component
|
* Constructor for the history list component
|
||||||
*
|
*
|
||||||
@ -47,12 +82,25 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
|||||||
titleService: Title,
|
titleService: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private repo: HistoryRepositoryService,
|
|
||||||
private viewModelStore: ViewModelStoreService,
|
private viewModelStore: ViewModelStoreService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private operator: OperatorService
|
private operator: OperatorService,
|
||||||
|
private timeTravelService: TimeTravelService,
|
||||||
|
private http: HttpService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private motionRepo: MotionRepositoryService,
|
||||||
|
private promptService: PromptService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar, repo);
|
super(titleService, translate, matSnackBar);
|
||||||
|
|
||||||
|
this.modelSelectForm = this.formBuilder.group({
|
||||||
|
model: []
|
||||||
|
});
|
||||||
|
this.collectionObserver = this.motionRepo.getViewModelListBehaviorSubject();
|
||||||
|
|
||||||
|
this.modelSelectForm.controls.model.valueChanges.subscribe((id: number) => {
|
||||||
|
this.queryElementId(this.currentCollection, id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,23 +108,34 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
|||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
super.setTitle('History');
|
super.setTitle('History');
|
||||||
this.initTable();
|
|
||||||
this.setFilters();
|
|
||||||
|
|
||||||
this.repo.getViewModelListObservable().subscribe(history => {
|
this.dataSource.filterPredicate = (history: History, filter: string) => {
|
||||||
this.sortAndPublish(history);
|
filter = filter ? filter.toLowerCase() : '';
|
||||||
});
|
|
||||||
|
if (!history) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const userfullname = this.getUserName(history);
|
||||||
* Sorts the given ViewHistory array and sets it in the table data source
|
if (userfullname.toLowerCase().indexOf(filter) >= 0) {
|
||||||
*
|
return true;
|
||||||
* @param unsortedHistoryList
|
}
|
||||||
*/
|
|
||||||
private sortAndPublish(unsortedHistoryList: ViewHistory[]): void {
|
if (
|
||||||
const sortedList = unsortedHistoryList.map(history => history).filter(item => item.information.length > 0);
|
this.getElementInfo(history) &&
|
||||||
sortedList.sort((a, b) => b.history.unixtime - a.history.unixtime);
|
this.getElementInfo(history)
|
||||||
this.dataSource.data = sortedList;
|
.toLowerCase()
|
||||||
|
.indexOf(filter) >= 0
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.parseInformation(history)
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(filter) >= 0
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,17 +151,12 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
|||||||
* Tries get the title of the BaseModel element corresponding to
|
* Tries get the title of the BaseModel element corresponding to
|
||||||
* a history object.
|
* a history object.
|
||||||
*
|
*
|
||||||
* @param history the history
|
* @param history a history object
|
||||||
* @returns the title of an old element or null if it could not be found
|
* @returns the title of the history element or null if it could not be found
|
||||||
*/
|
*/
|
||||||
public getElementInfo(history: ViewHistory): string {
|
public getElementInfo(history: History): string {
|
||||||
const oldElementTitle = this.repo.getOldModelInfo(history.getCollectionString(), history.getModelId());
|
const model = this.viewModelStore.get(history.collectionString, history.modelId);
|
||||||
|
return model ? model.getListTitle() : null;
|
||||||
if (oldElementTitle) {
|
|
||||||
return oldElementTitle;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,10 +165,13 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
|||||||
*
|
*
|
||||||
* @param history Represents the selected element
|
* @param history Represents the selected element
|
||||||
*/
|
*/
|
||||||
public async onClickRow(history: ViewHistory): Promise<void> {
|
public async onClickRow(history: History): Promise<void> {
|
||||||
if (this.operator.isInGroupIds(2)) {
|
if (!this.isSuperAdmin) {
|
||||||
await this.repo.browseHistory(history);
|
return;
|
||||||
const element = this.viewModelStore.get(history.getCollectionString(), history.getModelId());
|
}
|
||||||
|
|
||||||
|
await this.timeTravelService.loadHistoryPoint(history);
|
||||||
|
const element = this.viewModelStore.get(history.collectionString, history.modelId);
|
||||||
if (element && isDetailNavigable(element)) {
|
if (element && isDetailNavigable(element)) {
|
||||||
this.router.navigate([element.getDetailStateURL()]);
|
this.router.navigate([element.getDetailStateURL()]);
|
||||||
} else {
|
} else {
|
||||||
@ -122,35 +179,53 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
|||||||
this.raiseError(message);
|
this.raiseError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public getTimestamp(viewHistory: ViewHistory): string {
|
public getTimestamp(history: History): string {
|
||||||
return viewHistory.history.getLocaleString(langToLocale(this.translate.currentLang));
|
return history.getLocaleString(langToLocale(this.translate.currentLang));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for the delete all button
|
* clears the whole history.
|
||||||
*/
|
*/
|
||||||
public onDeleteAllButton(): void {
|
public async clearHistory(): Promise<void> {
|
||||||
if (this.operator.isInGroupIds(2)) {
|
const title = this.translate.instant('Are you sure you want delete the whole history?');
|
||||||
this.repo.delete();
|
if (await this.promptService.open(title)) {
|
||||||
|
try {
|
||||||
|
await this.http.delete(`${environment.urlPrefix}/core/history/information/`);
|
||||||
|
this.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
this.raiseError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public refresh(): void {
|
||||||
|
if (this.currentCollection && this.currentModelId) {
|
||||||
|
this.queryElementId(this.currentCollection, this.currentModelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a translated history information string which contains optional (translated) arguments.
|
* Returns a translated history information string which contains optional (translated) arguments.
|
||||||
*
|
*
|
||||||
* @param information history information string
|
* @param history the history
|
||||||
*/
|
*/
|
||||||
public parseInformation(information: string): string {
|
public parseInformation(history: History): string {
|
||||||
if (information.length) {
|
if (!history.information || !history.information.length) {
|
||||||
const base_string = this.translate.instant(information[0]);
|
return '';
|
||||||
let argument_string;
|
|
||||||
if (information.length > 1) {
|
|
||||||
argument_string = this.translate.instant(information[1]);
|
|
||||||
}
|
}
|
||||||
return base_string.replace(/{arg1}/g, argument_string);
|
|
||||||
|
const baseString = this.translate.instant(history.information[0]);
|
||||||
|
let argumentString;
|
||||||
|
if (history.information.length > 1) {
|
||||||
|
argumentString = this.translate.instant(history.information[1]);
|
||||||
}
|
}
|
||||||
|
return baseString.replace(/{arg1}/g, argumentString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUserName(history: History): string {
|
||||||
|
const user = this.viewModelStore.get(ViewUser, history.user_id);
|
||||||
|
return user ? user.full_name : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,31 +238,13 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory, His
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overwrites the dataSource's string filter with a more advanced option
|
* Sets the data source to the request element id given by the collection string and the id.
|
||||||
* using the display methods of this class.
|
|
||||||
*/
|
*/
|
||||||
private setFilters(): void {
|
private async queryElementId(collectionString: string, id: number): Promise<void> {
|
||||||
this.dataSource.filterPredicate = (data, filter) => {
|
const historyData = await this.http.get<History[]>(`${environment.urlPrefix}/core/history/information/`, null, {
|
||||||
if (!data || !data.information) {
|
type: 'element',
|
||||||
return false;
|
value: `${collectionString}:${id}`
|
||||||
}
|
});
|
||||||
filter = filter ? filter.toLowerCase() : '';
|
this.dataSource.data = historyData.map(data => new History(data));
|
||||||
if (
|
|
||||||
this.getElementInfo(data) &&
|
|
||||||
this.getElementInfo(data)
|
|
||||||
.toLowerCase()
|
|
||||||
.indexOf(filter) >= 0
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (data.user && data.user.full_name.toLowerCase().indexOf(filter) >= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
this.parseInformation(data.information)
|
|
||||||
.toLowerCase()
|
|
||||||
.indexOf(filter) >= 0
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import { AppConfig } from '../../core/app-config';
|
import { AppConfig } from '../../core/app-config';
|
||||||
import { History } from 'app/shared/models/core/history';
|
|
||||||
import { HistoryRepositoryService } from 'app/core/repositories/history/history-repository.service';
|
|
||||||
import { ViewHistory } from './models/view-history';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config object for history.
|
* Config object for history.
|
||||||
@ -9,14 +6,6 @@ import { ViewHistory } from './models/view-history';
|
|||||||
*/
|
*/
|
||||||
export const HistoryAppConfig: AppConfig = {
|
export const HistoryAppConfig: AppConfig = {
|
||||||
name: 'history',
|
name: 'history',
|
||||||
models: [
|
|
||||||
{
|
|
||||||
collectionString: 'core/history',
|
|
||||||
model: History,
|
|
||||||
viewModel: ViewHistory,
|
|
||||||
repository: HistoryRepositoryService
|
|
||||||
}
|
|
||||||
],
|
|
||||||
mainMenuEntries: [
|
mainMenuEntries: [
|
||||||
{
|
{
|
||||||
route: '/history',
|
route: '/history',
|
||||||
|
@ -1,102 +0,0 @@
|
|||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
|
||||||
import { History } from 'app/shared/models/core/history';
|
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
|
|
||||||
export type ProxyHistory = History & { user?: ViewUser };
|
|
||||||
|
|
||||||
export interface HistoryTitleInformation {
|
|
||||||
element_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View model for history objects
|
|
||||||
*/
|
|
||||||
export class ViewHistory extends BaseViewModel<ProxyHistory> implements HistoryTitleInformation {
|
|
||||||
public static COLLECTIONSTRING = History.COLLECTIONSTRING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the history property
|
|
||||||
*/
|
|
||||||
public get history(): ProxyHistory {
|
|
||||||
return this._model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the users ViewUser.
|
|
||||||
*/
|
|
||||||
public get user(): ViewUser | null {
|
|
||||||
return this.history.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the id of the history object
|
|
||||||
* Required by BaseViewModel
|
|
||||||
*
|
|
||||||
* @returns the id as number
|
|
||||||
*/
|
|
||||||
public get id(): number {
|
|
||||||
return this.history.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns the users full name
|
|
||||||
*/
|
|
||||||
public get user_full_name(): string {
|
|
||||||
return this.history.user ? this.history.user.full_name : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the elementIDs of the history object
|
|
||||||
*
|
|
||||||
* @returns the element ID as String
|
|
||||||
*/
|
|
||||||
public get element_id(): string {
|
|
||||||
return this.history.element_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the information about the history
|
|
||||||
*
|
|
||||||
* @returns a string with the information to the history object
|
|
||||||
*/
|
|
||||||
public get information(): string {
|
|
||||||
return this.history.information;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the time of the history as number
|
|
||||||
*
|
|
||||||
* @returns the unix timestamp as number
|
|
||||||
*/
|
|
||||||
public get now(): string {
|
|
||||||
return this.history.now;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construction of a ViewHistory
|
|
||||||
*
|
|
||||||
* @param history the real history BaseModel
|
|
||||||
* @param user the real user BaseModel
|
|
||||||
*/
|
|
||||||
public constructor(history: ProxyHistory) {
|
|
||||||
super(History.COLLECTIONSTRING, history);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts elementID into collection string
|
|
||||||
* @returns the CollectionString to the model
|
|
||||||
*/
|
|
||||||
public getCollectionString(): string {
|
|
||||||
return this.element_id.split(':')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the models ID from the elementID
|
|
||||||
* @returns a model id
|
|
||||||
*/
|
|
||||||
public getModelId(): number {
|
|
||||||
return +this.element_id.split(':')[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateDependencies(update: BaseViewModel): void {}
|
|
||||||
}
|
|
@ -531,6 +531,9 @@ button.mat-menu-item.selected {
|
|||||||
.spacer-left-10 {
|
.spacer-left-10 {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
.spacer-left-20 {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
.spacer-left-50 {
|
.spacer-left-50 {
|
||||||
margin-left: 50px !important;
|
margin-left: 50px !important;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
],
|
],
|
||||||
"no-arg": true,
|
"no-arg": true,
|
||||||
"no-bitwise": true,
|
"no-bitwise": true,
|
||||||
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
|
"no-console": [true, "table", "clear", "count", "countReset", "info", "time", "timeEnd", "timeline", "timelineEnd", "trace"],
|
||||||
"no-construct": true,
|
"no-construct": true,
|
||||||
"no-debugger": true,
|
"no-debugger": true,
|
||||||
"no-duplicate-super": true,
|
"no-duplicate-super": true,
|
||||||
|
@ -44,11 +44,3 @@ class ConfigAccessPermissions(BaseAccessPermissions):
|
|||||||
Access permissions container for the config (ConfigStore and
|
Access permissions container for the config (ConfigStore and
|
||||||
ConfigViewSet).
|
ConfigViewSet).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class HistoryAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for the Histroy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "core.can_see_history"
|
|
||||||
|
@ -29,7 +29,6 @@ class CoreAppConfig(AppConfig):
|
|||||||
from .views import (
|
from .views import (
|
||||||
ConfigViewSet,
|
ConfigViewSet,
|
||||||
CountdownViewSet,
|
CountdownViewSet,
|
||||||
HistoryViewSet,
|
|
||||||
ProjectorMessageViewSet,
|
ProjectorMessageViewSet,
|
||||||
ProjectorViewSet,
|
ProjectorViewSet,
|
||||||
ProjectionDefaultViewSet,
|
ProjectionDefaultViewSet,
|
||||||
@ -91,9 +90,6 @@ class CoreAppConfig(AppConfig):
|
|||||||
router.register(
|
router.register(
|
||||||
self.get_model("Countdown").get_collection_string(), CountdownViewSet
|
self.get_model("Countdown").get_collection_string(), CountdownViewSet
|
||||||
)
|
)
|
||||||
router.register(
|
|
||||||
self.get_model("History").get_collection_string(), HistoryViewSet
|
|
||||||
)
|
|
||||||
|
|
||||||
if "runserver" in sys.argv or "changeconfig" in sys.argv:
|
if "runserver" in sys.argv or "changeconfig" in sys.argv:
|
||||||
startup()
|
startup()
|
||||||
@ -123,7 +119,6 @@ class CoreAppConfig(AppConfig):
|
|||||||
"ProjectorMessage",
|
"ProjectorMessage",
|
||||||
"Countdown",
|
"Countdown",
|
||||||
"ConfigStore",
|
"ConfigStore",
|
||||||
"History",
|
|
||||||
):
|
):
|
||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
|
22
openslides/core/migrations/0024_auto_20190605_1105.py
Normal file
22
openslides/core/migrations/0024_auto_20190605_1105.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 2.2.1 on 2019-06-05 09:05
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("core", "0023_chyron_colors")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="history",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
@ -10,7 +10,6 @@ from ..utils.models import SET_NULL_AND_AUTOUPDATE, RESTModelMixin
|
|||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
CountdownAccessPermissions,
|
CountdownAccessPermissions,
|
||||||
HistoryAccessPermissions,
|
|
||||||
ProjectionDefaultAccessPermissions,
|
ProjectionDefaultAccessPermissions,
|
||||||
ProjectorAccessPermissions,
|
ProjectorAccessPermissions,
|
||||||
ProjectorMessageAccessPermissions,
|
ProjectorMessageAccessPermissions,
|
||||||
@ -260,12 +259,8 @@ class HistoryManager(models.Manager):
|
|||||||
instances = []
|
instances = []
|
||||||
history_time = now()
|
history_time = now()
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if (
|
if element.get("disable_history"):
|
||||||
element.get("disable_history")
|
# Do not update history if history is disabled.
|
||||||
or element["collection_string"]
|
|
||||||
== self.model.get_collection_string()
|
|
||||||
):
|
|
||||||
# Do not update history for history elements itself or if history is disabled.
|
|
||||||
continue
|
continue
|
||||||
# HistoryData is not a root rest element so there is no autoupdate and not history saving here.
|
# HistoryData is not a root rest element so there is no autoupdate and not history saving here.
|
||||||
data = HistoryData.objects.create(full_data=element["full_data"])
|
data = HistoryData.objects.create(full_data=element["full_data"])
|
||||||
@ -279,9 +274,7 @@ class HistoryManager(models.Manager):
|
|||||||
user_id=element.get("user_id"),
|
user_id=element.get("user_id"),
|
||||||
full_data=data,
|
full_data=data,
|
||||||
)
|
)
|
||||||
instance.save(
|
instance.save()
|
||||||
skip_autoupdate=True
|
|
||||||
) # Skip autoupdate and of course history saving.
|
|
||||||
instances.append(instance)
|
instances.append(instance)
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
@ -307,7 +300,7 @@ class HistoryManager(models.Manager):
|
|||||||
return instances
|
return instances
|
||||||
|
|
||||||
|
|
||||||
class History(RESTModelMixin, models.Model):
|
class History(models.Model):
|
||||||
"""
|
"""
|
||||||
Django model to save the history of OpenSlides.
|
Django model to save the history of OpenSlides.
|
||||||
|
|
||||||
@ -315,8 +308,6 @@ class History(RESTModelMixin, models.Model):
|
|||||||
delete a user you may lose the information of the user field here.
|
delete a user you may lose the information of the user field here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = HistoryAccessPermissions()
|
|
||||||
|
|
||||||
objects = HistoryManager()
|
objects = HistoryManager()
|
||||||
|
|
||||||
element_id = models.CharField(max_length=255)
|
element_id = models.CharField(max_length=255)
|
||||||
@ -328,7 +319,7 @@ class History(RESTModelMixin, models.Model):
|
|||||||
restricted = models.BooleanField(default=False)
|
restricted = models.BooleanField(default=False)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, null=True, on_delete=SET_NULL_AND_AUTOUPDATE
|
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
||||||
)
|
)
|
||||||
|
|
||||||
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
||||||
|
@ -12,7 +12,6 @@ from ..utils.validate import validate_html
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConfigStore,
|
ConfigStore,
|
||||||
Countdown,
|
Countdown,
|
||||||
History,
|
|
||||||
ProjectionDefault,
|
ProjectionDefault,
|
||||||
Projector,
|
Projector,
|
||||||
ProjectorMessage,
|
ProjectorMessage,
|
||||||
@ -173,18 +172,3 @@ class CountdownSerializer(ModelSerializer):
|
|||||||
"running",
|
"running",
|
||||||
)
|
)
|
||||||
unique_together = ("title",)
|
unique_together = ("title",)
|
||||||
|
|
||||||
|
|
||||||
class HistorySerializer(ModelSerializer):
|
|
||||||
"""
|
|
||||||
Serializer for core.models.Countdown objects.
|
|
||||||
|
|
||||||
Does not contain full data of history object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
information = JSONSerializerField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = History
|
|
||||||
fields = ("id", "element_id", "now", "information", "restricted", "user")
|
|
||||||
read_only_fields = ("now",)
|
|
||||||
|
@ -6,5 +6,10 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
|
url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
|
||||||
url(r"^version/$", views.VersionView.as_view(), name="core_version"),
|
url(r"^version/$", views.VersionView.as_view(), name="core_version"),
|
||||||
url(r"^history/$", views.HistoryView.as_view(), name="core_history"),
|
url(
|
||||||
|
r"^history/information/$",
|
||||||
|
views.HistoryInformationView.as_view(),
|
||||||
|
name="core_history_information",
|
||||||
|
),
|
||||||
|
url(r"^history/data/$", views.HistoryDataView.as_view(), name="core_history_data"),
|
||||||
]
|
]
|
||||||
|
@ -16,7 +16,7 @@ from ..users.models import User
|
|||||||
from ..utils import views as utils_views
|
from ..utils import views as utils_views
|
||||||
from ..utils.arguments import arguments
|
from ..utils.arguments import arguments
|
||||||
from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
|
from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
|
||||||
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from ..utils.plugins import (
|
from ..utils.plugins import (
|
||||||
get_plugin_description,
|
get_plugin_description,
|
||||||
get_plugin_license,
|
get_plugin_license,
|
||||||
@ -32,12 +32,10 @@ from ..utils.rest_api import (
|
|||||||
RetrieveModelMixin,
|
RetrieveModelMixin,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
list_route,
|
|
||||||
)
|
)
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
CountdownAccessPermissions,
|
CountdownAccessPermissions,
|
||||||
HistoryAccessPermissions,
|
|
||||||
ProjectionDefaultAccessPermissions,
|
ProjectionDefaultAccessPermissions,
|
||||||
ProjectorAccessPermissions,
|
ProjectorAccessPermissions,
|
||||||
ProjectorMessageAccessPermissions,
|
ProjectorMessageAccessPermissions,
|
||||||
@ -442,53 +440,6 @@ class CountdownViewSet(ModelViewSet):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint for History.
|
|
||||||
|
|
||||||
There are the following views: list, retrieve, clear_history.
|
|
||||||
"""
|
|
||||||
|
|
||||||
access_permissions = HistoryAccessPermissions()
|
|
||||||
queryset = History.objects.all()
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
|
||||||
"""
|
|
||||||
Returns True if the user has required permissions.
|
|
||||||
"""
|
|
||||||
if self.action in ("list", "retrieve"):
|
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "clear_history":
|
|
||||||
result = in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK])
|
|
||||||
else:
|
|
||||||
result = False
|
|
||||||
return result
|
|
||||||
|
|
||||||
@list_route(methods=["post"])
|
|
||||||
def clear_history(self, request):
|
|
||||||
"""
|
|
||||||
Deletes and rebuilds the history.
|
|
||||||
"""
|
|
||||||
# Collect all history objects with their collection_string and id.
|
|
||||||
args = []
|
|
||||||
for history_obj in History.objects.all():
|
|
||||||
args.append((history_obj.get_collection_string(), history_obj.pk))
|
|
||||||
|
|
||||||
# Delete history data and history (via CASCADE)
|
|
||||||
HistoryData.objects.all().delete()
|
|
||||||
|
|
||||||
# Trigger autoupdate.
|
|
||||||
if args:
|
|
||||||
inform_deleted_data(args)
|
|
||||||
|
|
||||||
# Rebuild history.
|
|
||||||
history_instances = History.objects.build_history()
|
|
||||||
inform_changed_data(history_instances)
|
|
||||||
|
|
||||||
# Setup response.
|
|
||||||
return Response({"detail": "History was deleted successfully."})
|
|
||||||
|
|
||||||
|
|
||||||
# Special API views
|
# Special API views
|
||||||
|
|
||||||
|
|
||||||
@ -533,7 +484,73 @@ class VersionView(utils_views.APIView):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class HistoryView(utils_views.APIView):
|
class HistoryInformationView(utils_views.APIView):
|
||||||
|
"""
|
||||||
|
View to retrieve information about OpenSlides history.
|
||||||
|
|
||||||
|
Use GET to search history information. The query parameter 'type' determines
|
||||||
|
the type of your search:
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
/?type=element&value=motions%2Fmotion%3A42 if your search for motion 42
|
||||||
|
|
||||||
|
Use DELETE to clear the history.
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_method_names = ["get", "delete"]
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
"""
|
||||||
|
Checks permission and parses query parameters.
|
||||||
|
"""
|
||||||
|
if not has_perm(self.request.user, "users.can_see_history"):
|
||||||
|
self.permission_denied(self.request)
|
||||||
|
type = self.request.query_params.get("type")
|
||||||
|
value = self.request.query_params.get("value")
|
||||||
|
if type not in ("element"):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid input. Type should be 'element' or 'text'."}
|
||||||
|
)
|
||||||
|
# We currently just support searching by element id.
|
||||||
|
data = self.get_data_element_search(value)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_data_element_search(self, value):
|
||||||
|
"""
|
||||||
|
Retrieves history information for element search.
|
||||||
|
"""
|
||||||
|
data = []
|
||||||
|
for instance in History.objects.filter(element_id=value).order_by("-now"):
|
||||||
|
data.append(
|
||||||
|
{
|
||||||
|
"element_id": instance.element_id,
|
||||||
|
"timestamp": instance.now.timestamp(),
|
||||||
|
"information": instance.information,
|
||||||
|
"resticted": instance.restricted,
|
||||||
|
"user_id": instance.user.pk if instance.user else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Deletes and rebuilds the history.
|
||||||
|
"""
|
||||||
|
# Check permission
|
||||||
|
if not in_some_groups(request.user.pk or 0, [GROUP_ADMIN_PK]):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
# Delete history data and history (via CASCADE)
|
||||||
|
HistoryData.objects.all().delete()
|
||||||
|
|
||||||
|
# Rebuild history.
|
||||||
|
History.objects.build_history()
|
||||||
|
|
||||||
|
return Response({"detail": "History was deleted and rebuild successfully."})
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryDataView(utils_views.APIView):
|
||||||
"""
|
"""
|
||||||
View to retrieve the history data of OpenSlides.
|
View to retrieve the history data of OpenSlides.
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import itertools
|
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
@ -234,27 +233,10 @@ def handle_changed_elements(elements: Iterable[Element]) -> None:
|
|||||||
element["full_data"] = instance.get_full_data()
|
element["full_data"] = instance.get_full_data()
|
||||||
|
|
||||||
# Save histroy here using sync code.
|
# Save histroy here using sync code.
|
||||||
history_instances = save_history(elements)
|
save_history(elements)
|
||||||
|
|
||||||
# Convert history instances to Elements.
|
|
||||||
history_elements: List[Element] = []
|
|
||||||
for history_instance in history_instances:
|
|
||||||
history_elements.append(
|
|
||||||
Element(
|
|
||||||
id=history_instance.get_rest_pk(),
|
|
||||||
collection_string=history_instance.get_collection_string(),
|
|
||||||
full_data=history_instance.get_full_data(),
|
|
||||||
disable_history=True, # This does not matter because history elements can never be part of the history itself.
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Chain elements and history elements.
|
|
||||||
itertools.chain(elements, history_elements)
|
|
||||||
|
|
||||||
# Update cache and send autoupdate using async code.
|
# Update cache and send autoupdate using async code.
|
||||||
async_to_sync(async_handle_collection_elements)(
|
async_to_sync(async_handle_collection_elements)(elements)
|
||||||
itertools.chain(elements, history_elements)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def save_history(elements: Iterable[Element]) -> Iterable:
|
def save_history(elements: Iterable[Element]) -> Iterable:
|
||||||
|
@ -111,7 +111,7 @@ class WebsocketThroughputLogger:
|
|||||||
async def check_and_flush(self) -> None:
|
async def check_and_flush(self) -> None:
|
||||||
# If we waited longer then 60 seconds, flush the data.
|
# If we waited longer then 60 seconds, flush the data.
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time > (self.time + 20):
|
if current_time > (self.time + 60):
|
||||||
|
|
||||||
send_ratio = receive_ratio = 1.0
|
send_ratio = receive_ratio = 1.0
|
||||||
if self.send_compressed > 0:
|
if self.send_compressed > 0:
|
||||||
|
Loading…
Reference in New Issue
Block a user