Merge pull request #3837 from FinnStutzenstein/constants

Angular constants via WebSocket
This commit is contained in:
Finn Stutzenstein 2018-09-04 09:40:12 +02:00 committed by GitHub
commit 1b02b7c692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 260 additions and 59 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,97 @@
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
* ```ts
* 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>('constants').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('constants', {});
}
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('constants', {});
}
}
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

@ -72,6 +72,13 @@ export class LoginComponent extends BaseComponent implements OnInit, OnDestroy {
*/ */
public inProcess = false; public inProcess = false;
/**
* The provacy policy send by the server.
*
* TODO: give an option to show it during login.
*/
public privacyPolicy: string;
/** /**
* Constructor for the login component * Constructor for the login component
* *
@ -106,9 +113,12 @@ export class LoginComponent extends BaseComponent implements OnInit, OnDestroy {
super.setTitle('Login'); super.setTitle('Login');
this.http.get<any>(environment.urlPrefix + '/users/login/', {}).subscribe(response => { this.http.get<any>(environment.urlPrefix + '/users/login/', {}).subscribe(response => {
this.installationNotice = this.matSnackBar.open(response.info_text, this.translate.instant('OK'), { if (response.info_text) {
duration: 5000 this.installationNotice = this.matSnackBar.open(response.info_text, this.translate.instant('OK'), {
}); duration: 5000
});
}
this.privacyPolicy = response.privacy_policy;
}); });
} }

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,9 +1,10 @@
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
from django.core.exceptions import ImproperlyConfigured
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from ..utils.projector import register_projector_elements from ..utils.projector import register_projector_elements
@ -37,6 +38,14 @@ class CoreAppConfig(AppConfig):
ProjectorViewSet, ProjectorViewSet,
TagViewSet, TagViewSet,
) )
from ..utils.constants import set_constants, get_constants_from_apps
# Set constants
try:
set_constants(get_constants_from_apps())
except ImproperlyConfigured:
# Database is not loaded. This happens in tests.
pass
# Define config variables and projector elements. # Define config variables and projector elements.
config.update_config_variables(get_config_variables()) config.update_config_variables(get_config_variables())
@ -74,6 +83,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 +99,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 +119,9 @@ 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 return constants
# not logged in (so does not have the config values yet).
privacy_policy = {
'name': 'PrivacyPolicy',
'value': config['general_event_privacy_policy']}
return [client_settings, config_variables, privacy_policy]
def call_save_default_values(**kwargs): def call_save_default_values(**kwargs):

View File

@ -15,6 +15,7 @@ from .. import __license__ as license, __url__ as url, __version__ as version
from ..utils import views as utils_views from ..utils import views as utils_views
from ..utils.auth import anonymous_is_enabled, has_perm from ..utils.auth import anonymous_is_enabled, has_perm
from ..utils.autoupdate import inform_changed_data, inform_deleted_data from ..utils.autoupdate import inform_changed_data, inform_deleted_data
from ..utils.constants import get_constants
from ..utils.plugins import ( from ..utils.plugins import (
get_plugin_description, get_plugin_description,
get_plugin_license, get_plugin_license,
@ -133,18 +134,9 @@ class WebclientJavaScriptView(utils_views.View):
# angular constants # angular constants
angular_constants = '' angular_constants = ''
for app in apps.get_app_configs(): for key, value in get_constants().items():
try: value = json.dumps(value)
# Each app can deliver values to angular when implementing this method. angular_constants += ".constant('{}', {})".format(key, value)
# 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
for constant in get_angular_constants():
value = json.dumps(constant['value'])
name = constant['name']
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

@ -453,6 +453,8 @@ class UserLoginView(APIView):
password='<strong>admin</strong>') password='<strong>admin</strong>')
else: else:
context['info_text'] = '' context['info_text'] = ''
# Add the privacy policy, so the client can display it even, it is not logged in.
context['privacy_policy'] = config['general_event_privacy_policy']
else: else:
# self.request.method == 'POST' # self.request.method == 'POST'
context['user_id'] = self.user.pk context['user_id'] = self.user.pk

View File

@ -0,0 +1,40 @@
from typing import Any, Dict
from django.apps import apps
def get_constants_from_apps() -> Dict[str, Any]:
out: 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
out.update(get_angular_constants())
return out
constants = None
def get_constants() -> Dict[str, Any]:
"""
Returns the constants.
This method only returns a static dict, so it is fast and can be used in a
async context.
"""
if constants is None:
raise RuntimeError("Constants are not set.")
return constants
def set_constants(value: Dict[str, Any]) -> None:
"""
Sets the constants variable.
"""
global constants
constants = value

View File

@ -15,6 +15,7 @@ from .collection import (
format_for_autoupdate, format_for_autoupdate,
from_channel_message, from_channel_message,
) )
from .constants import get_constants
class ProtocollAsyncJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): class ProtocollAsyncJsonWebsocketConsumer(AsyncJsonWebsocketConsumer):
@ -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|constants", # The server can sent other types
}, },
"content": { "content": {
"description": "The content of the package.", "description": "The content of the package.",
@ -137,6 +138,10 @@ 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 == 'constants':
# Return all constants to the client.
await self.send_json(type='constants', content=get_constants(), in_response=id)
async def send_notify(self, event: Dict[str, Any]) -> None: async def send_notify(self, event: Dict[str, Any]) -> None:
""" """
Send a notify message to the user. Send a notify message to the user.

View File

@ -1,4 +1,6 @@
import pytest
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from pytest_django.django_compat import is_django_unittest
from pytest_django.plugin import validate_django_db from pytest_django.plugin import validate_django_db
@ -38,3 +40,20 @@ def pytest_collection_modifyitems(items):
return 0 return 0
items.sort(key=weight_test_case) items.sort(key=weight_test_case)
@pytest.fixture(autouse=True)
def constants(request):
"""
Resets the constants on every test.
Uses fake constants, if the db is not in use.
"""
from openslides.utils.constants import set_constants, get_constants_from_apps
if 'django_db' in request.node.keywords or is_django_unittest(request):
# When the db is created, use the original constants
set_constants(get_constants_from_apps())
else:
# Else: Use fake constants
set_constants({'constant1': 'value1', 'constant2': 'value2'})

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

View File

@ -241,3 +241,18 @@ async def test_send_unknown_type(communicator):
response = await communicator.receive_json_from() response = await communicator.receive_json_from()
assert response['type'] == 'error' assert response['type'] == 'error'
assert response['in_response'] == 'test_id' assert response['in_response'] == 'test_id'
@pytest.mark.asyncio
async def test_request_constants(communicator, settings):
await set_config('general_system_enable_anonymous', True)
await communicator.connect()
# Await the startup data
await communicator.receive_json_from()
await communicator.send_json_to({'type': 'constants', 'content': '', 'id': 'test_id'})
response = await communicator.receive_json_from()
assert response['type'] == 'constants'
# See conftest.py for the content of 'content'
assert response['content'] == {'constant1': 'value1', 'constant2': 'value2'}