Refresh clients cache when the database was migrated

This commit is contained in:
FinnStutzenstein 2019-04-15 13:30:18 +02:00
parent e9a60a54fd
commit a715c0e432
16 changed files with 198 additions and 31 deletions

View File

@ -4,13 +4,14 @@ import { TranslateService } from '@ngx-translate/core';
import { take, filter } from 'rxjs/operators';
import { ConfigService } from './core/ui-services/config.service';
import { ConstantsService } from './core/ui-services/constants.service';
import { ConstantsService } from './core/core-services/constants.service';
import { CountUsersService } from './core/ui-services/count-users.service';
import { LoadFontService } from './core/ui-services/load-font.service';
import { LoginDataService } from './core/ui-services/login-data.service';
import { OperatorService } from './core/core-services/operator.service';
import { ServertimeService } from './core/core-services/servertime.service';
import { ThemeService } from './core/ui-services/theme.service';
import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade.service';
/**
* Angular's global App Component
@ -48,7 +49,8 @@ export class AppComponent {
themeService: ThemeService,
countUsersService: CountUsersService, // Needed to register itself.
configService: ConfigService,
loadFontService: LoadFontService
loadFontService: LoadFontService,
dataStoreUpgradeService: DataStoreUpgradeService // to start it.
) {
// manually add the supported languages
translate.addLangs(['en', 'de', 'cs']);

View File

@ -145,4 +145,22 @@ export class AutoupdateService {
console.log('requesting changed objects with DS max change id', this.DS.maxChangeId + 1);
this.websocketService.send('getElements', { change_id: this.DS.maxChangeId + 1 });
}
/**
* Does a full update: Requests all data from the server and sets the DS to the fresh data.
*/
public async doFullUpdate(): Promise<void> {
const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {});
let allModels: BaseModel[] = [];
for (const collection of Object.keys(response.changed)) {
if (this.modelMapper.isCollectionRegistered(collection)) {
allModels = allModels.concat(this.mapObjectsToBaseModels(collection, response.changed[collection]));
} else {
console.error(`Unregistered collection "${collection}". Ignore it.`);
}
}
await this.DS.set(allModels, response.to_change_id);
}
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { WebsocketService } from '../core-services/websocket.service';
import { WebsocketService } from './websocket.service';
import { Observable, of, Subject } from 'rxjs';
/**
@ -15,7 +15,7 @@ interface Constants {
*
* @example
* ```ts
* this.constantsService.get('OpenSlidesSettings').subscribe(constant => {
* this.constantsService.get('Settings').subscribe(constant => {
* console.log(constant);
* });
* ```

View File

@ -0,0 +1,17 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from '../../../e2e-imports.module';
import { DataStoreUpgradeService } from './data-store-upgrade.service';
describe('DataStoreUpgradeService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [DataStoreUpgradeService]
});
});
it('should be created', inject([DataStoreUpgradeService], (service: DataStoreUpgradeService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { ConstantsService } from './constants.service';
import { AutoupdateService } from './autoupdate.service';
import { StorageService } from './storage.service';
const MIGRATIONVERSION = 'MigrationVersion';
/**
* Manages upgrading the DataStore, if the migration version from the server is higher than the current one.
*/
@Injectable({
providedIn: 'root'
})
export class DataStoreUpgradeService {
/**
* @param autoupdateService
* @param constantsService
* @param storageService
*/
public constructor(
autoupdateService: AutoupdateService,
constantsService: ConstantsService,
storageService: StorageService
) {
constantsService.get<number>(MIGRATIONVERSION).subscribe(async version => {
const currentVersion = await storageService.get<number>(MIGRATIONVERSION);
await storageService.set(MIGRATIONVERSION, version);
if (currentVersion && currentVersion !== version) {
autoupdateService.doFullUpdate();
}
});
}
}

View File

@ -10,12 +10,26 @@ import { formatQueryParams, QueryParams } from '../query-params';
/**
* The generic message format in which messages are send and recieved by the server.
*/
interface WebsocketMessage {
interface BaseWebsocketMessage {
type: string;
content: any;
}
/**
* Outgoing messages must have an id.
*/
interface OutgoingWebsocketMessage extends BaseWebsocketMessage {
id: string;
}
/**
* Incomming messages may have an `in_response`, if they are an answer to a previously
* submitted request.
*/
interface IncommingWebsocketMessage extends BaseWebsocketMessage {
in_response?: string;
}
/**
* Service that handles WebSocket connections. Other services can register themselfs
* with {@method getOberservable} for a specific type of messages. The content will be published.
@ -89,6 +103,8 @@ export class WebsocketService {
*/
private subjects: { [type: string]: Subject<any> } = {};
private responseCallbacks: { [id: string]: [(val: any) => boolean, (error: string) => void | null] } = {};
/**
* Saves, if the WS Connection should be closed (e.g. after an explicit `close()`). Prohibits
* retry connection attempts.
@ -180,16 +196,7 @@ export class WebsocketService {
this.websocket.onmessage = (event: MessageEvent) => {
this.zone.run(() => {
const message: WebsocketMessage = JSON.parse(event.data);
const type: string = message.type;
if (type === 'error') {
console.error('Websocket error', message.content);
} else if (this.subjects[type]) {
// Pass the content to the registered subscribers.
this.subjects[type].next(message.content);
} else {
console.log(`Got unknown websocket message type "${type}" with content`, message.content);
}
this.handleMessage(event.data);
});
};
@ -234,6 +241,48 @@ export class WebsocketService {
};
}
/**
* Handles an incomming message.
*
* @param data The message
*/
private handleMessage(data: string): void {
const message: IncommingWebsocketMessage = JSON.parse(data);
const type = message.type;
const inResponse = message.in_response;
const callbacks = this.responseCallbacks[inResponse];
if (callbacks) {
delete this.responseCallbacks[inResponse];
}
if (type === 'error') {
console.error('Websocket error', message.content);
if (inResponse && callbacks && callbacks[1]) {
callbacks[1](message.content as string);
}
return;
}
// Try to fire a response callback directly. If it returnes true, the message is handeled
// and not distributed further
if (inResponse && callbacks && callbacks[0](message.content)) {
return;
}
if (this.subjects[type]) {
// Pass the content to the registered subscribers.
this.subjects[type].next(message.content);
} else {
console.warn(
`Got unknown websocket message type "${type}" (inResponse: ${inResponse}) with content`,
message.content
);
}
}
/**
* Closes the connection error notice
*/
private dismissConnectionErrorNotice(): void {
if (this.connectionErrorNotice) {
this.connectionErrorNotice.dismiss();
@ -269,13 +318,23 @@ export class WebsocketService {
*
* @param type the message type
* @param content the actual content
* @param success an optional success callback for a response
* @param error an optional error callback for a response
* @param id an optional id for the message. If not given, a random id will be generated and returned.
* @returns the message id
*/
public send<T>(type: string, content: T, id?: string): void {
public send<T, R>(
type: string,
content: T,
success?: (val: R) => boolean,
error?: (error: string) => void,
id?: string
): string {
if (!this.websocket) {
return;
}
const message: WebsocketMessage = {
const message: OutgoingWebsocketMessage = {
type: type,
content: content,
id: id
@ -290,6 +349,10 @@ export class WebsocketService {
}
}
if (success) {
this.responseCallbacks[message.id] = [success, error];
}
// Either send directly or add to queue, if not connected.
const jsonMessage = JSON.stringify(message);
if (this.isConnected) {
@ -297,5 +360,29 @@ export class WebsocketService {
} else {
this.sendQueueWhileNotConnected.push(jsonMessage);
}
return message.id;
}
/**
* Sends a message and waits for the response
*
* @param type the message type
* @param content the actual content
* @param id an optional id for the message. If not given, a random id will be generated and returned.
*/
public sendAndGetResponse<T, R>(type: string, content: T, id?: string): Promise<R> {
return new Promise<R>((resolve, reject) => {
this.send<T, R>(
type,
content,
val => {
resolve(val);
return true;
},
reject,
id
);
});
}
}

View File

@ -1,18 +1,18 @@
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { Config } from 'app/shared/models/core/config';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { ConstantsService } from 'app/core/ui-services/constants.service';
import { ConstantsService } from 'app/core/core-services/constants.service';
import { HttpService } from 'app/core/core-services/http.service';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewConfig } from 'app/site/config/models/view-config';
import { TranslateService } from '@ngx-translate/core';
/**
* Holds a single config item.
@ -107,7 +107,7 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Config);
this.constantsService.get('OpenSlidesConfigVariables').subscribe(constant => {
this.constantsService.get('ConfigVariables').subscribe(constant => {
this.createConfigStructure(constant);
this.updateConfigStructure(...Object.values(this.viewModelStore));
this.updateConfigListObservable();

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { ConstantsService } from '../../ui-services/constants.service';
import { ConstantsService } from '../../core-services/constants.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { Group } from 'app/shared/models/users/group';

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { Config } from '../../shared/models/core/config';
import { DataStoreService } from '../core-services/data-store.service';

View File

@ -12,7 +12,7 @@ interface ContentObject {
/**
* Determine visibility states for agenda items
* Coming from "OpenSlidesConfigVariables" property "agenda_hide_internal_items_on_projector"
* Coming from "ConfigVariables" property "agenda_hide_internal_items_on_projector"
*/
export const itemVisibilityChoices = [
{ key: 1, name: 'Public item', csvName: '' },

View File

@ -11,7 +11,7 @@ import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentPollService } from '../../services/assignment-poll.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ConstantsService } from 'app/core/ui-services/constants.service';
import { ConstantsService } from 'app/core/core-services/constants.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { OperatorService } from 'app/core/core-services/operator.service';

View File

@ -5,7 +5,7 @@ import { Assignment } from 'app/shared/models/assignments/assignment';
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment, AssignmentPhase } from '../models/view-assignment';
import { ConstantsService } from 'app/core/ui-services/constants.service';
import { ConstantsService } from 'app/core/core-services/constants.service';
@Injectable({
providedIn: 'root'

View File

@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
import { ConstantsService } from 'app/core/ui-services/constants.service';
import { ConstantsService } from 'app/core/core-services/constants.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { MotionPollDialogComponent } from './motion-poll-dialog.component';
@ -209,7 +209,7 @@ export class MotionPollComponent implements OnInit {
* Subscribe to the available majority choices as given in the server-side constants
*/
private subscribeMajorityChoices(): void {
this.constants.get<any>('OpenSlidesConfigVariables').subscribe(constants => {
this.constants.get<any>('ConfigVariables').subscribe(constants => {
const motionconst = constants.find(c => c.name === 'Motions');
if (motionconst) {
const ballotConst = motionconst.subgroups.find(s => s.name === 'Voting and ballot papers');

View File

@ -3,9 +3,9 @@ import { Injectable } from '@angular/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ViewMotion } from '../models/view-motion';
import { ConfigService } from 'app/core/ui-services/config.service';
import { ConstantsService } from 'app/core/ui-services/constants.service';
import { ConstantsService } from 'app/core/core-services/constants.service';
interface OpenSlidesSettings {
interface Settings {
MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS: boolean;
}
@ -30,7 +30,7 @@ export class LocalPermissionsService {
.get<boolean>('motions_amendments_enabled')
.subscribe(enabled => (this.amendmentEnabled = enabled));
this.constants
.get<OpenSlidesSettings>('OpenSlidesSettings')
.get<Settings>('Settings')
.subscribe(settings => (this.amendmentOfAmendment = settings.MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS));
}

View File

@ -6,6 +6,7 @@ from typing import Any, Dict, List, Set
from django.apps import AppConfig
from django.conf import settings
from django.db.models import Max
from django.db.models.signals import post_migrate, pre_delete
@ -155,7 +156,7 @@ class CoreAppConfig(AppConfig):
# Settings key does not exist. Do nothing. The client will
# treat this as undefined.
pass
constants["OpenSlidesSettings"] = client_settings_dict
constants["Settings"] = client_settings_dict
# Config variables
config_groups: List[Any] = []
@ -181,7 +182,14 @@ class CoreAppConfig(AppConfig):
)
# Add the config variable to the current group and subgroup.
config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data)
constants["OpenSlidesConfigVariables"] = config_groups
constants["ConfigVariables"] = config_groups
# get max migration id -> the "version" of the DB
from django.db.migrations.recorder import MigrationRecorder
constants["MigrationVersion"] = MigrationRecorder.Migration.objects.aggregate(
Max("id")
)["id__max"]
return constants