Angular constants via WebSocket

- new format for constants on the server
- adaptions for the old client
This commit is contained in:
FinnStutzenstein 2018-08-29 15:49:44 +02:00
parent b6f6d6f720
commit 8adaa6118a
10 changed files with 169 additions and 45 deletions

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { ConstantsService } from './constants.service';
describe('ConstantsService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ConstantsService]
});
});
it('should be created', inject([ConstantsService], (service: ConstantsService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,95 @@
import { Injectable } from '@angular/core';
import { OpenSlidesComponent } from 'app/openslides.component';
import { WebsocketService } from './websocket.service';
import { Observable, of, Subject } from 'rxjs';
/**
* constants have a key associated with the data.
*/
interface Constants {
[key: string]: any;
}
/**
* Get constants from the server.
*
* @example
* this.constantsService.get('OpenSlidesSettings').subscribe(constant => {
* console.log(constant);
* });
*/
@Injectable({
providedIn: 'root'
})
export class ConstantsService extends OpenSlidesComponent {
/**
* The constants
*/
private constants: Constants;
/**
* Flag, if the websocket connection is open.
*/
private websocketOpen = false;
/**
* Flag, if constants are requested, but the server hasn't send them yet.
*/
private pending = false;
/**
* Pending requests will be notified by these subjects, one per key.
*/
private pendingSubject: { [key: string]: Subject<any> } = {};
/**
* @param websocketService
*/
public constructor(private websocketService: WebsocketService) {
super();
// The hook for recieving constants.
websocketService.getOberservable<Constants>('constantsResponse').subscribe(constants => {
this.constants = constants;
if (this.pending) {
// send constants to subscribers that await constants.
this.pending = false;
Object.keys(this.pendingSubject).forEach(key => {
this.pendingSubject[key].next(this.constants[key]);
});
}
});
// We can request constants, if the websocket connection opens.
websocketService.connectEvent.subscribe(() => {
if (!this.websocketOpen && this.pending) {
this.websocketService.send('constantsRequest', {});
}
this.websocketOpen = true;
});
}
/**
* Get the constant named by key.
* @param key The constant to get.
*/
public get(key: string): Observable<any> {
if (this.constants) {
return of(this.constants[key]);
} else {
// we have to request constants.
if (!this.pending) {
this.pending = true;
// if the connection is open, we directly can send the request.
if (this.websocketOpen) {
this.websocketService.send('constantsRequest', {});
}
}
if (!this.pendingSubject[key]) {
this.pendingSubject[key] = new Subject<any>();
}
return this.pendingSubject[key].asObservable();
}
}
}

View File

@ -38,7 +38,7 @@ export class OpenSlidesService extends OpenSlidesComponent {
// Handler that gets called, if the websocket connection reconnects after a disconnection. // Handler that gets called, if the websocket connection reconnects after a disconnection.
// There might have changed something on the server, so we check the operator, if he changed. // There might have changed something on the server, so we check the operator, if he changed.
websocketService.getReconnectObservable().subscribe(() => { websocketService.reconnectEvent.subscribe(() => {
this.checkOperator(); this.checkOperator();
}); });
} }

View File

@ -1,4 +1,4 @@
import { Injectable, NgZone } from '@angular/core'; import { Injectable, NgZone, EventEmitter } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material'; import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
@ -36,7 +36,26 @@ export class WebsocketService {
/** /**
* Subjects that will be called, if a reconnect was successful. * Subjects that will be called, if a reconnect was successful.
*/ */
private reconnectSubject: Subject<void>; private _reconnectEvent: EventEmitter<void> = new EventEmitter<void>();
/**
* Getter for the reconnect event.
*/
public get reconnectEvent(): EventEmitter<void> {
return this._reconnectEvent;
}
/**
* Listeners will be nofitied, if the wesocket connection is establiched.
*/
private _connectEvent: EventEmitter<void> = new EventEmitter<void>();
/**
* Getter for the connect event.
*/
public get connectEvent(): EventEmitter<void> {
return this._connectEvent;
}
/** /**
* The websocket. * The websocket.
@ -57,9 +76,7 @@ export class WebsocketService {
private matSnackBar: MatSnackBar, private matSnackBar: MatSnackBar,
private zone: NgZone, private zone: NgZone,
public translate: TranslateService public translate: TranslateService
) { ) {}
this.reconnectSubject = new Subject<void>();
}
/** /**
* Creates a new WebSocket connection and handles incomming events. * Creates a new WebSocket connection and handles incomming events.
@ -91,8 +108,9 @@ export class WebsocketService {
this.connectionErrorNotice.dismiss(); this.connectionErrorNotice.dismiss();
this.connectionErrorNotice = null; this.connectionErrorNotice = null;
} }
this.reconnectSubject.next(); this._reconnectEvent.emit();
} }
this._connectEvent.emit();
}); });
}; };
@ -156,13 +174,6 @@ export class WebsocketService {
return this.subjects[type].asObservable(); return this.subjects[type].asObservable();
} }
/**
* get the reconnect observable. It will be published, if a reconnect was sucessful.
*/
public getReconnectObservable(): Observable<void> {
return this.reconnectSubject.asObservable();
}
/** /**
* Sends a message to the server with the content and the given type. * Sends a message to the server with the content and the given type.
* *

View File

@ -47,14 +47,11 @@ class AssignmentsAppConfig(AppConfig):
def get_angular_constants(self): def get_angular_constants(self):
assignment = self.get_model('Assignment') assignment = self.get_model('Assignment')
InnerItem = TypedDict('InnerItem', {'value': int, 'display_name': str}) Item = TypedDict('Item', {'value': int, 'display_name': str})
Item = TypedDict('Item', {'name': str, 'value': List[InnerItem]}) phases: List[Item] = []
data: Item = {
'name': 'AssignmentPhases',
'value': []}
for phase in assignment.PHASES: for phase in assignment.PHASES:
data['value'].append({ phases.append({
'value': phase[0], 'value': phase[0],
'display_name': phase[1], 'display_name': phase[1],
}) })
return [data] return {'AssignmentPhases': phases}

View File

@ -1,6 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from operator import attrgetter from operator import attrgetter
from typing import Any, List from typing import Any, Dict, List
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
@ -74,6 +74,8 @@ class CoreAppConfig(AppConfig):
def get_angular_constants(self): def get_angular_constants(self):
from .config import config from .config import config
constants: Dict[str, Any] = {}
# Client settings # Client settings
client_settings_keys = [ client_settings_keys = [
'MOTION_IDENTIFIER_MIN_DIGITS', 'MOTION_IDENTIFIER_MIN_DIGITS',
@ -88,9 +90,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
client_settings = { constants['OpenSlidesSettings'] = client_settings_dict
'name': 'OpenSlidesSettings',
'value': client_settings_dict}
# Config variables # Config variables
config_groups: List[Any] = [] config_groups: List[Any] = []
@ -110,17 +110,13 @@ class CoreAppConfig(AppConfig):
items=[])) items=[]))
# 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)
config_variables = { constants['OpenSlidesConfigVariables'] = config_groups
'name': 'OpenSlidesConfigVariables',
'value': config_groups}
# Send the privacy policy to the client. A user should view them, even he is # Send the privacy policy to the client. A user should view them, even he is
# not logged in (so does not have the config values yet). # not logged in (so does not have the config values yet).
privacy_policy = { constants['PrivacyPolicy'] = config['general_event_privacy_policy']
'name': 'PrivacyPolicy',
'value': config['general_event_privacy_policy']}
return [client_settings, config_variables, privacy_policy] return constants
def call_save_default_values(**kwargs): def call_save_default_values(**kwargs):

View File

@ -141,10 +141,9 @@ class WebclientJavaScriptView(utils_views.View):
except AttributeError: except AttributeError:
# The app doesn't have this method. Continue to next app. # The app doesn't have this method. Continue to next app.
continue continue
for constant in get_angular_constants(): for key, value in get_angular_constants().items():
value = json.dumps(constant['value']) value = json.dumps(value)
name = constant['name'] angular_constants += ".constant('{}', {})".format(key, value)
angular_constants += ".constant('{}', {})".format(name, value)
# Use JavaScript loadScript function from # Use JavaScript loadScript function from
# http://balpha.de/2011/10/jquery-script-insertion-and-its-consequences-for-debugging/ # http://balpha.de/2011/10/jquery-script-insertion-and-its-consequences-for-debugging/

View File

@ -58,7 +58,4 @@ class UsersAppConfig(AppConfig):
permissions.append({ permissions.append({
'display_name': permission.name, 'display_name': permission.name,
'value': '.'.join((permission.content_type.app_label, permission.codename,))}) 'value': '.'.join((permission.content_type.app_label, permission.codename,))})
permission_settings = { return {'permissions': permissions}
'name': 'permissions',
'value': permissions}
return [permission_settings]

View File

@ -4,6 +4,7 @@ import jsonschema
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.apps import apps
from ..core.config import config from ..core.config import config
from ..core.models import Projector from ..core.models import Projector
@ -30,7 +31,7 @@ class ProtocollAsyncJsonWebsocketConsumer(AsyncJsonWebsocketConsumer):
"type": { "type": {
"description": "Defines what kind of packages is packed.", "description": "Defines what kind of packages is packed.",
"type": "string", "type": "string",
"pattern": "notify", # The server can sent other types "pattern": "notify|constantsRequest", # The server can sent other types
}, },
"content": { "content": {
"description": "The content of the package.", "description": "The content of the package.",
@ -136,6 +137,19 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
) )
else: else:
await self.send_json(type='error', content='Invalid notify message', in_response=id) await self.send_json(type='error', content='Invalid notify message', in_response=id)
elif type == 'constantsRequest':
angular_constants: Dict[str, Any] = {}
for app in apps.get_app_configs():
try:
# Each app can deliver values to angular when implementing this method.
# It should return a list with dicts containing the 'name' and 'value'.
get_angular_constants = app.get_angular_constants
except AttributeError:
# The app doesn't have this method. Continue to next app.
continue
constants = await database_sync_to_async(get_angular_constants)()
angular_constants.update(constants)
await self.send_json(type='constantsResponse', content=angular_constants, in_response=id)
async def send_notify(self, event: Dict[str, Any]) -> None: async def send_notify(self, event: Dict[str, Any]) -> None:
""" """

View File

@ -94,17 +94,17 @@ class WebclientJavaScriptView(TestCase):
response = self.client.get(reverse('core_webclient_javascript', args=['site'])) response = self.client.get(reverse('core_webclient_javascript', args=['site']))
content = response.content.decode() content = response.content.decode()
constants = self.get_angular_constants_from_apps() constants = self.get_angular_constants_from_apps()
for constant in constants: for key, constant in constants.items():
self.assertTrue(json.dumps(constant['value']) in content) self.assertTrue(json.dumps(constant) in content)
def get_angular_constants_from_apps(self): def get_angular_constants_from_apps(self):
constants = [] constants = {}
for app in apps.get_app_configs(): for app in apps.get_app_configs():
try: try:
get_angular_constants = app.get_angular_constants get_angular_constants = app.get_angular_constants
except AttributeError: except AttributeError:
continue continue
constants.extend(get_angular_constants()) constants.update(get_angular_constants())
return constants return constants