commit
9a88717dab
13
CHANGELOG
13
CHANGELOG
@ -4,12 +4,17 @@
|
|||||||
|
|
||||||
https://openslides.org/
|
https://openslides.org/
|
||||||
|
|
||||||
Version 2.0.1 (unreleased)
|
Version 2.1 (unreleased)
|
||||||
==========================
|
========================
|
||||||
[https://github.com/OpenSlides/OpenSlides/milestones/2.0.1]
|
[https://github.com/OpenSlides/OpenSlides/milestones/2.1]
|
||||||
|
|
||||||
|
Core:
|
||||||
|
- Used Django Channels instead of Tornado.
|
||||||
|
- Added support for big assemblies with lots of users.
|
||||||
|
|
||||||
Other:
|
Other:
|
||||||
- Remove config cache to support multiple threads or processes.
|
- Removed config cache to support multiple threads or processes.
|
||||||
|
- Fixed bug, that the last change of a config value was not send via autoupdate.
|
||||||
|
|
||||||
|
|
||||||
Version 2.0 (2016-04-18)
|
Version 2.0 (2016-04-18)
|
||||||
|
50
README.rst
50
README.rst
@ -79,15 +79,14 @@ To start OpenSlides simply run::
|
|||||||
|
|
||||||
$ openslides
|
$ openslides
|
||||||
|
|
||||||
If you run this command the first time, a new database and the admin
|
If you run this command the first time, a new database and the admin account
|
||||||
account (Username: `admin`, Password: `admin`) will be created. Please
|
(Username: `admin`, Password: `admin`) will be created. Please change the
|
||||||
change the password after first login!
|
password after first login!
|
||||||
|
|
||||||
OpenSlides will start using the integrated Tornado webserver. It will also
|
OpenSlides will start a webserver. It will also try to open the webinterface in
|
||||||
try to open the webinterface in your default webbrowser. The server will
|
your default webbrowser. The server will try to listen on the local ip address
|
||||||
try to listen on the local ip address on port 8000. That means that the
|
on port 8000. That means that the server will be available to everyone on your
|
||||||
server will be available to everyone on your local network (at least for
|
local network (at least for commonly used network configurations).
|
||||||
commonly used network configurations).
|
|
||||||
|
|
||||||
If you use a virtual environment (see step b.), do not forget to activate
|
If you use a virtual environment (see step b.), do not forget to activate
|
||||||
the environment before restart after you have closed the terminal::
|
the environment before restart after you have closed the terminal::
|
||||||
@ -122,6 +121,32 @@ If you want to contribute to OpenSlides, have a look at `OpenSlides website
|
|||||||
<https://github.com/OpenSlides/OpenSlides/blob/master/DEVELOPMENT.rst>`_.
|
<https://github.com/OpenSlides/OpenSlides/blob/master/DEVELOPMENT.rst>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Installation for big assemblies
|
||||||
|
===============================
|
||||||
|
|
||||||
|
The installation steps described above install OpenSlides in a way, that does
|
||||||
|
not support hundreds of concurrent clients. To install OpenSlides in for big
|
||||||
|
assemblies, some config variables have to be changed in the OpenSlides settings
|
||||||
|
usualy called settings.py.
|
||||||
|
|
||||||
|
The configuration values, that have to be altered are:
|
||||||
|
|
||||||
|
* CACHES
|
||||||
|
* CHANNEL_LAYERS
|
||||||
|
* DATABASES
|
||||||
|
|
||||||
|
Please see:
|
||||||
|
* http://channels.readthedocs.io/en/latest/deploying.html
|
||||||
|
* https://docs.djangoproject.com/en/1.9/topics/cache/
|
||||||
|
* https://github.com/sebleier/django-redis-cache
|
||||||
|
* https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||||
|
|
||||||
|
You should use a webserver like Apache HTTP Server or nginx to serve the static
|
||||||
|
and media files and as proxy server in front of OpenSlides server. You also
|
||||||
|
should use a database like PostgreSQL and Redis as channels backend and cache
|
||||||
|
backend.
|
||||||
|
|
||||||
|
|
||||||
Used software
|
Used software
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@ -140,6 +165,8 @@ OpenSlides uses the following projects or parts of them:
|
|||||||
|
|
||||||
* `html5lib <https://github.com/html5lib/html5lib-python>`_, License: MIT
|
* `html5lib <https://github.com/html5lib/html5lib-python>`_, License: MIT
|
||||||
|
|
||||||
|
* `Django Channels <https://github.com/andrewgodwin/channels/>`_, License: MIT
|
||||||
|
|
||||||
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
|
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|
||||||
@ -157,12 +184,6 @@ OpenSlides uses the following projects or parts of them:
|
|||||||
|
|
||||||
* `Six <http://pythonhosted.org/six/>`_, License: MIT
|
* `Six <http://pythonhosted.org/six/>`_, License: MIT
|
||||||
|
|
||||||
* `sockjs-tornado <https://github.com/mrjoes/sockjs-tornado>`_,
|
|
||||||
License: MIT
|
|
||||||
|
|
||||||
* `Tornado <http://www.tornadoweb.org/en/stable/>`_, License: Apache
|
|
||||||
License v2.0
|
|
||||||
|
|
||||||
* `Whoosh <https://bitbucket.org/mchaput/whoosh/wiki/Home>`_, License: BSD
|
* `Whoosh <https://bitbucket.org/mchaput/whoosh/wiki/Home>`_, License: BSD
|
||||||
|
|
||||||
* Several JavaScript packages (see ``bower.json``)
|
* Several JavaScript packages (see ``bower.json``)
|
||||||
@ -200,7 +221,6 @@ OpenSlides uses the following projects or parts of them:
|
|||||||
* `open-sans-fontface <https://github.com/FontFaceKit/open-sans>`_, License: Apache License version 2.0
|
* `open-sans-fontface <https://github.com/FontFaceKit/open-sans>`_, License: Apache License version 2.0
|
||||||
* `pdfjs-dist <http://mozilla.github.io/pdf.js/>`_, License: Apache-2.0
|
* `pdfjs-dist <http://mozilla.github.io/pdf.js/>`_, License: Apache-2.0
|
||||||
* `roboto-condensed <https://github.com/davidcunningham/roboto-condensed>`_, License: Apache 2.0
|
* `roboto-condensed <https://github.com/davidcunningham/roboto-condensed>`_, License: Apache 2.0
|
||||||
* `sockjs <https://github.com/sockjs/sockjs-client>`_, License: MIT
|
|
||||||
* `tinymce <http://www.tinymce.com>`_, License: LGPL-2.1
|
* `tinymce <http://www.tinymce.com>`_, License: LGPL-2.1
|
||||||
* `tinymce-i18n <https://github.com/OpenSlides/tinymce-i18n>`_, License: LGPL-2.1
|
* `tinymce-i18n <https://github.com/OpenSlides/tinymce-i18n>`_, License: LGPL-2.1
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
"ngBootbox": "~0.1.3",
|
"ngBootbox": "~0.1.3",
|
||||||
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
|
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
|
||||||
"roboto-condensed": "~0.3.0",
|
"roboto-condensed": "~0.3.0",
|
||||||
"sockjs": "~0.3.4",
|
|
||||||
"tinymce-i18n": "OpenSlides/tinymce-i18n#a186ad61e0aa30fdf657e88f405f966d790f0805"
|
"tinymce-i18n": "OpenSlides/tinymce-i18n#a186ad61e0aa30fdf657e88f405f966d790f0805"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
@ -185,7 +185,13 @@ def start(args):
|
|||||||
|
|
||||||
# Start the webserver
|
# Start the webserver
|
||||||
# Tell django not to reload. OpenSlides uses the reload method from tornado
|
# Tell django not to reload. OpenSlides uses the reload method from tornado
|
||||||
execute_from_command_line(['manage.py', 'runserver', '%s:%s' % (args.host, args.port), '--noreload'])
|
# Use insecure to serve static files, even when DEBUG is False.
|
||||||
|
execute_from_command_line([
|
||||||
|
'manage.py',
|
||||||
|
'runserver',
|
||||||
|
'{}:{}'.format(args.host, args.port),
|
||||||
|
'--noreload',
|
||||||
|
'--insecure'])
|
||||||
|
|
||||||
|
|
||||||
def createsettings(args):
|
def createsettings(args):
|
||||||
|
@ -12,7 +12,7 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
if created and hasattr(instance, 'get_agenda_title'):
|
if created and hasattr(instance, 'get_agenda_title'):
|
||||||
Item.objects.create(content_object=instance)
|
Item.objects.create(content_object=instance)
|
||||||
inform_changed_data(False, instance)
|
inform_changed_data(instance)
|
||||||
|
|
||||||
|
|
||||||
def listen_to_related_object_post_delete(sender, instance, **kwargs):
|
def listen_to_related_object_post_delete(sender, instance, **kwargs):
|
||||||
|
9
openslides/asgi.py
Normal file
9
openslides/asgi.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from channels.asgi import get_channel_layer
|
||||||
|
|
||||||
|
from .utils.main import setup_django_settings_module
|
||||||
|
|
||||||
|
# Loads the openslides setting. You can use your own settings by setting the
|
||||||
|
# environment variable DJANGO_SETTINGS_MODULE
|
||||||
|
setup_django_settings_module()
|
||||||
|
|
||||||
|
channel_layer = get_channel_layer()
|
@ -1,82 +0,0 @@
|
|||||||
import errno
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management.commands.runserver import Command as _Command
|
|
||||||
from django.utils import six
|
|
||||||
from django.utils.encoding import force_text, get_system_encoding
|
|
||||||
|
|
||||||
from openslides.utils.autoupdate import run_tornado
|
|
||||||
|
|
||||||
|
|
||||||
class Command(_Command):
|
|
||||||
"""
|
|
||||||
Runserver command from django core, but starts the tornado webserver.
|
|
||||||
|
|
||||||
Only the line to run tornado has changed from the django default
|
|
||||||
implementation.
|
|
||||||
|
|
||||||
The Code is from django 1.9
|
|
||||||
"""
|
|
||||||
help = 'Starts the Tornado webserver.'
|
|
||||||
|
|
||||||
# TODO: do not start tornado when the settings says so
|
|
||||||
|
|
||||||
def inner_run(self, *args, **options):
|
|
||||||
# If an exception was silenced in ManagementUtility.execute in order
|
|
||||||
# to be raised in the child process, raise it now.
|
|
||||||
# OPENSLIDES: We do not use the django autoreload command
|
|
||||||
# autoreload.raise_last_exception()
|
|
||||||
|
|
||||||
# OPENSLIDES: This line is not needed by tornado
|
|
||||||
# threading = options.get('use_threading')
|
|
||||||
shutdown_message = options.get('shutdown_message', '')
|
|
||||||
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
|
|
||||||
|
|
||||||
self.stdout.write("Performing system checks...\n\n")
|
|
||||||
self.check(display_num_errors=True)
|
|
||||||
self.check_migrations()
|
|
||||||
now = datetime.now().strftime('%B %d, %Y - %X')
|
|
||||||
if six.PY2:
|
|
||||||
now = now.decode(get_system_encoding())
|
|
||||||
self.stdout.write(now)
|
|
||||||
self.stdout.write((
|
|
||||||
"Django version %(version)s, using settings %(settings)r\n"
|
|
||||||
"Starting development server at http://%(addr)s:%(port)s/\n"
|
|
||||||
"Quit the server with %(quit_command)s.\n"
|
|
||||||
) % {
|
|
||||||
"version": self.get_version(),
|
|
||||||
"settings": settings.SETTINGS_MODULE,
|
|
||||||
"addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
|
|
||||||
"port": self.port,
|
|
||||||
"quit_command": quit_command,
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
|
||||||
handler = self.get_handler(*args, **options)
|
|
||||||
run_tornado(
|
|
||||||
self.addr,
|
|
||||||
int(self.port),
|
|
||||||
handler,
|
|
||||||
ipv6=self.use_ipv6)
|
|
||||||
except socket.error as e:
|
|
||||||
# Use helpful error messages instead of ugly tracebacks.
|
|
||||||
ERRORS = {
|
|
||||||
errno.EACCES: "You don't have permission to access that port.",
|
|
||||||
errno.EADDRINUSE: "That port is already in use.",
|
|
||||||
errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
error_text = ERRORS[e.errno]
|
|
||||||
except KeyError:
|
|
||||||
error_text = force_text(e)
|
|
||||||
self.stderr.write("Error: %s" % error_text)
|
|
||||||
# Need to use an OS exit because sys.exit doesn't work in a thread
|
|
||||||
os._exit(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if shutdown_message:
|
|
||||||
self.stdout.write(shutdown_message)
|
|
||||||
sys.exit(0)
|
|
35
openslides/core/migrations/0002_session.py
Normal file
35
openslides/core/migrations/0002_session.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('sessions', '0001_initial'),
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Session',
|
||||||
|
fields=[
|
||||||
|
('session_ptr',
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to='sessions.Session')),
|
||||||
|
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'sessions',
|
||||||
|
'abstract': False,
|
||||||
|
'verbose_name': 'session',
|
||||||
|
},
|
||||||
|
bases=('sessions.session',),
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.sessions.models import Session as DjangoSession
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
|
||||||
@ -252,3 +253,19 @@ class ChatMessage(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Message {}'.format(self.timestamp)
|
return 'Message {}'.format(self.timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
class Session(DjangoSession):
|
||||||
|
"""
|
||||||
|
Model like the Django db session, which saves the user as ForeignKey instead
|
||||||
|
of an encoded value.
|
||||||
|
"""
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_session_store_class(cls):
|
||||||
|
from .session_backend import SessionStore
|
||||||
|
return SessionStore
|
||||||
|
23
openslides/core/session_backend.py
Normal file
23
openslides/core/session_backend.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.contrib.sessions.backends.db import \
|
||||||
|
SessionStore as DjangoSessionStore
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStore(DjangoSessionStore):
|
||||||
|
"""
|
||||||
|
Like the Django db Session store, but saves the user into the db field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_model_class(cls):
|
||||||
|
# Avoids a circular import
|
||||||
|
from .models import Session
|
||||||
|
return Session
|
||||||
|
|
||||||
|
def create_model_instance(self, data):
|
||||||
|
"""
|
||||||
|
Set the user from data to the db field. Set to None, if its a session
|
||||||
|
from an anonymous user.
|
||||||
|
"""
|
||||||
|
model = super().create_model_instance(data)
|
||||||
|
model.user_id = data['_auth_user_id'] if '_auth_user_id' in data else None
|
||||||
|
return model
|
@ -41,7 +41,7 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
function (DS, $rootScope) {
|
function (DS, $rootScope) {
|
||||||
var socket = null;
|
var socket = null;
|
||||||
var recInterval = null;
|
var recInterval = null;
|
||||||
$rootScope.connected = true;
|
$rootScope.connected = false;
|
||||||
|
|
||||||
var Autoupdate = {
|
var Autoupdate = {
|
||||||
messageReceivers: [],
|
messageReceivers: [],
|
||||||
@ -55,7 +55,7 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
var newConnect = function () {
|
var newConnect = function () {
|
||||||
socket = new SockJS(location.origin + "/sockjs");
|
socket = new WebSocket('ws://' + location.host + '/ws/');
|
||||||
clearInterval(recInterval);
|
clearInterval(recInterval);
|
||||||
socket.onopen = function () {
|
socket.onopen = function () {
|
||||||
$rootScope.connected = true;
|
$rootScope.connected = true;
|
||||||
@ -168,22 +168,20 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
'autoupdate',
|
'autoupdate',
|
||||||
'dsEject',
|
'dsEject',
|
||||||
function (DS, autoupdate, dsEject) {
|
function (DS, autoupdate, dsEject) {
|
||||||
autoupdate.onMessage(function(data) {
|
autoupdate.onMessage(function(json) {
|
||||||
// TODO: when MODEL.find() is called after this
|
// TODO: when MODEL.find() is called after this
|
||||||
// a new request is fired. This could be a bug in DS
|
// a new request is fired. This could be a bug in DS
|
||||||
|
|
||||||
// TODO: Do not send the status code to the client, but make the decission
|
var data = JSON.parse(json);
|
||||||
// on the server side. It is an implementation detail, that tornado
|
|
||||||
// sends request to wsgi, which should not concern the client.
|
|
||||||
console.log("Received object: " + data.collection + ", " + data.id);
|
console.log("Received object: " + data.collection + ", " + data.id);
|
||||||
var instance = DS.get(data.collection, data.id);
|
var instance = DS.get(data.collection, data.id);
|
||||||
if (data.status_code == 200) {
|
if (data.action == 'changed') {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
// The instance is in the local db
|
// The instance is in the local db
|
||||||
dsEject(data.collection, instance);
|
dsEject(data.collection, instance);
|
||||||
}
|
}
|
||||||
DS.inject(data.collection, data.data);
|
DS.inject(data.collection, data.data);
|
||||||
} else if (data.status_code == 404) {
|
} else if (data.action == 'deleted') {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
// The instance is in the local db
|
// The instance is in the local db
|
||||||
dsEject(data.collection, instance);
|
dsEject(data.collection, instance);
|
||||||
|
@ -8,6 +8,9 @@ AUTH_USER_MODEL = 'users.User'
|
|||||||
|
|
||||||
AUTHENTICATION_BACKENDS = ('openslides.users.auth.CustomizedModelBackend',)
|
AUTHENTICATION_BACKENDS = ('openslides.users.auth.CustomizedModelBackend',)
|
||||||
|
|
||||||
|
# Uses a db session backend, that saves the user_id directly in the db
|
||||||
|
SESSION_ENGINE = 'openslides.core.session_backend'
|
||||||
|
|
||||||
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
||||||
|
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
@ -74,6 +77,7 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.humanize',
|
'django.contrib.humanize',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'channels',
|
||||||
'openslides.poll', # TODO: try to remove this line
|
'openslides.poll', # TODO: try to remove this line
|
||||||
'openslides.agenda',
|
'openslides.agenda',
|
||||||
'openslides.motions',
|
'openslides.motions',
|
||||||
@ -93,26 +97,9 @@ CACHES = {
|
|||||||
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
|
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
# Use Haystack with Whoosh for full text search
|
|
||||||
HAYSTACK_CONNECTIONS = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Haystack updates search index after each save/delete action by apps
|
|
||||||
HAYSTACK_SIGNAL_PROCESSOR = 'openslides.utils.haystack_processor.OpenSlidesProcessor'
|
|
||||||
|
|
||||||
# Adds all automaticly collected plugins
|
# Adds all automaticly collected plugins
|
||||||
INSTALLED_PLUGINS = collect_plugins()
|
INSTALLED_PLUGINS = collect_plugins()
|
||||||
|
|
||||||
# Set this True to use tornado as single wsgi server. Set this False to use
|
|
||||||
# other webserver like Apache or Nginx as wsgi server.
|
|
||||||
USE_TORNADO_AS_WSGI_SERVER = True
|
|
||||||
|
|
||||||
OPENSLIDES_WSGI_NETWORK_LOCATION = ''
|
|
||||||
|
|
||||||
|
|
||||||
TEST_RUNNER = 'openslides.utils.test.OpenSlidesDiscoverRunner'
|
TEST_RUNNER = 'openslides.utils.test.OpenSlidesDiscoverRunner'
|
||||||
|
|
||||||
# Config for the REST Framework
|
# Config for the REST Framework
|
||||||
@ -122,3 +109,11 @@ REST_FRAMEWORK = {
|
|||||||
'openslides.users.auth.RESTFrameworkAnonymousAuthentication',
|
'openslides.users.auth.RESTFrameworkAnonymousAuthentication',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Config for channels
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'asgiref.inmemory.ChannelLayer',
|
||||||
|
'ROUTING': 'openslides.routing.channel_routing',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
9
openslides/routing.py
Normal file
9
openslides/routing.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from channels.routing import route
|
||||||
|
|
||||||
|
from openslides.utils.autoupdate import send_data, ws_add, ws_disconnect
|
||||||
|
|
||||||
|
channel_routing = [
|
||||||
|
route("websocket.connect", ws_add, path='/ws/'),
|
||||||
|
route("websocket.disconnect", ws_disconnect),
|
||||||
|
route("autoupdate.send_data", send_data),
|
||||||
|
]
|
@ -1,211 +1,132 @@
|
|||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import posixpath
|
|
||||||
from importlib import import_module
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
from django.conf import settings
|
from asgiref.inmemory import ChannelLayer
|
||||||
from django.core.wsgi import get_wsgi_application
|
from channels import Channel, Group
|
||||||
from sockjs.tornado import SockJSConnection, SockJSRouter
|
from channels.auth import channel_session_user, channel_session_user_from_http
|
||||||
from tornado.httpserver import HTTPServer
|
from django.apps import apps
|
||||||
from tornado.ioloop import IOLoop
|
from django.utils import timezone
|
||||||
from tornado.options import parse_command_line
|
|
||||||
from tornado.web import (
|
|
||||||
Application,
|
|
||||||
FallbackHandler,
|
|
||||||
HTTPError,
|
|
||||||
StaticFileHandler,
|
|
||||||
)
|
|
||||||
from tornado.wsgi import WSGIContainer
|
|
||||||
|
|
||||||
from ..users.auth import AnonymousUser, get_user
|
from ..users.auth import AnonymousUser
|
||||||
|
from ..users.models import User
|
||||||
from .access_permissions import BaseAccessPermissions
|
from .access_permissions import BaseAccessPermissions
|
||||||
|
|
||||||
RUNNING_HOST = None
|
|
||||||
RUNNING_PORT = None
|
|
||||||
|
|
||||||
|
def get_logged_in_users():
|
||||||
class DjangoStaticFileHandler(StaticFileHandler):
|
|
||||||
"""
|
"""
|
||||||
Handels static data by using the django finders.
|
Helper to get all logged in users.
|
||||||
|
|
||||||
Only needed in the "small" version with tornado as wsgi server.
|
Only works with the OpenSlides session backend.
|
||||||
"""
|
"""
|
||||||
|
return User.objects.exclude(session=None).filter(session__expire_date__gte=timezone.now()).distinct()
|
||||||
def initialize(self):
|
|
||||||
"""Overwrite some attributes."""
|
|
||||||
# NOTE: root is never actually used and default_filename is not
|
|
||||||
# supported (must always be None)
|
|
||||||
self.root = ''
|
|
||||||
self.default_filename = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_absolute_path(cls, root, path):
|
|
||||||
from django.contrib.staticfiles import finders
|
|
||||||
normalized_path = posixpath.normpath(unquote(path)).lstrip('/')
|
|
||||||
absolute_path = finders.find(normalized_path)
|
|
||||||
return absolute_path
|
|
||||||
|
|
||||||
def validate_absolute_path(self, root, absolute_path):
|
|
||||||
# differences from base implementation:
|
|
||||||
# - we ignore self.root since our files do not necessarily have
|
|
||||||
# a shared root prefix
|
|
||||||
# - we do not handle self.default_filename (we do not use it and it
|
|
||||||
# does not make much sense here anyway)
|
|
||||||
if absolute_path is None or not os.path.exists(absolute_path):
|
|
||||||
raise HTTPError(404)
|
|
||||||
if not os.path.isfile(absolute_path):
|
|
||||||
raise HTTPError(403, 'The requested resource is not a file.')
|
|
||||||
return absolute_path
|
|
||||||
|
|
||||||
|
|
||||||
class OpenSlidesSockJSConnection(SockJSConnection):
|
def get_model_from_collection_string(collection_string):
|
||||||
"""
|
"""
|
||||||
SockJS connection for OpenSlides.
|
Returns a model class which belongs to the argument collection_string.
|
||||||
"""
|
"""
|
||||||
waiters = set()
|
def model_generator():
|
||||||
|
|
||||||
def on_open(self, info):
|
|
||||||
self.waiters.add(self)
|
|
||||||
self.connection_info = info
|
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
OpenSlidesSockJSConnection.waiters.remove(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def send_object(cls, json_container):
|
|
||||||
"""
|
"""
|
||||||
Sends an OpenSlides object to all connected clients (waiters).
|
Yields all models of all apps.
|
||||||
"""
|
"""
|
||||||
# Load JSON
|
for app_config in apps.get_app_configs():
|
||||||
container = json.loads(json_container)
|
for model in app_config.get_models():
|
||||||
|
yield model
|
||||||
|
|
||||||
# Search our AccessPermission class.
|
for model in model_generator():
|
||||||
for access_permissions in BaseAccessPermissions.get_all():
|
|
||||||
if access_permissions.get_dispatch_uid() == container.get('dispatch_uid'):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError('Invalid container. A valid dispatch_uid is missing.')
|
|
||||||
|
|
||||||
# Loop over all waiters
|
|
||||||
for waiter in cls.waiters:
|
|
||||||
# Read waiter's former cookies and parse session cookie to get user instance.
|
|
||||||
try:
|
try:
|
||||||
session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME]
|
model_collection_string = model.get_collection_string()
|
||||||
except KeyError:
|
except AttributeError:
|
||||||
# There is no session cookie so use anonymous user here.
|
# Skip models which do not have the method get_collection_string.
|
||||||
user = AnonymousUser()
|
pass
|
||||||
else:
|
else:
|
||||||
# Get session from session store and use it to retrieve the user.
|
if model_collection_string == collection_string:
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
# The model was found.
|
||||||
session = engine.SessionStore(session_cookie.value)
|
|
||||||
fake_request = type('FakeRequest', (), {})()
|
|
||||||
fake_request.session = session
|
|
||||||
user = get_user(fake_request)
|
|
||||||
|
|
||||||
# Two cases: models instance was changed or deleted
|
|
||||||
if container.get('action') == 'changed':
|
|
||||||
data = access_permissions.get_restricted_data(container.get('full_data'), user)
|
|
||||||
if data is None:
|
|
||||||
# There are no data for the user so he can't see the object. Skip him.
|
|
||||||
break
|
break
|
||||||
output = {
|
|
||||||
'status_code': 200, # TODO: Refactor this. Use strings like 'change' or 'delete'.
|
|
||||||
'collection': container['collection_string'],
|
|
||||||
'id': container['rest_pk'],
|
|
||||||
'data': data}
|
|
||||||
elif container.get('action') == 'deleted':
|
|
||||||
output = {
|
|
||||||
'status_code': 404, # TODO: Refactor this. Use strings like 'change' or 'delete'.
|
|
||||||
'collection': container['collection_string'],
|
|
||||||
'id': container['rest_pk']}
|
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid container. A valid action is missing.')
|
# No model was found in all apps.
|
||||||
|
raise ValueError('Invalid message. A valid collection_string is missing.')
|
||||||
# Send output to the waiter (client).
|
return model
|
||||||
waiter.send(output)
|
|
||||||
|
|
||||||
|
|
||||||
def run_tornado(addr, port, *args, **kwargs):
|
# Connected to websocket.connect
|
||||||
|
@channel_session_user_from_http
|
||||||
|
def ws_add(message):
|
||||||
"""
|
"""
|
||||||
Starts the tornado webserver as wsgi server for OpenSlides.
|
Adds the websocket connection to a group specific to the connecting user.
|
||||||
|
|
||||||
It runs in one thread.
|
The group with the name 'user-None' stands for all anonymous users.
|
||||||
"""
|
"""
|
||||||
# Save the port and the addr in a global var
|
Group('user-{}'.format(message.user.id)).add(message.reply_channel)
|
||||||
global RUNNING_HOST, RUNNING_PORT
|
|
||||||
RUNNING_HOST = addr
|
|
||||||
RUNNING_PORT = port
|
|
||||||
|
|
||||||
# Don't try to read the command line args from openslides
|
|
||||||
parse_command_line(args=[])
|
|
||||||
|
|
||||||
# Setup WSGIContainer
|
|
||||||
app = WSGIContainer(get_wsgi_application())
|
|
||||||
|
|
||||||
# Collect urls
|
|
||||||
sock_js_router = SockJSRouter(OpenSlidesSockJSConnection, '/sockjs')
|
|
||||||
other_urls = [
|
|
||||||
(r'%s(.*)' % settings.STATIC_URL, DjangoStaticFileHandler),
|
|
||||||
(r'%s(.*)' % settings.MEDIA_URL, StaticFileHandler, {'path': settings.MEDIA_ROOT}),
|
|
||||||
('.*', FallbackHandler, dict(fallback=app))]
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
debug = settings.DEBUG
|
|
||||||
tornado_app = Application(sock_js_router.urls + other_urls, autoreload=debug, debug=debug)
|
|
||||||
server = HTTPServer(tornado_app)
|
|
||||||
server.listen(port=port, address=addr)
|
|
||||||
IOLoop.instance().start()
|
|
||||||
|
|
||||||
# Reset the global vars
|
|
||||||
RUNNING_HOST = None
|
|
||||||
RUNNING_PORT = None
|
|
||||||
|
|
||||||
|
|
||||||
def inform_changed_data(is_delete, *args):
|
# Connected to websocket.disconnect
|
||||||
|
@channel_session_user
|
||||||
|
def ws_disconnect(message):
|
||||||
|
Group('user-{}'.format(message.user.id)).discard(message.reply_channel)
|
||||||
|
|
||||||
|
|
||||||
|
def send_data(message):
|
||||||
"""
|
"""
|
||||||
Informs all users about changed data.
|
Informs all users about changed data.
|
||||||
|
|
||||||
The first argument is whether the object or the objects are deleted.
|
The argument message has to be a dict with the keywords collection_string
|
||||||
The other arguments are the changed or deleted Django/OpenSlides model
|
(string), pk (positive integer), id_deleted (boolean) and dispatch_uid
|
||||||
instances.
|
(string).
|
||||||
"""
|
"""
|
||||||
if settings.USE_TORNADO_AS_WSGI_SERVER:
|
for access_permissions in BaseAccessPermissions.get_all():
|
||||||
for instance in args:
|
if access_permissions.get_dispatch_uid() == message['dispatch_uid']:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid message. A valid dispatch_uid is missing.')
|
||||||
|
|
||||||
|
if not message['is_deleted']:
|
||||||
|
Model = get_model_from_collection_string(message['collection_string'])
|
||||||
|
instance = Model.objects.get(pk=message['pk'])
|
||||||
|
full_data = access_permissions.get_full_data(instance)
|
||||||
|
|
||||||
|
# Loop over all logged in users and the anonymous user.
|
||||||
|
for user in itertools.chain(get_logged_in_users(), [AnonymousUser()]):
|
||||||
|
channel = Group('user-{}'.format(user.id))
|
||||||
|
output = {
|
||||||
|
'collection': message['collection_string'],
|
||||||
|
'id': instance.get_rest_pk(),
|
||||||
|
'action': 'deleted' if message['is_deleted'] else 'changed'}
|
||||||
|
if not message['is_deleted']:
|
||||||
|
data = access_permissions.get_restricted_data(full_data, user)
|
||||||
|
if data is None:
|
||||||
|
# There are no data for the user so he can't see the object. Skip him.
|
||||||
|
continue
|
||||||
|
output['data'] = data
|
||||||
|
channel.send({'text': json.dumps(output)})
|
||||||
|
|
||||||
|
|
||||||
|
def inform_changed_data(instance, is_deleted=False):
|
||||||
try:
|
try:
|
||||||
root_instance = instance.get_root_rest_element()
|
root_instance = instance.get_root_rest_element()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Instance has no method get_root_rest_element. Just skip it.
|
# Instance has no method get_root_rest_element. Just ignore it.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
access_permissions = root_instance.get_access_permissions()
|
try:
|
||||||
container = {
|
Channel('autoupdate.send_data').send({
|
||||||
'dispatch_uid': access_permissions.get_dispatch_uid(),
|
|
||||||
'collection_string': root_instance.get_collection_string(),
|
'collection_string': root_instance.get_collection_string(),
|
||||||
'rest_pk': root_instance.get_rest_pk()}
|
'pk': root_instance.pk,
|
||||||
if is_delete and instance == root_instance:
|
'is_deleted': is_deleted and instance == root_instance,
|
||||||
# A root instance is deleted.
|
'dispatch_uid': root_instance.get_access_permissions().get_dispatch_uid()})
|
||||||
container['action'] = 'deleted'
|
except ChannelLayer.ChannelFull:
|
||||||
else:
|
|
||||||
# A non root instance is deleted or any instance is just changed.
|
|
||||||
container['action'] = 'changed'
|
|
||||||
root_instance.refresh_from_db()
|
|
||||||
container['full_data'] = access_permissions.get_full_data(root_instance)
|
|
||||||
OpenSlidesSockJSConnection.send_object(json.dumps(container))
|
|
||||||
else:
|
|
||||||
pass
|
pass
|
||||||
# TODO: Implement big variant with Apache or Nginx as WSGI webserver.
|
|
||||||
|
|
||||||
|
|
||||||
def inform_changed_data_receiver(sender, instance, **kwargs):
|
def inform_changed_data_receiver(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Receiver for the inform_changed_data function to use in a signal.
|
Receiver for the inform_changed_data function to use in a signal.
|
||||||
"""
|
"""
|
||||||
inform_changed_data(False, instance)
|
inform_changed_data(instance)
|
||||||
|
|
||||||
|
|
||||||
def inform_deleted_data_receiver(sender, instance, **kwargs):
|
def inform_deleted_data_receiver(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Receiver for the inform_changed_data function to use in a signal.
|
Receiver for the inform_changed_data function to use in a signal.
|
||||||
"""
|
"""
|
||||||
inform_changed_data(True, instance)
|
inform_changed_data(instance, is_deleted=True)
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
Settings file for OpenSlides
|
Settings file for OpenSlides
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/1.7/topics/settings/
|
https://docs.djangoproject.com/en/1.9/topics/settings/
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
https://docs.djangoproject.com/en/1.9/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -26,7 +26,6 @@ SECRET_KEY = %(secret_key)r
|
|||||||
# SECURITY WARNING: Don't run with debug turned on in production!
|
# SECURITY WARNING: Don't run with debug turned on in production!
|
||||||
|
|
||||||
DEBUG = %(debug)s
|
DEBUG = %(debug)s
|
||||||
TEMPLATE_DEBUG = DEBUG
|
|
||||||
|
|
||||||
|
|
||||||
# OpenSlides plugins
|
# OpenSlides plugins
|
||||||
@ -41,7 +40,7 @@ INSTALLED_APPS += INSTALLED_PLUGINS
|
|||||||
|
|
||||||
# Database
|
# Database
|
||||||
# Change this to use MySQL or PostgreSQL.
|
# Change this to use MySQL or PostgreSQL.
|
||||||
# See https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
# See https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
@ -51,6 +50,25 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Big Mode
|
||||||
|
# Uncomment the following lines to activate redis as channel and cache backend.
|
||||||
|
# You have to install a redis server and the python packages asgi_redis and
|
||||||
|
# django-redis for this to work.
|
||||||
|
# See https://channels.readthedocs.io/en/latest/backends.html#redis
|
||||||
|
# https://niwinz.github.io/django-redis/latest/#_user_guide
|
||||||
|
|
||||||
|
# CHANNEL_LAYERS['default']['BACKEND'] = 'asgi_redis.RedisChannelLayer'
|
||||||
|
# CACHES = {
|
||||||
|
# "default": {
|
||||||
|
# "BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
# "LOCATION": "redis://127.0.0.1:6379/1",
|
||||||
|
# "OPTIONS": {
|
||||||
|
# "CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
# Some other settings
|
# Some other settings
|
||||||
|
|
||||||
TIME_ZONE = 'Europe/Berlin'
|
TIME_ZONE = 'Europe/Berlin'
|
||||||
@ -63,5 +81,4 @@ TEMPLATE_DIRS = (
|
|||||||
|
|
||||||
STATICFILES_DIRS = [os.path.join(OPENSLIDES_USER_DATA_PATH, 'static')] + STATICFILES_DIRS
|
STATICFILES_DIRS = [os.path.join(OPENSLIDES_USER_DATA_PATH, 'static')] + STATICFILES_DIRS
|
||||||
|
|
||||||
|
|
||||||
SEARCH_INDEX = os.path.join(OPENSLIDES_USER_DATA_PATH, 'search_index')
|
SEARCH_INDEX = os.path.join(OPENSLIDES_USER_DATA_PATH, 'search_index')
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"gulp-jshint": "~2.0.0",
|
"gulp-jshint": "~2.0.0",
|
||||||
"gulp-rename": "~1.2.2",
|
"gulp-rename": "~1.2.2",
|
||||||
"gulp-uglify": "~1.5.2",
|
"gulp-uglify": "~1.5.2",
|
||||||
|
"jshint": "~2.9.2",
|
||||||
"main-bower-files": "~2.11.1",
|
"main-bower-files": "~2.11.1",
|
||||||
"po2json": "~0.4.1",
|
"po2json": "~0.4.1",
|
||||||
"sprintf-js": "~1.0.3",
|
"sprintf-js": "~1.0.3",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Requirements for OpenSlides in production in alphabetical order
|
# Requirements for OpenSlides in production in alphabetical order
|
||||||
Django>=1.8,<1.10
|
Django>=1.8,<1.10
|
||||||
beautifulsoup4>=4.4,<4.5
|
beautifulsoup4>=4.4,<4.5
|
||||||
|
channels>=0.14,<0.15
|
||||||
djangorestframework>=3.2.0,<3.4.0
|
djangorestframework>=3.2.0,<3.4.0
|
||||||
html5lib>=0.9,<1.0
|
html5lib>=0.9,<1.0
|
||||||
jsonfield>=0.9.19,<1.1
|
jsonfield>=0.9.19,<1.1
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import json
|
import json
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides import __version__ as version
|
from openslides import __version__ as version
|
||||||
from openslides.core.config import ConfigHandler, ConfigVariable
|
from openslides.core.config import ConfigVariable, config
|
||||||
from openslides.core.models import CustomSlide, Projector
|
from openslides.core.models import CustomSlide, Projector
|
||||||
from openslides.utils.rest_api import ValidationError
|
from openslides.utils.rest_api import ValidationError
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
config = ConfigHandler()
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectorAPI(TestCase):
|
class ProjectorAPI(TestCase):
|
||||||
"""
|
"""
|
||||||
@ -75,18 +72,19 @@ class VersionView(TestCase):
|
|||||||
'version': 'unknown'}]})
|
'version': 'unknown'}]})
|
||||||
|
|
||||||
|
|
||||||
@patch('openslides.core.config.config', config)
|
|
||||||
@patch('openslides.core.views.config', config)
|
|
||||||
class ConfigViewSet(TestCase):
|
class ConfigViewSet(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests requests to deal with config variables.
|
Tests requests to deal with config variables.
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
# Save the old value of the config object and add the test values
|
||||||
|
# TODO: Can be changed to setUpClass when Django 1.8 is no longer supported
|
||||||
|
self._config_values = config.config_variables.copy()
|
||||||
config.update_config_variables(set_simple_config_view_integration_config_test())
|
config.update_config_variables(set_simple_config_view_integration_config_test())
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# Reset the config variables
|
# Reset the config variables
|
||||||
config.config_variables = {}
|
config.config_variables = self._config_values
|
||||||
|
|
||||||
def test_retrieve(self):
|
def test_retrieve(self):
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
|
0
tests/integration/utils/__init__.py
Normal file
0
tests/integration/utils/__init__.py
Normal file
46
tests/integration/utils/autoupdate.py
Normal file
46
tests/integration/utils/autoupdate.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from openslides.core.models import Session
|
||||||
|
from openslides.users.models import User
|
||||||
|
from openslides.utils.autoupdate import get_logged_in_users
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLoggedInUsers(TestCase):
|
||||||
|
def test_call(self):
|
||||||
|
"""
|
||||||
|
Test to call the function with:
|
||||||
|
* A user that session has not expired
|
||||||
|
* A user that session has expired
|
||||||
|
* A user that has no session
|
||||||
|
* An anonymous user that session hot not expired
|
||||||
|
|
||||||
|
Only the user with the session that has not expired should be returned
|
||||||
|
"""
|
||||||
|
user1 = User.objects.create(username='user1')
|
||||||
|
user2 = User.objects.create(username='user2')
|
||||||
|
User.objects.create(username='user3')
|
||||||
|
|
||||||
|
# Create a session with a user, that expires in 5 hours
|
||||||
|
Session.objects.create(user=user1, expire_data=timezone.now() + timedelta(hours=5))
|
||||||
|
|
||||||
|
# Create a session with a user, that is expired before 5 hours
|
||||||
|
Session.objects.create(user=user2, expire_data=timezone.now() + timedelta(hours=-5))
|
||||||
|
|
||||||
|
# Create a session with an anonymous user, that expires in 5 hours
|
||||||
|
Session.objects.create(user=None, expire_data=timezone.now() + timedelta(hours=5))
|
||||||
|
|
||||||
|
self.assertEqual(list(get_logged_in_users()), [user1])
|
||||||
|
|
||||||
|
def test_unique(self):
|
||||||
|
"""
|
||||||
|
Test the function with a user that has two not expired session.
|
||||||
|
The user should be returned only once.
|
||||||
|
"""
|
||||||
|
user1 = User.objects.create(username='user1')
|
||||||
|
Session.objects.create(user=user1, expire_data=timezone.now() + timedelta(hours=1))
|
||||||
|
Session.objects.create(user=user1, expire_data=timezone.now() + timedelta(hours=2))
|
||||||
|
|
||||||
|
self.assertEqual(list(get_logged_in_users()), [user1])
|
@ -1,6 +1,4 @@
|
|||||||
from unittest.mock import patch
|
from openslides.core.config import ConfigVariable, config
|
||||||
|
|
||||||
from openslides.core.config import ConfigHandler, ConfigVariable
|
|
||||||
from openslides.core.exceptions import ConfigError, ConfigNotFound
|
from openslides.core.exceptions import ConfigError, ConfigNotFound
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
@ -8,12 +6,12 @@ from openslides.utils.test import TestCase
|
|||||||
class TestConfigException(Exception):
|
class TestConfigException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
config = ConfigHandler()
|
|
||||||
|
|
||||||
|
|
||||||
@patch('openslides.core.config.config', config)
|
|
||||||
class HandleConfigTest(TestCase):
|
class HandleConfigTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
# Save the old value of the config object and add the test values
|
||||||
|
# TODO: Can be changed to setUpClass when Django 1.8 is no longer supported
|
||||||
|
self._config_values = config.config_variables.copy()
|
||||||
config.update_config_variables(set_grouped_config_view())
|
config.update_config_variables(set_grouped_config_view())
|
||||||
config.update_config_variables(set_simple_config_view())
|
config.update_config_variables(set_simple_config_view())
|
||||||
config.update_config_variables(set_simple_config_view_multiple_vars())
|
config.update_config_variables(set_simple_config_view_multiple_vars())
|
||||||
@ -22,7 +20,7 @@ class HandleConfigTest(TestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# Reset the config variables
|
# Reset the config variables
|
||||||
config.config_variables = {}
|
config.config_variables = self._config_values
|
||||||
|
|
||||||
def get_config_var(self, key):
|
def get_config_var(self, key):
|
||||||
return config[key]
|
return config[key]
|
||||||
|
Loading…
Reference in New Issue
Block a user