Change system for autoupdate on the projector (#2394)

* Second websocket channel for the projector

* Removed use of projector requirements for REST API requests.

Refactored data serializing for projector websocket connection.

* Refactor the way that the projector autoupdate get its data.

* Fixed missing assignment slide title for hidden items.

* Release all items for item list slide and list of speakers slide. Fixed error with motion workflow.

* Created CollectionElement class which helps to handle autoupdate.
This commit is contained in:
Oskar Hahn 2016-09-17 22:26:23 +02:00 committed by GitHub
parent 6ade5630ff
commit 6abb0976c2
48 changed files with 681 additions and 482 deletions

View File

@ -16,7 +16,6 @@ Assignments:
- Remove unused assignment config to publish winner election results only. - Remove unused assignment config to publish winner election results only.
Core: Core:
- Used Django Channels instead of Tornado.
- Added support for big assemblies with lots of users. - Added support for big assemblies with lots of users.
- Added HTML support for messages on the projector. - Added HTML support for messages on the projector.
@ -38,6 +37,7 @@ Other:
- Removed 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. - Fixed bug, that the last change of a config value was not send via autoupdate.
- Added template hooks for plugins (in item detail view and motion poll form). - Added template hooks for plugins (in item detail view and motion poll form).
- Used Django Channels instead of Tornado. Refactoring of the autoupdate process.
Version 2.0 (2016-04-18) Version 2.0 (2016-04-18)

View File

@ -5,7 +5,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Item and ItemViewSet. Access permissions container for Item and ItemViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -19,15 +19,28 @@ class ItemAccessPermissions(BaseAccessPermissions):
return ItemSerializer return ItemSerializer
# TODO: In the following method we use full_data['is_hidden'] but this can be out of date.
def get_restricted_data(self, full_data, user): def get_restricted_data(self, full_data, user):
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. for the user.
""" """
if (self.can_retrieve(user) and if (user.has_perm('agenda.can_see') and
(not full_data['is_hidden'] or (not full_data['is_hidden'] or
user.has_perm('agenda.can_see_hidden_items'))): user.has_perm('agenda.can_see_hidden_items'))):
data = full_data data = full_data
else: else:
data = None data = None
return data return data
def get_projector_data(self, full_data):
"""
Returns the restricted serialized data for the instance prepared
for the projector. Removes field 'comment'.
"""
data = {}
for key in full_data.keys():
if key != 'comment':
data[key] = full_data[key]
return data

View File

@ -1,15 +1,14 @@
from openslides.core.exceptions import ProjectorException from ..core.config import config
from openslides.utils.projector import ProjectorElement, ProjectorRequirement from ..core.exceptions import ProjectorException
from ..utils.projector import ProjectorElement
from .models import Item from .models import Item
from .views import ItemViewSet
class ItemListSlide(ProjectorElement): class ItemListSlide(ProjectorElement):
""" """
Slide definitions for Item model. Slide definitions for Item model.
This is only for list slides. This is only for item list slides.
Set 'id' to None to get a list slide of all root items. Set 'id' to an Set 'id' to None to get a list slide of all root items. Set 'id' to an
integer to get a list slide of the children of the metioned item. integer to get a list slide of the children of the metioned item.
@ -26,18 +25,7 @@ class ItemListSlide(ProjectorElement):
raise ProjectorException('Item does not exist.') raise ProjectorException('Item does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
pk = config_entry.get('id', 'tree') yield from Item.objects.all()
if pk is None or config_entry.get('tree', False):
# Root list slide or slide with tree.
yield ProjectorRequirement(
view_class=ItemViewSet,
view_action='tree')
# Root list slide and children list slide.
# Related objects like users and tags are not unlocked.
yield ProjectorRequirement(
view_class=ItemViewSet,
view_action='list')
class ListOfSpeakersSlide(ProjectorElement): class ListOfSpeakersSlide(ProjectorElement):
@ -49,10 +37,7 @@ class ListOfSpeakersSlide(ProjectorElement):
name = 'agenda/list-of-speakers' name = 'agenda/list-of-speakers'
def check_data(self): def check_data(self):
pk = self.config_entry.get('id') if not Item.objects.filter(pk=self.config_entry.get('id')).exists():
if pk is None:
raise ProjectorException('Id must not be None.')
if not Item.objects.filter(pk=pk).exists():
raise ProjectorException('Item does not exist.') raise ProjectorException('Item does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
@ -65,12 +50,12 @@ class ListOfSpeakersSlide(ProjectorElement):
# Item does not exist. Just do nothing. # Item does not exist. Just do nothing.
pass pass
else: else:
yield ProjectorRequirement( yield item
view_class=ItemViewSet, for speaker in item.speakers.filter(end_time=None):
view_action='retrieve', # Yield current speaker and next speakers
pk=str(item.pk)) yield speaker.user
for speaker in item.speakers.all(): query = (item.speakers.exclude(end_time=None)
yield ProjectorRequirement( .order_by('-end_time')[:config['agenda_show_last_speakers']])
view_class=speaker.user.get_view_class(), for speaker in query:
view_action='retrieve', # Yield last speakers
pk=str(speaker.user_id)) yield speaker.user

View File

@ -10,9 +10,14 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
Receiver function to create agenda items. It is connected to the signal Receiver function to create agenda items. It is connected to the signal
django.db.models.signals.post_save during app loading. django.db.models.signals.post_save during app loading.
""" """
if created and hasattr(instance, 'get_agenda_title'): if hasattr(instance, 'get_agenda_title'):
if created:
# If the object is created, the related_object has to be sent again.
Item.objects.create(content_object=instance) Item.objects.create(content_object=instance)
inform_changed_data(instance) inform_changed_data(instance)
else:
# If the object has changed, then also the agenda item has to be sent.
inform_changed_data(instance.agenda_item)
def listen_to_related_object_post_delete(sender, instance, **kwargs): def listen_to_related_object_post_delete(sender, instance, **kwargs):

View File

@ -25,8 +25,6 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var id = $scope.element.id;
Agenda.find(id);
User.findAll();
Agenda.bindOne(id, $scope, 'item'); Agenda.bindOne(id, $scope, 'item');
} }
]) ])
@ -41,7 +39,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
// Attention! Each object that is used here has to be dealt on server side. // Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
Agenda.findAll();
// Bind agenda tree to the scope // Bind agenda tree to the scope
$scope.$watch(function () { $scope.$watch(function () {
return Agenda.lastModified(); return Agenda.lastModified();

View File

@ -41,9 +41,9 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list', 'manage_speaker', 'tree'): elif self.action in ('metadata', 'manage_speaker', 'tree'):
result = self.request.user.has_perm('agenda.can_see') result = self.request.user.has_perm('agenda.can_see')
# For manage_speaker and tree requests the rest of the check is # For manage_speaker and tree requests the rest of the check is
# done in the specific method. See below. # done in the specific method. See below.
@ -63,6 +63,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
Checks if the requesting user has permission to see also an Checks if the requesting user has permission to see also an
organizational item if it is one. organizational item if it is one.
""" """
# TODO: Move this logic to access_permissions.ItemAccessPermissions.
if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'): if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'):
self.permission_denied(request) self.permission_denied(request)
@ -70,6 +71,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
""" """
Filters organizational items if the user has no permission to see them. Filters organizational items if the user has no permission to see them.
""" """
# TODO: Move this logic to access_permissions.ItemAccessPermissions.
queryset = super().get_queryset() queryset = super().get_queryset()
if not self.request.user.has_perm('agenda.can_see_hidden_items'): if not self.request.user.has_perm('agenda.can_see_hidden_items'):
pk_list = [item.pk for item in Item.objects.get_only_agenda_items()] pk_list = [item.pk for item in Item.objects.get_only_agenda_items()]

View File

@ -5,7 +5,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Assignment and AssignmentViewSet. Access permissions container for Assignment and AssignmentViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -17,7 +17,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
""" """
from .serializers import AssignmentFullSerializer, AssignmentShortSerializer from .serializers import AssignmentFullSerializer, AssignmentShortSerializer
if user is None or user.has_perm('assignments.can_manage'): if user is None or (user.has_perm('assignments.can_see') and user.has_perm('assignments.can_manage')):
serializer_class = AssignmentFullSerializer serializer_class = AssignmentFullSerializer
else: else:
serializer_class = AssignmentShortSerializer serializer_class = AssignmentShortSerializer
@ -29,9 +29,20 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
for the user. Removes unpublished polls for non admins so that they for the user. Removes unpublished polls for non admins so that they
only get a result like the AssignmentShortSerializer would give them. only get a result like the AssignmentShortSerializer would give them.
""" """
if user.has_perm('assignments.can_manage'): if user.has_perm('assignments.can_see') and user.has_perm('assignments.can_manage'):
data = full_data data = full_data
elif user.has_perm('assignments.can_see'):
data = full_data.copy()
data['polls'] = [poll for poll in data['polls'] if poll['published']]
else: else:
data = None
return data
def get_projector_data(self, full_data):
"""
Returns the restricted serialized data for the instance prepared
for the projector. Removes several fields.
"""
data = full_data.copy() data = full_data.copy()
data['polls'] = [poll for poll in data['polls'] if poll['published']] data['polls'] = [poll for poll in data['polls'] if poll['published']]
return data return data

View File

@ -1,63 +1,29 @@
from openslides.core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from openslides.core.views import TagViewSet from ..utils.projector import ProjectorElement
from openslides.utils.projector import ProjectorElement, ProjectorRequirement from .models import Assignment
from .models import Assignment, AssignmentPoll
from .views import AssignmentViewSet
class AssignmentSlide(ProjectorElement): class AssignmentSlide(ProjectorElement):
""" """
Slide definitions for Assignment model. Slide definitions for Assignment model.
Set 'id' to get a detail slide. Omit it to get a list slide.
""" """
name = 'assignments/assignment' name = 'assignments/assignment'
def check_data(self): def check_data(self):
pk = self.config_entry.get('id') if not Assignment.objects.filter(pk=self.config_entry.get('id')).exists():
if pk is not None:
# Detail slide.
if not Assignment.objects.filter(pk=pk).exists():
raise ProjectorException('Election does not exist.') raise ProjectorException('Election does not exist.')
poll_id = self.config_entry.get('poll')
if poll_id is not None:
# Poll slide.
if not AssignmentPoll.objects.filter(pk=poll_id).exists():
raise ProjectorException('Poll does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
pk = config_entry.get('id')
if pk is None:
# List slide. Related objects like users and tags are not unlocked.
yield ProjectorRequirement(
view_class=AssignmentViewSet,
view_action='list')
else:
# Detail slide.
try: try:
assignment = Assignment.objects.get(pk=pk) assignment = Assignment.objects.get(pk=config_entry.get('id'))
except Assignment.DoesNotExist: except Assignment.DoesNotExist:
# Assignment does not exist. Just do nothing. # Assignment does not exist. Just do nothing.
pass pass
else: else:
yield ProjectorRequirement( yield assignment
view_class=AssignmentViewSet, yield assignment.agenda_item
view_action='retrieve',
pk=str(assignment.pk))
for user in assignment.related_users.all(): for user in assignment.related_users.all():
yield ProjectorRequirement( yield user
view_class=user.get_view_class(),
view_action='retrieve',
pk=str(user.pk))
for poll in assignment.polls.all().prefetch_related('options'): for poll in assignment.polls.all().prefetch_related('options'):
for option in poll.options.all(): for option in poll.options.all():
yield ProjectorRequirement( yield option.candidate
view_class=option.candidate.get_view_class(),
view_action='retrieve',
pk=str(option.candidate_id))
for tag in assignment.tags.all():
yield ProjectorRequirement(
view_class=TagViewSet,
view_action='retrieve',
pk=str(tag.pk))

View File

@ -24,16 +24,10 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment
var id = $scope.element.id; var id = $scope.element.id;
var poll = $scope.element.poll; var poll = $scope.element.poll;
// load assignemt object and related agenda item
Assignment.find(id).then(function(assignment) {
Assignment.loadRelations(assignment, 'agenda_item');
});
Assignment.bindOne(id, $scope, 'assignment'); Assignment.bindOne(id, $scope, 'assignment');
Assignment.getPhases().then(function(phases) { Assignment.getPhases().then(function(phases) {
$scope.phases = phases; $scope.phases = phases;
}); });
// load all users
User.findAll();
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
} }
]); ]);

View File

@ -12,7 +12,7 @@
<!-- Title --> <!-- Title -->
<div id="title"> <div id="title">
<h1>{{ assignment.agenda_item.getTitle() }}</h1> <h1>{{ assignment.agenda_item.getTitle() || assignment.title }}</h1>
<h2> <h2>
<translate>Election</translate> <translate>Election</translate>
</h2> </h2>

View File

@ -52,10 +52,11 @@ class AssignmentViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
result = self.request.user.has_perm('assignments.can_see') # Everybody is allowed to see the metadata.
result = True
elif self.action in ('create', 'partial_update', 'update', 'destroy', elif self.action in ('create', 'partial_update', 'update', 'destroy',
'mark_elected', 'create_poll'): 'mark_elected', 'create_poll'):
result = (self.request.user.has_perm('assignments.can_see') and result = (self.request.user.has_perm('assignments.can_see') and

View File

@ -5,7 +5,7 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Projector and ProjectorViewSet. Access permissions container for Projector and ProjectorViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -24,7 +24,7 @@ class CustomSlideAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for CustomSlide and CustomSlideViewSet. Access permissions container for CustomSlide and CustomSlideViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -43,7 +43,7 @@ class TagAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Tag and TagViewSet. Access permissions container for Tag and TagViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -66,7 +66,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for ChatMessage and ChatMessageViewSet. Access permissions container for ChatMessage and ChatMessageViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -88,7 +88,7 @@ class ConfigAccessPermissions(BaseAccessPermissions):
Access permissions container for the config (ConfigStore and Access permissions container for the config (ConfigStore and
ConfigViewSet). ConfigViewSet).
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """

View File

@ -141,6 +141,12 @@ class ConfigHandler:
if config_variable.translatable: if config_variable.translatable:
yield config_variable.name yield config_variable.name
def get_collection_string(self):
"""
Returns the collection_string from the CollectionStore.
"""
return ConfigStore.get_collection_string()
config = ConfigHandler() config = ConfigHandler()
""" """
Final entry point to get an set config variables. To get a config variable Final entry point to get an set config variables. To get a config variable

View File

@ -1,4 +1,5 @@
from django.core.validators import MaxLengthValidator from django.core.validators import MaxLengthValidator
from openslides.core.config import ConfigVariable from openslides.core.config import ConfigVariable

View File

@ -111,11 +111,9 @@ class Projector(RESTModelMixin, models.Model):
result[key]['error'] = str(e) result[key]['error'] = str(e)
return result return result
@classmethod def get_all_requirements(self):
def get_all_requirements(cls):
""" """
Generator which returns all ProjectorRequirement instances of all Generator which returns all instances that are shown on this projector.
active projector elements.
""" """
# Get all elements from all apps. # Get all elements from all apps.
elements = {} elements = {}
@ -123,12 +121,38 @@ class Projector(RESTModelMixin, models.Model):
elements[element.name] = element elements[element.name] = element
# Generator # Generator
for projector in cls.objects.all(): for key, value in self.config.items():
for key, value in projector.config.items():
element = elements.get(value['name']) element = elements.get(value['name'])
if element is not None: if element is not None:
for requirement in element.get_requirements(value): yield from element.get_requirements(value)
yield requirement
def collection_element_is_shown(self, collection_element):
"""
Returns True if this collection element is shown on this projector.
"""
for requirement in self.get_all_requirements():
if (requirement.get_collection_string() == collection_element['collection_string'] and
requirement.pk == collection_element['id']):
result = True
break
else:
result = False
return result
@classmethod
def get_projectors_that_show_this(cls, collection_element):
"""
Returns a list of the projectors that show this collection element.
"""
result = []
for projector in cls.objects.all():
if projector.collection_element_is_shown(collection_element):
result.append(projector)
return result
def need_full_update_for(self, collection_element):
# TODO: Implement this for all ProjectorElements (also for config values!)
return True
class CustomSlide(RESTModelMixin, models.Model): class CustomSlide(RESTModelMixin, models.Model):

View File

@ -1,11 +1,9 @@
from django.utils.timezone import now from django.utils.timezone import now
from openslides.utils.projector import ProjectorElement, ProjectorRequirement from ..utils.projector import ProjectorElement
from .config import config from .config import config
from .exceptions import ProjectorException from .exceptions import ProjectorException
from .models import CustomSlide, Projector from .models import CustomSlide, Projector
from .views import CustomSlideViewSet
class CustomSlideSlide(ProjectorElement): class CustomSlideSlide(ProjectorElement):
@ -19,12 +17,14 @@ class CustomSlideSlide(ProjectorElement):
raise ProjectorException('Custom slide does not exist.') raise ProjectorException('Custom slide does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
pk = config_entry.get('id') try:
if pk is not None: custom_slide = CustomSlide.objects.get(pk=config_entry.get('id'))
yield ProjectorRequirement( except CustomSlide.DoesNotExist:
view_class=CustomSlideViewSet, # Custom slide does not exist. Just do nothing.
view_action='retrieve', pass
pk=str(pk)) else:
yield custom_slide
yield custom_slide.agenda_item
class Clock(ProjectorElement): class Clock(ProjectorElement):

View File

@ -38,11 +38,22 @@ angular.module('OpenSlidesApp.core', [
.factory('autoupdate', [ .factory('autoupdate', [
'DS', 'DS',
'$rootScope', '$rootScope',
function (DS, $rootScope) { 'REALM',
function (DS, $rootScope, REALM) {
var socket = null; var socket = null;
var recInterval = null; var recInterval = null;
$rootScope.connected = false; $rootScope.connected = false;
var websocketPath;
if (REALM == 'site') {
websocketPath = '/ws/site/';
} else if (REALM == 'projector') {
// TODO: At the moment there is only one projector. Find out which one is requested
websocketPath = '/ws/projector/1/';
} else {
console.error('The constant REALM is not set properly.');
}
var Autoupdate = { var Autoupdate = {
messageReceivers: [], messageReceivers: [],
onMessage: function (receiver) { onMessage: function (receiver) {
@ -55,7 +66,7 @@ angular.module('OpenSlidesApp.core', [
} }
}; };
var newConnect = function () { var newConnect = function () {
socket = new WebSocket('ws://' + location.host + '/ws/'); socket = new WebSocket('ws://' + location.host + websocketPath);
clearInterval(recInterval); clearInterval(recInterval);
socket.onopen = function () { socket.onopen = function () {
$rootScope.connected = true; $rootScope.connected = true;
@ -175,8 +186,11 @@ angular.module('OpenSlidesApp.core', [
autoupdate.onMessage(function(json) { 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: If you don't have the permission to see a projector, the
var data = JSON.parse(json); // variable json is a string with an error message. Therefor
// the next line fails.
var dataList = JSON.parse(json);
_.forEach(dataList, function(data) {
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.action == 'changed') { if (data.action == 'changed') {
@ -192,32 +206,47 @@ angular.module('OpenSlidesApp.core', [
} }
DS.eject(data.collection, data.id); DS.eject(data.collection, data.id);
} }
// If you want to handle more status codes, change server });
// restrictions in utils/autoupdate.py.
}); });
} }
]) ])
.factory('loadGlobalData', [ // Save the server time to the rootscope.
'$rootScope', .run([
'$http', '$http',
'ChatMessage', '$rootScope',
function ($http, $rootScope) {
// Loads server time and calculates server offset
$rootScope.serverOffset = Math.floor(Date.now() / 1000);
$http.get('/core/servertime/')
.then(function(data) {
$rootScope.serverOffset = Math.floor(Date.now() / 1000 - data.data);
});
}
])
.run([
'Config', 'Config',
'Projector', '$rootScope',
function ($rootScope, $http, ChatMessage, Config, Projector) { function (Config, $rootScope) {
return function () {
// Puts the config object into each scope.
Config.findAll().then(function() {
$rootScope.config = function (key) { $rootScope.config = function (key) {
try { try {
return Config.get(key).value; return Config.get(key).value;
} }
catch(err) { catch(err) {
console.log("Unkown config key: " + key);
return ''; return '';
} }
}; };
}); }
])
.factory('loadGlobalData', [
'ChatMessage',
'Config',
'Projector',
function (ChatMessage, Config, Projector) {
return function () {
Config.findAll();
// Loads all projector data // Loads all projector data
Projector.findAll(); Projector.findAll();
@ -233,23 +262,10 @@ angular.module('OpenSlidesApp.core', [
}); });
}); });
//} //}
// Loads server time and calculates server offset
$http.get('/core/servertime/').then(function(data) {
$rootScope.serverOffset = Math.floor( Date.now() / 1000 - data.data );
});
}; };
} }
]) ])
// Load the global data on startup
.run([
'loadGlobalData',
function(loadGlobalData) {
loadGlobalData();
}
])
// Template hooks // Template hooks

View File

@ -5,6 +5,9 @@
// The core module for the OpenSlides projector // The core module for the OpenSlides projector
angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core']) angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Can be used to find out if the projector or the side is used
.constant('REALM', 'projector')
// Provider to register slides in a .config() statement. // Provider to register slides in a .config() statement.
.provider('slides', [ .provider('slides', [
function() { function() {
@ -26,7 +29,7 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
element.template = slidesMap[element.name].template; element.template = slidesMap[element.name].template;
elements.push(element); elements.push(element);
} else { } else {
console.log("Unknown slide: " + element.name); console.error("Unknown slide: " + element.name);
} }
}); });
return elements; return elements;
@ -61,7 +64,9 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('ProjectorContainerCtrl', [ .controller('ProjectorContainerCtrl', [
'$scope', '$scope',
'Config', 'Config',
function($scope, Config) { 'loadGlobalData',
function($scope, Config, loadGlobalData) {
loadGlobalData();
// watch for changes in Config // watch for changes in Config
var last_conf; var last_conf;
$scope.$watch(function () { $scope.$watch(function () {
@ -123,21 +128,25 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
'Projector', 'Projector',
'slides', 'slides',
function($scope, Projector, slides) { function($scope, Projector, slides) {
Projector.find(1).then(function() {
$scope.$watch(function () { $scope.$watch(function () {
// TODO: Use the current projector. At the moment there is only one.
return Projector.lastModified(1); return Projector.lastModified(1);
}, function () { }, function () {
// TODO: Use the current projector. At the moment there is only one
var projector = Projector.get(1);
if (projector) {
$scope.elements = []; $scope.elements = [];
_.forEach(slides.getElements(Projector.get(1)), function(element) { _.forEach(slides.getElements(projector), function(element) {
if (!element.error) { if (!element.error) {
$scope.elements.push(element); $scope.elements.push(element);
} else { } else {
console.error("Error for slide " + element.name + ": " + element.error); console.error("Error for slide " + element.name + ": " + element.error);
} }
}); });
// TODO: Use the current projector. At the moment there is only one
$scope.scroll = -5 * Projector.get(1).scroll; $scope.scroll = -5 * Projector.get(1).scroll;
$scope.scale = 100 + 20 * Projector.get(1).scale; $scope.scale = 100 + 20 * Projector.get(1).scale;
}); }
}); });
} }
]) ])
@ -150,9 +159,6 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var id = $scope.element.id;
Customslide.find(id).then(function(customslide) {
Customslide.loadRelations(customslide, 'agenda_item');
});
Customslide.bindOne(id, $scope, 'customslide'); Customslide.bindOne(id, $scope, 'customslide');
} }
]) ])

View File

@ -18,6 +18,10 @@ angular.module('OpenSlidesApp.core.site', [
'ui.tinymce', 'ui.tinymce',
'luegg.directives', 'luegg.directives',
]) ])
// Can be used to find out if the projector or the side is used
.constant('REALM', 'site')
.factory('PdfMakeDocumentProvider', [ .factory('PdfMakeDocumentProvider', [
'gettextCatalog', 'gettextCatalog',
'Config', 'Config',
@ -802,6 +806,14 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
// Load the global data on startup
.run([
'loadGlobalData',
function(loadGlobalData) {
loadGlobalData();
}
])
// Options for TinyMCE editor used in various create and edit views. // Options for TinyMCE editor used in various create and edit views.
.factory('Editor', [ .factory('Editor', [
'gettextCatalog', 'gettextCatalog',

View File

@ -177,9 +177,9 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
result = self.request.user.has_perm('core.can_see_projector') result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements', elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
'deactivate_elements', 'clear_elements', 'control_view', 'set_resolution'): 'deactivate_elements', 'clear_elements', 'control_view', 'set_resolution'):
@ -433,8 +433,8 @@ class CustomSlideViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
else: else:
result = self.request.user.has_perm('core.can_manage_projector') result = self.request.user.has_perm('core.can_manage_projector')
return result return result
@ -454,9 +454,9 @@ class TagViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
# Every authenticated user can see the metadata and list tags. # Every authenticated user can see the metadata and list tags.
# Anonymous users can do so if they are enabled. # Anonymous users can do so if they are enabled.
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous'] result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
@ -510,9 +510,9 @@ class ConfigViewSet(ViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
# Every authenticated user can see the metadata and list or # Every authenticated user can see the metadata and list or
# retrieve the config. Anonymous users can do so if they are # retrieve the config. Anonymous users can do so if they are
# enabled. # enabled.
@ -579,13 +579,13 @@ class ChatMessageViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
else: else:
# We do not want anonymous users to use the chat even the anonymous # We do not want anonymous users to use the chat even the anonymous
# group has the permission core.can_use_chat. # group has the permission core.can_use_chat.
result = ( result = (
self.action in ('metadata', 'list', 'create') and self.action in ('metadata', 'create') and
self.request.user.is_authenticated() and self.request.user.is_authenticated() and
self.request.user.has_perm('core.can_use_chat')) self.request.user.has_perm('core.can_use_chat'))
return result return result

View File

@ -145,5 +145,8 @@ CHANNEL_LAYERS = {
'default': { 'default': {
'BACKEND': 'asgiref.inmemory.ChannelLayer', 'BACKEND': 'asgiref.inmemory.ChannelLayer',
'ROUTING': 'openslides.routing.channel_routing', 'ROUTING': 'openslides.routing.channel_routing',
'CONFIG': {
'capacity': 1000,
},
}, },
} }

View File

@ -5,7 +5,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Mediafile and MediafileViewSet. Access permissions container for Mediafile and MediafileViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """

View File

@ -1,8 +1,6 @@
from openslides.core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from openslides.utils.projector import ProjectorElement, ProjectorRequirement from ..utils.projector import ProjectorElement
from .models import Mediafile from .models import Mediafile
from .views import MediafileViewSet
class MediafileSlide(ProjectorElement): class MediafileSlide(ProjectorElement):
@ -12,15 +10,14 @@ class MediafileSlide(ProjectorElement):
name = 'mediafiles/mediafile' name = 'mediafiles/mediafile'
def check_data(self): def check_data(self):
try: if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists():
Mediafile.objects.get(pk=self.config_entry.get('id'))
except Mediafile.DoesNotExist:
raise ProjectorException('File does not exist.') raise ProjectorException('File does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
pk = config_entry.get('id') try:
if pk is not None: mediafile = Mediafile.objects.get(pk=config_entry.get('id'))
yield ProjectorRequirement( except Mediafile.DoesNotExist:
view_class=MediafileViewSet, # Mediafile does not exist. Just do nothing.
view_action='retrieve', pass
pk=str(pk)) else:
yield mediafile

View File

@ -18,11 +18,9 @@ angular.module('OpenSlidesApp.mediafiles.projector', ['OpenSlidesApp.mediafiles'
'Mediafile', 'Mediafile',
function($scope, Mediafile) { function($scope, Mediafile) {
// load mediafile object // load mediafile object
var mediafile = Mediafile.find($scope.element.id); var mediafile = Mediafile.get($scope.element.id);
mediafile.then(function(mediafile) {
$scope.pdfName = mediafile.title; $scope.pdfName = mediafile.title;
$scope.pdfUrl = mediafile.mediafileUrl; $scope.pdfUrl = mediafile.mediafileUrl;
});
} }
]); ]);

View File

@ -19,9 +19,9 @@ class MediafileViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
result = self.request.user.has_perm('mediafiles.can_see') result = self.request.user.has_perm('mediafiles.can_see')
elif self.action == 'create': elif self.action == 'create':
result = (self.request.user.has_perm('mediafiles.can_see') and result = (self.request.user.has_perm('mediafiles.can_see') and

View File

@ -10,7 +10,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Motion and MotionViewSet. Access permissions container for Motion and MotionViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -43,6 +43,21 @@ class MotionAccessPermissions(BaseAccessPermissions):
pass pass
return data return data
def get_projector_data(self, full_data):
"""
Returns the restricted serialized data for the instance prepared
for the projector. Removes several fields.
"""
data = full_data.copy()
for i, field in enumerate(self.get_comments_config_fields()):
if not field.get('public'):
try:
data['comments'][i] = None
except IndexError:
# No data in range. Just do nothing.
pass
return data
def get_comments_config_fields(self): def get_comments_config_fields(self):
""" """
Take input from config field and parse it. It can be some Take input from config field and parse it. It can be some
@ -110,7 +125,7 @@ class CategoryAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Category and CategoryViewSet. Access permissions container for Category and CategoryViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -129,7 +144,7 @@ class WorkflowAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Workflow and WorkflowViewSet. Access permissions container for Workflow and WorkflowViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """

View File

@ -3,7 +3,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import jsonfield.fields import jsonfield.fields
from django.db import migrations from django.db import migrations

View File

@ -1,66 +1,27 @@
from openslides.core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from openslides.core.views import TagViewSet from ..utils.projector import ProjectorElement
from openslides.utils.projector import ProjectorElement, ProjectorRequirement
from .models import Motion from .models import Motion
from .views import CategoryViewSet, MotionViewSet, WorkflowViewSet
class MotionSlide(ProjectorElement): class MotionSlide(ProjectorElement):
""" """
Slide definitions for Motion model. Slide definitions for Motion model.
Set 'id' to get a detail slide. Omit it to get a list slide.
""" """
name = 'motions/motion' name = 'motions/motion'
def check_data(self): def check_data(self):
pk = self.config_entry.get('id') if not Motion.objects.filter(pk=self.config_entry.get('id')).exists():
if pk is not None:
# Detail slide.
if not Motion.objects.filter(pk=pk).exists():
raise ProjectorException('Motion does not exist.') raise ProjectorException('Motion does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
pk = config_entry.get('id')
if pk is None:
# List slide. Related objects like users and tags are not unlocked.
yield ProjectorRequirement(
view_class=MotionViewSet,
view_action='list')
else:
# Detail slide.
try: try:
motion = Motion.objects.get(pk=pk) motion = Motion.objects.get(pk=config_entry.get('id'))
except Motion.DoesNotExist: except Motion.DoesNotExist:
# Motion does not exist. Just do nothing. # Motion does not exist. Just do nothing.
pass pass
else: else:
yield ProjectorRequirement( yield motion
view_class=MotionViewSet, yield motion.agenda_item
view_action='retrieve', yield motion.state.workflow
pk=str(motion.pk)) yield from motion.submitters.all()
if motion.category: yield from motion.supporters.all()
yield ProjectorRequirement(
view_class=CategoryViewSet,
view_action='retrieve',
pk=str(motion.category.pk))
yield ProjectorRequirement(
view_class=WorkflowViewSet,
view_action='retrieve',
pk=str(motion.workflow))
for submitter in motion.submitters.all():
yield ProjectorRequirement(
view_class=submitter.get_view_class(),
view_action='retrieve',
pk=str(submitter.pk))
for supporter in motion.supporters.all():
yield ProjectorRequirement(
view_class=supporter.get_view_class(),
view_action='retrieve',
pk=str(supporter.pk))
for tag in motion.tags.all():
yield ProjectorRequirement(
view_class=TagViewSet,
view_action='retrieve',
pk=str(tag.pk))

View File

@ -60,14 +60,6 @@ angular.module('OpenSlidesApp.motions', [
} }
]) ])
// Load all MotionWorkflows at startup
.run([
'Workflow',
function (Workflow) {
Workflow.findAll();
}
])
.factory('MotionPoll', [ .factory('MotionPoll', [
'DS', 'DS',
'gettextCatalog', 'gettextCatalog',
@ -452,7 +444,8 @@ angular.module('OpenSlidesApp.motions', [
.run([ .run([
'Motion', 'Motion',
'Category', 'Category',
function(Motion, Category) {} 'Workflow',
function(Motion, Category, Workflow) {}
]) ])

View File

@ -23,18 +23,8 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var id = $scope.element.id;
// load motion object and related agenda item
Motion.find(id).then(function(motion) {
Motion.loadRelations(motion, 'agenda_item');
});
Motion.bindOne(id, $scope, 'motion'); Motion.bindOne(id, $scope, 'motion');
// load all users
User.findAll();
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Config.bindOne('motions_default_line_numbering', $scope, 'line_numbering');
} }
]); ]);

View File

@ -525,6 +525,14 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
} }
]) ])
// Load all MotionWorkflows at startup
.run([
'Workflow',
function (Workflow) {
Workflow.findAll();
}
])
// Service for generic motion form (create and update) // Service for generic motion form (create and update)
.factory('MotionForm', [ .factory('MotionForm', [
'gettextCatalog', 'gettextCatalog',

View File

@ -71,7 +71,7 @@
<!-- Text --> <!-- Text -->
<div ng-bind-html="motion.getTextWithLineBreaks() | trusted" <div ng-bind-html="motion.getTextWithLineBreaks() | trusted"
class="motion-text line-numbers-{{ line_numbering.value }}"></div> class="motion-text line-numbers-{{ config('motions_default_line_numbering') }}"></div>
<!-- Reason --> <!-- Reason -->
<h3 ng-if="motion.getReason()" translate>Reason</h3> <h3 ng-if="motion.getReason()" translate>Reason</h3>

View File

@ -53,9 +53,9 @@ class MotionViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list', 'partial_update', 'update'): elif self.action in ('metadata', 'partial_update', 'update'):
result = self.request.user.has_perm('motions.can_see') result = self.request.user.has_perm('motions.can_see')
# For partial_update and update requests the rest of the check is # For partial_update and update requests the rest of the check is
# done in the update method. See below. # done in the update method. See below.
@ -373,9 +373,9 @@ class CategoryViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
result = self.request.user.has_perm('motions.can_see') result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy', 'numbering'): elif self.action in ('create', 'partial_update', 'update', 'destroy', 'numbering'):
result = (self.request.user.has_perm('motions.can_see') and result = (self.request.user.has_perm('motions.can_see') and
@ -450,9 +450,9 @@ class WorkflowViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list'): elif self.action == 'metadata':
result = self.request.user.has_perm('motions.can_see') result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy'): elif self.action in ('create', 'partial_update', 'update', 'destroy'):
result = (self.request.user.has_perm('motions.can_see') and result = (self.request.user.has_perm('motions.can_see') and

View File

@ -1,9 +1,25 @@
from channels.routing import route from channels.routing import include, route
from openslides.utils.autoupdate import send_data, ws_add, ws_disconnect from openslides.utils.autoupdate import (
send_data,
ws_add_projector,
ws_add_site,
ws_disconnect_projector,
ws_disconnect_site,
)
projector_routing = [
route("websocket.connect", ws_add_projector),
route("websocket.disconnect", ws_disconnect_projector),
]
site_routing = [
route("websocket.connect", ws_add_site),
route("websocket.disconnect", ws_disconnect_site),
]
channel_routing = [ channel_routing = [
route("websocket.connect", ws_add, path='/ws/'), include(projector_routing, path=r'^/ws/projector/(?P<projector_id>\d+)/$'),
route("websocket.disconnect", ws_disconnect), include(site_routing, path=r'^/ws/site/$'),
route("autoupdate.send_data", send_data), route("autoupdate.send_data", send_data),
] ]

View File

@ -5,7 +5,7 @@ class UserAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for User and UserViewSet. Access permissions container for User and UserViewSet.
""" """
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access model instances. Returns True if the user has read access model instances.
""" """
@ -33,12 +33,34 @@ class UserAccessPermissions(BaseAccessPermissions):
""" """
from .serializers import USERCANSEESERIALIZER_FIELDS, USERCANSEEEXTRASERIALIZER_FIELDS from .serializers import USERCANSEESERIALIZER_FIELDS, USERCANSEEEXTRASERIALIZER_FIELDS
if user.has_perm('users.can_manage'): NO_DATA = 0
data = full_data LITTLE_DATA = 1
else: MANY_DATA = 2
FULL_DATA = 3
# Check user permissions.
if user.has_perm('users.can_see_name'):
if user.has_perm('users.can_see_extra_data'): if user.has_perm('users.can_see_extra_data'):
if user.has_perm('users.can_manage'):
case = FULL_DATA
else:
case = MANY_DATA
else:
case = LITTLE_DATA
else:
case = NO_DATA
# Setup data.
if case == FULL_DATA:
data = full_data
elif case == NO_DATA:
data = None
else:
# case in (LITTLE_DATA, ḾANY_DATA)
if case == MANY_DATA:
fields = USERCANSEEEXTRASERIALIZER_FIELDS fields = USERCANSEEEXTRASERIALIZER_FIELDS
else: else:
# case == LITTLE_DATA
fields = USERCANSEESERIALIZER_FIELDS fields = USERCANSEESERIALIZER_FIELDS
# Let only some fields pass this method. # Let only some fields pass this method.
data = {} data = {}
@ -46,3 +68,17 @@ class UserAccessPermissions(BaseAccessPermissions):
if key in fields: if key in fields:
data[key] = full_data[key] data[key] = full_data[key]
return data return data
def get_projector_data(self, full_data):
"""
Returns the restricted serialized data for the instance prepared
for the projector. Removes several fields.
"""
from .serializers import USERCANSEESERIALIZER_FIELDS
# Let only some fields pass this method.
data = {}
for key in full_data.keys():
if key in USERCANSEESERIALIZER_FIELDS:
data[key] = full_data[key]
return data

View File

@ -207,14 +207,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
# Return result # Return result
return name return name
def get_view_class(self):
"""
Returns the main view class (viewset class) that should be unlocked
if the user (means its name) appears on a slide.
"""
from .views import UserViewSet
return UserViewSet
def get_search_index_string(self): def get_search_index_string(self):
""" """
Returns a string that can be indexed for the search. Returns a string that can be indexed for the search.

View File

@ -1,12 +1,11 @@
from ..core.exceptions import ProjectorException from ..core.exceptions import ProjectorException
from ..utils.projector import ProjectorElement, ProjectorRequirement from ..utils.projector import ProjectorElement
from .models import User from .models import User
from .views import GroupViewSet, UserViewSet
class UserSlide(ProjectorElement): class UserSlide(ProjectorElement):
""" """
Slide definitions for user model. Slide definitions for User model.
""" """
name = 'users/user' name = 'users/user'
@ -15,20 +14,10 @@ class UserSlide(ProjectorElement):
raise ProjectorException('User does not exist.') raise ProjectorException('User does not exist.')
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
pk = config_entry.get('id')
if pk is not None:
try: try:
user = User.objects.get(pk=pk) user = User.objects.get(pk=config_entry.get('id'))
except User.DoesNotExist: except User.DoesNotExist:
# User does not exist. Just do nothing. # User does not exist. Just do nothing.
pass pass
else: else:
yield ProjectorRequirement( yield user
view_class=UserViewSet,
view_action='retrieve',
pk=str(user.pk))
for group in user.groups.all():
yield ProjectorRequirement(
view_class=GroupViewSet,
view_action='retrieve',
pk=str(group.pk))

View File

@ -11,7 +11,6 @@ from ..utils.rest_api import (
) )
from .models import Group, User from .models import Group, User
USERCANSEESERIALIZER_FIELDS = ( USERCANSEESERIALIZER_FIELDS = (
'id', 'id',
'username', 'username',

View File

@ -21,7 +21,6 @@ angular.module('OpenSlidesApp.users.projector', ['OpenSlidesApp.users'])
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var id = $scope.element.id;
User.find(id);
User.bindOne(id, $scope, 'user'); User.bindOne(id, $scope, 'user');
} }
]); ]);

View File

@ -37,9 +37,9 @@ class UserViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
if self.action == 'retrieve': if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().can_retrieve(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'list', 'update', 'partial_update'): elif self.action in ('metadata', 'update', 'partial_update'):
result = self.request.user.has_perm('users.can_see_name') result = self.request.user.has_perm('users.can_see_name')
elif self.action in ('create', 'destroy', 'reset_password'): elif self.action in ('create', 'destroy', 'reset_password'):
result = (self.request.user.has_perm('users.can_see_name') and result = (self.request.user.has_perm('users.can_see_name') and

View File

@ -33,7 +33,7 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
if not cls.__name__ == 'BaseAccessPermissions': if not cls.__name__ == 'BaseAccessPermissions':
return cls.__name__ return cls.__name__
def can_retrieve(self, user): def check_permissions(self, user):
""" """
Returns True if the user has read access to model instances. Returns True if the user has read access to model instances.
""" """
@ -65,13 +65,21 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
if the user has limited access. Default: Returns full data if the if the user has limited access. Default: Returns full data if the
user has read access to model instances. user has read access to model instances.
Hint: You should override this method if your Hint: You should override this method if your get_serializer_class()
get_serializer_class() method may return different serializer for method returns different serializers for different users or if you
different users or if you have access restrictions in your view or have access restrictions in your view or viewset in methods like
viewset in methods like retrieve() or check_object_permissions(). retrieve(), list() or check_object_permissions().
""" """
if self.can_retrieve(user): if self.check_permissions(user):
data = full_data data = full_data
else: else:
data = None data = None
return data return data
def get_projector_data(self, full_data):
"""
Returns the serialized data for the projector. Returns None if has no
access to this specific data. Returns reduced data if the user has
limited access. Default: Returns full data.
"""
return full_data

View File

@ -4,13 +4,14 @@ import json
from asgiref.inmemory import ChannelLayer from asgiref.inmemory import ChannelLayer
from channels import Channel, Group from channels import Channel, Group
from channels.auth import channel_session_user, channel_session_user_from_http from channels.auth import channel_session_user, channel_session_user_from_http
from django.apps import apps
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from ..core.config import config
from ..core.models import Projector
from ..users.auth import AnonymousUser from ..users.auth import AnonymousUser
from ..users.models import User from ..users.models import User
from .access_permissions import BaseAccessPermissions from .collection import CollectionElement
def get_logged_in_users(): def get_logged_in_users():
@ -22,37 +23,23 @@ def get_logged_in_users():
return User.objects.exclude(session=None).filter(session__expire_date__gte=timezone.now()).distinct() return User.objects.exclude(session=None).filter(session__expire_date__gte=timezone.now()).distinct()
def get_model_from_collection_string(collection_string): def get_projector_element_data(projector):
""" """
Returns a model class which belongs to the argument collection_string. Returns a list of dicts that are required for a specific projector.
"""
def model_generator():
"""
Yields all models of all apps.
"""
for app_config in apps.get_app_configs():
for model in app_config.get_models():
yield model
for model in model_generator(): The argument projector has to be a projector instance.
try: """
model_collection_string = model.get_collection_string() output = []
except AttributeError: for requirement in projector.get_all_requirements():
# Skip models which do not have the method get_collection_string. required_collection_element = CollectionElement.from_instance(requirement)
pass element_dict = required_collection_element.as_autoupdate_for_projector()
else: if element_dict is not None:
if model_collection_string == collection_string: output.append(element_dict)
# The model was found. return output
break
else:
# No model was found in all apps.
raise ValueError('Invalid message. A valid collection_string is missing.')
return model
# Connected to websocket.connect
@channel_session_user_from_http @channel_session_user_from_http
def ws_add(message): def ws_add_site(message):
""" """
Adds the websocket connection to a group specific to the connecting user. Adds the websocket connection to a group specific to the connecting user.
@ -61,45 +48,107 @@ def ws_add(message):
Group('user-{}'.format(message.user.id)).add(message.reply_channel) Group('user-{}'.format(message.user.id)).add(message.reply_channel)
# Connected to websocket.disconnect
@channel_session_user @channel_session_user
def ws_disconnect(message): def ws_disconnect_site(message):
"""
This function is called, when a client on the site disconnects.
"""
Group('user-{}'.format(message.user.id)).discard(message.reply_channel) Group('user-{}'.format(message.user.id)).discard(message.reply_channel)
@channel_session_user_from_http
def ws_add_projector(message, projector_id):
"""
Adds the websocket connection to a group specific to the projector with the given id.
Also sends all data that are shown on the projector.
"""
user = message.user
# user is the django anonymous user. We have our own.
if user.is_anonymous:
user = AnonymousUser()
if not user.has_perm('core.can_see_projector'):
message.reply_channel.send({'text': 'No permissions to see this projector.'})
else:
try:
projector = Projector.objects.get(pk=projector_id)
except Projector.DoesNotExist:
message.reply_channel.send({'text': 'The projector {} does not exist.'.format(projector_id)})
else:
# At first, the client is added to the projector group, so it is
# informed if the data change.
Group('projector-{}'.format(projector_id)).add(message.reply_channel)
# Send all elements that are on the projector.
output = get_projector_element_data(projector)
# Send all config elements.
for key, value in config.items():
output.append({
'collection': config.get_collection_string(),
'id': key,
'action': 'changed',
'data': {'key': key, 'value': value}})
# Send the projector instance.
collection_element = CollectionElement.from_instance(projector)
output.append(collection_element.as_autoupdate_for_projector())
# Send all the data that was only collected before
message.reply_channel.send({'text': json.dumps(output)})
def ws_disconnect_projector(message, projector_id):
"""
This function is called, when a client on the projector disconnects.
"""
Group('projector-{}'.format(projector_id)).discard(message.reply_channel)
def send_data(message): def send_data(message):
""" """
Informs all users about changed data. Informs all users about changed data.
The argument message has to be a dict with the keywords collection_string
(string), pk (positive integer), id_deleted (boolean) and dispatch_uid
(string).
""" """
for access_permissions in BaseAccessPermissions.get_all(): collection_element = CollectionElement.from_values(**message)
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. # Loop over all logged in users and the anonymous user.
for user in itertools.chain(get_logged_in_users(), [AnonymousUser()]): for user in itertools.chain(get_logged_in_users(), [AnonymousUser()]):
channel = Group('user-{}'.format(user.id)) channel = Group('user-{}'.format(user.id))
output = { output = collection_element.as_autoupdate_for_user(user)
'collection': message['collection_string'], if output is None:
'id': message['pk'], # == 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. # There are no data for the user so he can't see the object. Skip him.
continue continue
output['data'] = data channel.send({'text': json.dumps([output])})
channel.send({'text': json.dumps(output)})
# Get the projector elements where data have to be sent and if whole projector
# has to be updated.
if collection_element.collection_string == config.get_collection_string():
# Config elements are always send to each projector
projectors = Projector.objects.all()
send_all = None # The decission is done later
elif collection_element.collection_string == Projector.get_collection_string():
# Update a projector, when the projector element is updated.
projectors = [collection_element.get_instance()]
send_all = True
elif collection_element.is_deleted():
projectors = Projector.objects.all()
send_all = False
else:
# Other elements are only send to the projector they are currently shown
projectors = Projector.get_projectors_that_show_this(message)
send_all = None # The decission is done later
for projector in projectors:
if send_all is None:
send_all = projector.need_full_update_for(message)
if send_all:
output = get_projector_element_data(projector)
else:
output = []
output.append(collection_element.as_autoupdate_for_projector())
if output:
Group('projector-{}'.format(projector.pk)).send(
{'text': json.dumps(output)})
def inform_changed_data(instance, is_deleted=False): def inform_changed_data(instance, is_deleted=False):
@ -109,23 +158,20 @@ def inform_changed_data(instance, is_deleted=False):
# Instance has no method get_root_rest_element. Just ignore it. # Instance has no method get_root_rest_element. Just ignore it.
pass pass
else: else:
message_dict = { collection_element = CollectionElement.from_instance(
'collection_string': root_instance.get_collection_string(), root_instance,
'pk': root_instance.pk, is_deleted=is_deleted and instance == root_instance)
'is_deleted': is_deleted and instance == root_instance,
'dispatch_uid': root_instance.get_access_permissions().get_dispatch_uid(),
}
# If currently there is an open database transaction, then the following # If currently there is an open database transaction, then the following
# function is only called, when the transaction is commited. If there # function is only called, when the transaction is commited. If there
# is currently no transaction, then the function is called immediately. # is currently no transaction, then the function is called immediately.
def send_autoupdate(message): def send_autoupdate():
try: try:
Channel('autoupdate.send_data').send(message) Channel('autoupdate.send_data').send(collection_element.as_channels_message())
except ChannelLayer.ChannelFull: except ChannelLayer.ChannelFull:
pass pass
transaction.on_commit(lambda: send_autoupdate(message_dict)) transaction.on_commit(send_autoupdate)
def inform_changed_data_receiver(sender, instance, **kwargs): def inform_changed_data_receiver(sender, instance, **kwargs):

View File

@ -0,0 +1,154 @@
from django.apps import apps
class CollectionElement:
@classmethod
def from_instance(cls, instance, is_deleted=False):
"""
Returns a collection element from a database instance.
"""
return cls(instance=instance, is_deleted=is_deleted)
@classmethod
def from_values(cls, collection_string, id, is_deleted=False):
"""
Returns a collection element from a collection_string and an id.
"""
return cls(collection_string=collection_string, id=id, is_deleted=is_deleted)
def __init__(self, instance=None, is_deleted=False, collection_string=None, id=None):
"""
Do not use this. Use the methods from_instance() or from_values().
"""
if instance is not None:
self.collection_string = instance.get_collection_string()
self.id = instance.pk
elif collection_string is not None and id is not None:
self.collection_string = collection_string
self.id = id
else:
raise RuntimeError(
'Invalid state. Use CollectionElement.from_instance() or '
'CollectionElement.from_values() but not CollectionElement() '
'directly.')
self.instance = instance
self.deleted = is_deleted
def as_channels_message(self):
"""
Returns a dictonary that can be used to send the object through the
channels system.
"""
return {
'collection_string': self.collection_string,
'id': self.id,
'is_deleted': self.is_deleted()}
def as_autoupdate_for_user(self, user):
"""
Returns a dict that can be sent through the autoupdate system for a site
user.
Returns None if the user can not see the element.
"""
output = {
'collection': self.collection_string,
'id': self.id,
'action': 'deleted' if self.is_deleted() else 'changed',
}
if not self.is_deleted():
data = self.get_access_permissions().get_restricted_data(
self.get_full_data(), user)
if data is None:
# The user is not allowed to see this element. Reset output to None.
output = None
else:
output['data'] = data
return output
def as_autoupdate_for_projector(self):
"""
Returns a dict that can be sent through the autoupdate system for the
projector.
Returns None if the projector can not see the element.
"""
output = {
'collection': self.collection_string,
'id': self.id,
'action': 'deleted' if self.is_deleted() else 'changed',
}
if not self.is_deleted():
data = self.get_access_permissions().get_projector_data(
self.get_full_data())
if data is None:
# The user is not allowed to see this element. Reset output to None.
output = None
else:
output['data'] = data
return output
def get_model(self):
"""
Returns the django model that is used for this collection.
"""
return get_model_from_collection_string(self.collection_string)
def get_instance(self):
"""
Returns the instance as django object.
May raise a DoesNotExist exception.
"""
if self.is_deleted():
raise RuntimeError("The collection element is deleted.")
if self.instance is None:
self.instance = self.get_model().objects.get(pk=self.id)
return self.instance
def get_access_permissions(self):
"""
Returns the get_access_permissions object for the this collection element.
"""
return self.get_model().get_access_permissions()
def get_full_data(self):
"""
Returns the full_data of this collection_element from with all other
dics can be generated.
"""
return self.get_access_permissions().get_full_data(self.get_instance())
def is_deleted(self):
"""
Returns Ture if the item is marked as deleted.
"""
return self.deleted
def get_model_from_collection_string(collection_string):
"""
Returns a model class which belongs to the argument collection_string.
"""
def model_generator():
"""
Yields all models of all apps.
"""
for app_config in apps.get_app_configs():
for model in app_config.get_models():
yield model
for model in model_generator():
try:
model_collection_string = model.get_collection_string()
except AttributeError:
# Skip models which do not have the method get_collection_string.
pass
else:
if model_collection_string == collection_string:
# The model was found.
break
else:
# No model was found in all apps.
raise ValueError('Invalid message. A valid collection_string is missing.')
return model

View File

@ -31,12 +31,13 @@ class RESTModelMixin:
""" """
return self return self
def get_access_permissions(self): @classmethod
def get_access_permissions(cls):
""" """
Returns a container to handle access permissions for this model and Returns a container to handle access permissions for this model and
its corresponding viewset. its corresponding viewset.
""" """
return self.access_permissions return cls.access_permissions
@classmethod @classmethod
def get_collection_string(cls): def get_collection_string(cls):

View File

@ -68,36 +68,7 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
def get_requirements(self, config_entry): def get_requirements(self, config_entry):
""" """
Returns an iterable of ProjectorRequirement instances to setup Returns an iterable of instances that are required for this projector
which views should be accessable for projector clients if the element. The config_entry has to be given.
projector element is active. The config_entry has to be given.
""" """
return () return ()
class ProjectorRequirement:
"""
Container for required views. Such a view is defined by its class, its
action and its kwargs which come from the URL path.
"""
def __init__(self, view_class, view_action, **kwargs):
self.view_class = view_class
self.view_action = view_action
self.kwargs = kwargs
def is_currently_required(self, view_instance):
"""
Returns True if the view_instance matches the initiated data of this
requirement.
"""
if not type(view_instance) == self.view_class:
result = False
elif not view_instance.action == self.view_action:
result = False
else:
result = True
for key in view_instance.kwargs:
if not self.kwargs[key] == view_instance.kwargs[key]:
result = False
break
return result

View File

@ -95,9 +95,9 @@ class PermissionMixin:
""" """
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet. Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
The methods check_view_permissions or check_projector_requirements are The method check_view_permissions is evaluated. If it returns False
evaluated. If both return False self.permission_denied() is called. self.permission_denied() is called. Django REST Framework's permission
Django REST Framework's permission system is disabled. system is disabled.
Also connects container to handle access permissions for model and Also connects container to handle access permissions for model and
viewset. viewset.
@ -106,12 +106,12 @@ class PermissionMixin:
def get_permissions(self): def get_permissions(self):
""" """
Overridden method to check view and projector permissions. Returns an Overridden method to check view permissions. Returns an empty
empty iterable so Django REST framework won't do any other iterable so Django REST framework won't do any other permission
permission checks by evaluating Django REST framework style permission checks by evaluating Django REST framework style permission classes
classes and the request passes. and the request passes.
""" """
if not self.check_view_permissions() and not self.check_projector_requirements(): if not self.check_view_permissions():
self.permission_denied(self.request) self.permission_denied(self.request)
return () return ()
@ -120,25 +120,11 @@ class PermissionMixin:
Override this and return True if the requesting user should be able to Override this and return True if the requesting user should be able to
get access to your view. get access to your view.
Use access permissions container for retrieve requests. Don't forget to use access permissions container for list and retrieve
requests.
""" """
return False return False
def check_projector_requirements(self):
"""
Helper method which returns True if the current request (on this
view instance) is required for at least one active projector element.
"""
from openslides.core.models import Projector
result = False
if self.request.user.has_perm('core.can_see_projector'):
for requirement in Projector.get_all_requirements():
if requirement.is_currently_required(view_instance=self):
result = True
break
return result
def get_access_permissions(self): def get_access_permissions(self):
""" """
Returns a container to handle access permissions for this viewset and Returns a container to handle access permissions for this viewset and

View File

@ -6,7 +6,6 @@ import os
from openslides.global_settings import * # noqa from openslides.global_settings import * # noqa
# Path to the directory for user specific data files # Path to the directory for user specific data files
OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))

View File

@ -6,7 +6,6 @@ import os
from openslides.global_settings import * # noqa from openslides.global_settings import * # noqa
# Path to the directory for user specific data files # Path to the directory for user specific data files
OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))