diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 7c99755db..f0e6dc613 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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']); diff --git a/client/src/app/core/core-services/autoupdate.service.ts b/client/src/app/core/core-services/autoupdate.service.ts index c771352dc..68d5728fd 100644 --- a/client/src/app/core/core-services/autoupdate.service.ts +++ b/client/src/app/core/core-services/autoupdate.service.ts @@ -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 { + 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); + } } diff --git a/client/src/app/core/ui-services/constants.service.spec.ts b/client/src/app/core/core-services/constants.service.spec.ts similarity index 100% rename from client/src/app/core/ui-services/constants.service.spec.ts rename to client/src/app/core/core-services/constants.service.spec.ts diff --git a/client/src/app/core/ui-services/constants.service.ts b/client/src/app/core/core-services/constants.service.ts similarity index 94% rename from client/src/app/core/ui-services/constants.service.ts rename to client/src/app/core/core-services/constants.service.ts index e4a0cc61f..3d4c507ad 100644 --- a/client/src/app/core/ui-services/constants.service.ts +++ b/client/src/app/core/core-services/constants.service.ts @@ -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); * }); * ``` diff --git a/client/src/app/core/core-services/data-store-upgrade.service.spec.ts b/client/src/app/core/core-services/data-store-upgrade.service.spec.ts new file mode 100644 index 000000000..091233bfb --- /dev/null +++ b/client/src/app/core/core-services/data-store-upgrade.service.spec.ts @@ -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(); + })); +}); diff --git a/client/src/app/core/core-services/data-store-upgrade.service.ts b/client/src/app/core/core-services/data-store-upgrade.service.ts new file mode 100644 index 000000000..743b76971 --- /dev/null +++ b/client/src/app/core/core-services/data-store-upgrade.service.ts @@ -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(MIGRATIONVERSION).subscribe(async version => { + const currentVersion = await storageService.get(MIGRATIONVERSION); + await storageService.set(MIGRATIONVERSION, version); + if (currentVersion && currentVersion !== version) { + autoupdateService.doFullUpdate(); + } + }); + } +} diff --git a/client/src/app/core/core-services/websocket.service.ts b/client/src/app/core/core-services/websocket.service.ts index 85a31d43e..03cbfd625 100644 --- a/client/src/app/core/core-services/websocket.service.ts +++ b/client/src/app/core/core-services/websocket.service.ts @@ -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 } = {}; + 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(type: string, content: T, id?: string): void { + public send( + 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(type: string, content: T, id?: string): Promise { + return new Promise((resolve, reject) => { + this.send( + type, + content, + val => { + resolve(val); + return true; + }, + reject, + id + ); + }); } } diff --git a/client/src/app/core/repositories/config/config-repository.service.ts b/client/src/app/core/repositories/config/config-repository.service.ts index f92c3a96a..f0a495f06 100644 --- a/client/src/app/core/repositories/config/config-repository.service.ts +++ b/client/src/app/core/repositories/config/config-repository.service.ts @@ -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 ) { 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(); diff --git a/client/src/app/core/repositories/users/group-repository.service.ts b/client/src/app/core/repositories/users/group-repository.service.ts index ee02196ad..a40a7c621 100644 --- a/client/src/app/core/repositories/users/group-repository.service.ts +++ b/client/src/app/core/repositories/users/group-repository.service.ts @@ -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'; diff --git a/client/src/app/core/ui-services/config.service.ts b/client/src/app/core/ui-services/config.service.ts index c3cb275e3..c3da2a131 100644 --- a/client/src/app/core/ui-services/config.service.ts +++ b/client/src/app/core/ui-services/config.service.ts @@ -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'; diff --git a/client/src/app/shared/models/agenda/item.ts b/client/src/app/shared/models/agenda/item.ts index 5c2e2b647..5f1f8a8e4 100644 --- a/client/src/app/shared/models/agenda/item.ts +++ b/client/src/app/shared/models/agenda/item.ts @@ -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: '' }, diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts index 00d654c2b..b3503d034 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts @@ -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'; diff --git a/client/src/app/site/assignments/services/assignment-filter.service.ts b/client/src/app/site/assignments/services/assignment-filter.service.ts index 78e0d212d..1051ce19a 100644 --- a/client/src/app/site/assignments/services/assignment-filter.service.ts +++ b/client/src/app/site/assignments/services/assignment-filter.service.ts @@ -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' diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts index 856bc5101..d62c0114f 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-poll/motion-poll.component.ts @@ -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('OpenSlidesConfigVariables').subscribe(constants => { + this.constants.get('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'); diff --git a/client/src/app/site/motions/services/local-permissions.service.ts b/client/src/app/site/motions/services/local-permissions.service.ts index b41ca247b..341ec5620 100644 --- a/client/src/app/site/motions/services/local-permissions.service.ts +++ b/client/src/app/site/motions/services/local-permissions.service.ts @@ -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('motions_amendments_enabled') .subscribe(enabled => (this.amendmentEnabled = enabled)); this.constants - .get('OpenSlidesSettings') + .get('Settings') .subscribe(settings => (this.amendmentOfAmendment = settings.MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS)); } diff --git a/openslides/core/apps.py b/openslides/core/apps.py index f1d51b1b1..b8a38f592 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -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