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 { take, filter } from 'rxjs/operators';
import { ConfigService } from './core/ui-services/config.service'; 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 { CountUsersService } from './core/ui-services/count-users.service';
import { LoadFontService } from './core/ui-services/load-font.service'; import { LoadFontService } from './core/ui-services/load-font.service';
import { LoginDataService } from './core/ui-services/login-data.service'; import { LoginDataService } from './core/ui-services/login-data.service';
import { OperatorService } from './core/core-services/operator.service'; import { OperatorService } from './core/core-services/operator.service';
import { ServertimeService } from './core/core-services/servertime.service'; import { ServertimeService } from './core/core-services/servertime.service';
import { ThemeService } from './core/ui-services/theme.service'; import { ThemeService } from './core/ui-services/theme.service';
import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade.service';
/** /**
* Angular's global App Component * Angular's global App Component
@ -48,7 +49,8 @@ export class AppComponent {
themeService: ThemeService, themeService: ThemeService,
countUsersService: CountUsersService, // Needed to register itself. countUsersService: CountUsersService, // Needed to register itself.
configService: ConfigService, configService: ConfigService,
loadFontService: LoadFontService loadFontService: LoadFontService,
dataStoreUpgradeService: DataStoreUpgradeService // to start it.
) { ) {
// manually add the supported languages // manually add the supported languages
translate.addLangs(['en', 'de', 'cs']); 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); console.log('requesting changed objects with DS max change id', this.DS.maxChangeId + 1);
this.websocketService.send('getElements', { 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 { Injectable } from '@angular/core';
import { WebsocketService } from '../core-services/websocket.service'; import { WebsocketService } from './websocket.service';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
/** /**
@ -15,7 +15,7 @@ interface Constants {
* *
* @example * @example
* ```ts * ```ts
* this.constantsService.get('OpenSlidesSettings').subscribe(constant => { * this.constantsService.get('Settings').subscribe(constant => {
* console.log(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. * The generic message format in which messages are send and recieved by the server.
*/ */
interface WebsocketMessage { interface BaseWebsocketMessage {
type: string; type: string;
content: any; content: any;
}
/**
* Outgoing messages must have an id.
*/
interface OutgoingWebsocketMessage extends BaseWebsocketMessage {
id: string; 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 * Service that handles WebSocket connections. Other services can register themselfs
* with {@method getOberservable} for a specific type of messages. The content will be published. * 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 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 * Saves, if the WS Connection should be closed (e.g. after an explicit `close()`). Prohibits
* retry connection attempts. * retry connection attempts.
@ -180,16 +196,7 @@ export class WebsocketService {
this.websocket.onmessage = (event: MessageEvent) => { this.websocket.onmessage = (event: MessageEvent) => {
this.zone.run(() => { this.zone.run(() => {
const message: WebsocketMessage = JSON.parse(event.data); this.handleMessage(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);
}
}); });
}; };
@ -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 { private dismissConnectionErrorNotice(): void {
if (this.connectionErrorNotice) { if (this.connectionErrorNotice) {
this.connectionErrorNotice.dismiss(); this.connectionErrorNotice.dismiss();
@ -269,13 +318,23 @@ export class WebsocketService {
* *
* @param type the message type * @param type the message type
* @param content the actual content * @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) { if (!this.websocket) {
return; return;
} }
const message: WebsocketMessage = { const message: OutgoingWebsocketMessage = {
type: type, type: type,
content: content, content: content,
id: id 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. // Either send directly or add to queue, if not connected.
const jsonMessage = JSON.stringify(message); const jsonMessage = JSON.stringify(message);
if (this.isConnected) { if (this.isConnected) {
@ -297,5 +360,29 @@ export class WebsocketService {
} else { } else {
this.sendQueueWhileNotConnected.push(jsonMessage); 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 { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from 'app/core/repositories/base-repository'; import { BaseRepository } from 'app/core/repositories/base-repository';
import { Config } from 'app/shared/models/core/config'; import { Config } from 'app/shared/models/core/config';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from 'app/core/core-services/data-store.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 { HttpService } from 'app/core/core-services/http.service';
import { Identifiable } from 'app/shared/models/base/identifiable'; import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewConfig } from 'app/site/config/models/view-config'; import { ViewConfig } from 'app/site/config/models/view-config';
import { TranslateService } from '@ngx-translate/core';
/** /**
* Holds a single config item. * Holds a single config item.
@ -107,7 +107,7 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Config); super(DS, dataSend, mapperService, viewModelStoreService, translate, Config);
this.constantsService.get('OpenSlidesConfigVariables').subscribe(constant => { this.constantsService.get('ConfigVariables').subscribe(constant => {
this.createConfigStructure(constant); this.createConfigStructure(constant);
this.updateConfigStructure(...Object.values(this.viewModelStore)); this.updateConfigStructure(...Object.values(this.viewModelStore));
this.updateConfigListObservable(); this.updateConfigListObservable();

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { BaseRepository } from '../base-repository'; import { BaseRepository } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; 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 { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { Group } from 'app/shared/models/users/group'; import { Group } from 'app/shared/models/users/group';

View File

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

View File

@ -12,7 +12,7 @@ interface ContentObject {
/** /**
* Determine visibility states for agenda items * 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 = [ export const itemVisibilityChoices = [
{ key: 1, name: 'Public item', csvName: '' }, { 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 { AssignmentPollService } from '../../services/assignment-poll.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { BaseViewComponent } from 'app/site/base/base-view'; 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 { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service'; import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { OperatorService } from 'app/core/core-services/operator.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 { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { StorageService } from 'app/core/core-services/storage.service'; import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment, AssignmentPhase } from '../models/view-assignment'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CalculablePollKey } from 'app/core/ui-services/poll.service'; 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 { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { MotionPoll } from 'app/shared/models/motions/motion-poll'; import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { MotionPollDialogComponent } from './motion-poll-dialog.component'; 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 * Subscribe to the available majority choices as given in the server-side constants
*/ */
private subscribeMajorityChoices(): void { 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'); const motionconst = constants.find(c => c.name === 'Motions');
if (motionconst) { if (motionconst) {
const ballotConst = motionconst.subgroups.find(s => s.name === 'Voting and ballot papers'); 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 { OperatorService } from 'app/core/core-services/operator.service';
import { ViewMotion } from '../models/view-motion'; import { ViewMotion } from '../models/view-motion';
import { ConfigService } from 'app/core/ui-services/config.service'; 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; MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS: boolean;
} }
@ -30,7 +30,7 @@ export class LocalPermissionsService {
.get<boolean>('motions_amendments_enabled') .get<boolean>('motions_amendments_enabled')
.subscribe(enabled => (this.amendmentEnabled = enabled)); .subscribe(enabled => (this.amendmentEnabled = enabled));
this.constants this.constants
.get<OpenSlidesSettings>('OpenSlidesSettings') .get<Settings>('Settings')
.subscribe(settings => (this.amendmentOfAmendment = settings.MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS)); .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.apps import AppConfig
from django.conf import settings from django.conf import settings
from django.db.models import Max
from django.db.models.signals import post_migrate, pre_delete 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 # Settings key does not exist. Do nothing. The client will
# treat this as undefined. # treat this as undefined.
pass pass
constants["OpenSlidesSettings"] = client_settings_dict constants["Settings"] = client_settings_dict
# Config variables # Config variables
config_groups: List[Any] = [] config_groups: List[Any] = []
@ -181,7 +182,14 @@ class CoreAppConfig(AppConfig):
) )
# Add the config variable to the current group and subgroup. # Add the config variable to the current group and subgroup.
config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data) 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 return constants