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.
Core:
- Used Django Channels instead of Tornado.
- Added support for big assemblies with lots of users.
- Added HTML support for messages on the projector.
@ -38,6 +37,7 @@ Other:
- Removed config cache to support multiple threads or processes.
- 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).
- Used Django Channels instead of Tornado. Refactoring of the autoupdate process.
Version 2.0 (2016-04-18)

View File

@ -5,7 +5,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
"""
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.
"""
@ -19,15 +19,28 @@ class ItemAccessPermissions(BaseAccessPermissions):
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):
"""
Returns the restricted serialized data for the instance prepared
for the user.
"""
if (self.can_retrieve(user) and
if (user.has_perm('agenda.can_see') and
(not full_data['is_hidden'] or
user.has_perm('agenda.can_see_hidden_items'))):
data = full_data
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 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 openslides.utils.projector import ProjectorElement, ProjectorRequirement
from ..core.config import config
from ..core.exceptions import ProjectorException
from ..utils.projector import ProjectorElement
from .models import Item
from .views import ItemViewSet
class ItemListSlide(ProjectorElement):
"""
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
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.')
def get_requirements(self, config_entry):
pk = config_entry.get('id', 'tree')
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')
yield from Item.objects.all()
class ListOfSpeakersSlide(ProjectorElement):
@ -49,10 +37,7 @@ class ListOfSpeakersSlide(ProjectorElement):
name = 'agenda/list-of-speakers'
def check_data(self):
pk = self.config_entry.get('id')
if pk is None:
raise ProjectorException('Id must not be None.')
if not Item.objects.filter(pk=pk).exists():
if not Item.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Item does not exist.')
def get_requirements(self, config_entry):
@ -65,12 +50,12 @@ class ListOfSpeakersSlide(ProjectorElement):
# Item does not exist. Just do nothing.
pass
else:
yield ProjectorRequirement(
view_class=ItemViewSet,
view_action='retrieve',
pk=str(item.pk))
for speaker in item.speakers.all():
yield ProjectorRequirement(
view_class=speaker.user.get_view_class(),
view_action='retrieve',
pk=str(speaker.user_id))
yield item
for speaker in item.speakers.filter(end_time=None):
# Yield current speaker and next speakers
yield speaker.user
query = (item.speakers.exclude(end_time=None)
.order_by('-end_time')[:config['agenda_show_last_speakers']])
for speaker in query:
# Yield last speakers
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
django.db.models.signals.post_save during app loading.
"""
if created and hasattr(instance, 'get_agenda_title'):
Item.objects.create(content_object=instance)
inform_changed_data(instance)
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)
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):

View File

@ -25,8 +25,6 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Agenda.find(id);
User.findAll();
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.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
Agenda.findAll();
// Bind agenda tree to the scope
$scope.$watch(function () {
return Agenda.lastModified();

View File

@ -41,9 +41,9 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'manage_speaker', 'tree'):
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'manage_speaker', 'tree'):
result = self.request.user.has_perm('agenda.can_see')
# For manage_speaker and tree requests the rest of the check is
# 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
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'):
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.
"""
# TODO: Move this logic to access_permissions.ItemAccessPermissions.
queryset = super().get_queryset()
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()]

View File

@ -5,7 +5,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
"""
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.
"""
@ -17,7 +17,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
"""
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
else:
serializer_class = AssignmentShortSerializer
@ -29,9 +29,20 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
for the user. Removes unpublished polls for non admins so that they
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
else:
elif user.has_perm('assignments.can_see'):
data = full_data.copy()
data['polls'] = [poll for poll in data['polls'] if poll['published']]
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['polls'] = [poll for poll in data['polls'] if poll['published']]
return data

View File

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

View File

@ -24,16 +24,10 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment
var id = $scope.element.id;
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.getPhases().then(function(phases) {
$scope.phases = phases;
});
// load all users
User.findAll();
User.bindAll({}, $scope, 'users');
}
]);

View File

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

View File

@ -52,10 +52,11 @@ class AssignmentViewSet(ModelViewSet):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
result = self.request.user.has_perm('assignments.can_see')
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata':
# Everybody is allowed to see the metadata.
result = True
elif self.action in ('create', 'partial_update', 'update', 'destroy',
'mark_elected', 'create_poll'):
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.
"""
def can_retrieve(self, user):
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
@ -24,7 +24,7 @@ class CustomSlideAccessPermissions(BaseAccessPermissions):
"""
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.
"""
@ -43,7 +43,7 @@ class TagAccessPermissions(BaseAccessPermissions):
"""
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.
"""
@ -66,7 +66,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
"""
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.
"""
@ -88,7 +88,7 @@ class ConfigAccessPermissions(BaseAccessPermissions):
Access permissions container for the config (ConfigStore and
ConfigViewSet).
"""
def can_retrieve(self, user):
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""

View File

@ -141,6 +141,12 @@ class ConfigHandler:
if config_variable.translatable:
yield config_variable.name
def get_collection_string(self):
"""
Returns the collection_string from the CollectionStore.
"""
return ConfigStore.get_collection_string()
config = ConfigHandler()
"""
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 openslides.core.config import ConfigVariable

View File

@ -111,11 +111,9 @@ class Projector(RESTModelMixin, models.Model):
result[key]['error'] = str(e)
return result
@classmethod
def get_all_requirements(cls):
def get_all_requirements(self):
"""
Generator which returns all ProjectorRequirement instances of all
active projector elements.
Generator which returns all instances that are shown on this projector.
"""
# Get all elements from all apps.
elements = {}
@ -123,12 +121,38 @@ class Projector(RESTModelMixin, models.Model):
elements[element.name] = element
# Generator
for key, value in self.config.items():
element = elements.get(value['name'])
if element is not None:
yield from element.get_requirements(value)
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():
for key, value in projector.config.items():
element = elements.get(value['name'])
if element is not None:
for requirement in element.get_requirements(value):
yield requirement
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):

View File

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

View File

@ -38,11 +38,22 @@ angular.module('OpenSlidesApp.core', [
.factory('autoupdate', [
'DS',
'$rootScope',
function (DS, $rootScope) {
'REALM',
function (DS, $rootScope, REALM) {
var socket = null;
var recInterval = null;
$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 = {
messageReceivers: [],
onMessage: function (receiver) {
@ -55,7 +66,7 @@ angular.module('OpenSlidesApp.core', [
}
};
var newConnect = function () {
socket = new WebSocket('ws://' + location.host + '/ws/');
socket = new WebSocket('ws://' + location.host + websocketPath);
clearInterval(recInterval);
socket.onopen = function () {
$rootScope.connected = true;
@ -175,49 +186,67 @@ angular.module('OpenSlidesApp.core', [
autoupdate.onMessage(function(json) {
// TODO: when MODEL.find() is called after this
// a new request is fired. This could be a bug in DS
var data = JSON.parse(json);
console.log("Received object: " + data.collection + ", " + data.id);
var instance = DS.get(data.collection, data.id);
if (data.action == 'changed') {
if (instance) {
// The instance is in the local db
dsEject(data.collection, instance);
// TODO: If you don't have the permission to see a projector, the
// 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);
var instance = DS.get(data.collection, data.id);
if (data.action == 'changed') {
if (instance) {
// The instance is in the local db
dsEject(data.collection, instance);
}
DS.inject(data.collection, data.data);
} else if (data.action == 'deleted') {
if (instance) {
// The instance is in the local db
dsEject(data.collection, instance);
}
DS.eject(data.collection, data.id);
}
DS.inject(data.collection, data.data);
} else if (data.action == 'deleted') {
if (instance) {
// The instance is in the local db
dsEject(data.collection, instance);
}
DS.eject(data.collection, data.id);
}
// If you want to handle more status codes, change server
// restrictions in utils/autoupdate.py.
});
});
}
])
.factory('loadGlobalData', [
'$rootScope',
// Save the server time to the rootscope.
.run([
'$http',
'$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',
'$rootScope',
function (Config, $rootScope) {
$rootScope.config = function (key) {
try {
return Config.get(key).value;
}
catch(err) {
return '';
}
};
}
])
.factory('loadGlobalData', [
'ChatMessage',
'Config',
'Projector',
function ($rootScope, $http, ChatMessage, Config, Projector) {
function (ChatMessage, Config, Projector) {
return function () {
// Puts the config object into each scope.
Config.findAll().then(function() {
$rootScope.config = function(key) {
try {
return Config.get(key).value;
}
catch(err) {
console.log("Unkown config key: " + key);
return '';
}
};
});
Config.findAll();
// Loads all projector data
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

View File

@ -5,6 +5,9 @@
// The core module for the OpenSlides projector
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('slides', [
function() {
@ -26,7 +29,7 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
element.template = slidesMap[element.name].template;
elements.push(element);
} else {
console.log("Unknown slide: " + element.name);
console.error("Unknown slide: " + element.name);
}
});
return elements;
@ -61,7 +64,9 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('ProjectorContainerCtrl', [
'$scope',
'Config',
function($scope, Config) {
'loadGlobalData',
function($scope, Config, loadGlobalData) {
loadGlobalData();
// watch for changes in Config
var last_conf;
$scope.$watch(function () {
@ -123,21 +128,25 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
'Projector',
'slides',
function($scope, Projector, slides) {
Projector.find(1).then(function() {
$scope.$watch(function () {
return Projector.lastModified(1);
}, function () {
$scope.$watch(function () {
// TODO: Use the current projector. At the moment there is only one.
return Projector.lastModified(1);
}, function () {
// TODO: Use the current projector. At the moment there is only one
var projector = Projector.get(1);
if (projector) {
$scope.elements = [];
_.forEach(slides.getElements(Projector.get(1)), function(element) {
_.forEach(slides.getElements(projector), function(element) {
if (!element.error) {
$scope.elements.push(element);
} else {
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.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
// class.
var id = $scope.element.id;
Customslide.find(id).then(function(customslide) {
Customslide.loadRelations(customslide, 'agenda_item');
});
Customslide.bindOne(id, $scope, 'customslide');
}
])

View File

@ -18,6 +18,10 @@ angular.module('OpenSlidesApp.core.site', [
'ui.tinymce',
'luegg.directives',
])
// Can be used to find out if the projector or the side is used
.constant('REALM', 'site')
.factory('PdfMakeDocumentProvider', [
'gettextCatalog',
'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.
.factory('Editor', [
'gettextCatalog',

View File

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

View File

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

View File

@ -5,7 +5,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
"""
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.
"""

View File

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

View File

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

View File

@ -19,9 +19,9 @@ class MediafileViewSet(ModelViewSet):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata':
result = self.request.user.has_perm('mediafiles.can_see')
elif self.action == 'create':
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.
"""
def can_retrieve(self, user):
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
@ -43,6 +43,21 @@ class MotionAccessPermissions(BaseAccessPermissions):
pass
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):
"""
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.
"""
def can_retrieve(self, user):
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
@ -129,7 +144,7 @@ class WorkflowAccessPermissions(BaseAccessPermissions):
"""
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.
"""

View File

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

View File

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

View File

@ -60,14 +60,6 @@ angular.module('OpenSlidesApp.motions', [
}
])
// Load all MotionWorkflows at startup
.run([
'Workflow',
function (Workflow) {
Workflow.findAll();
}
])
.factory('MotionPoll', [
'DS',
'gettextCatalog',
@ -452,7 +444,8 @@ angular.module('OpenSlidesApp.motions', [
.run([
'Motion',
'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
// class.
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');
// load all users
User.findAll();
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)
.factory('MotionForm', [
'gettextCatalog',

View File

@ -71,7 +71,7 @@
<!-- Text -->
<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 -->
<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.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'partial_update', 'update'):
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'partial_update', 'update'):
result = self.request.user.has_perm('motions.can_see')
# For partial_update and update requests the rest of the check is
# done in the update method. See below.
@ -373,9 +373,9 @@ class CategoryViewSet(ModelViewSet):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata':
result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy', 'numbering'):
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.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list'):
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata':
result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
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 = [
route("websocket.connect", ws_add, path='/ws/'),
route("websocket.disconnect", ws_disconnect),
include(projector_routing, path=r'^/ws/projector/(?P<projector_id>\d+)/$'),
include(site_routing, path=r'^/ws/site/$'),
route("autoupdate.send_data", send_data),
]

View File

@ -5,7 +5,7 @@ class UserAccessPermissions(BaseAccessPermissions):
"""
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.
"""
@ -33,12 +33,34 @@ class UserAccessPermissions(BaseAccessPermissions):
"""
from .serializers import USERCANSEESERIALIZER_FIELDS, USERCANSEEEXTRASERIALIZER_FIELDS
if user.has_perm('users.can_manage'):
data = full_data
else:
NO_DATA = 0
LITTLE_DATA = 1
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_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
else:
# case == LITTLE_DATA
fields = USERCANSEESERIALIZER_FIELDS
# Let only some fields pass this method.
data = {}
@ -46,3 +68,17 @@ class UserAccessPermissions(BaseAccessPermissions):
if key in fields:
data[key] = full_data[key]
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 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):
"""
Returns a string that can be indexed for the search.

View File

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

View File

@ -11,7 +11,6 @@ from ..utils.rest_api import (
)
from .models import Group, User
USERCANSEESERIALIZER_FIELDS = (
'id',
'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
// class.
var id = $scope.element.id;
User.find(id);
User.bindOne(id, $scope, 'user');
}
]);

View File

@ -37,9 +37,9 @@ class UserViewSet(ModelViewSet):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'update', 'partial_update'):
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'update', 'partial_update'):
result = self.request.user.has_perm('users.can_see_name')
elif self.action in ('create', 'destroy', 'reset_password'):
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':
return cls.__name__
def can_retrieve(self, user):
def check_permissions(self, user):
"""
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
user has read access to model instances.
Hint: You should override this method if your
get_serializer_class() method may return different serializer for
different users or if you have access restrictions in your view or
viewset in methods like retrieve() or check_object_permissions().
Hint: You should override this method if your get_serializer_class()
method returns different serializers for different users or if you
have access restrictions in your view or viewset in methods like
retrieve(), list() or check_object_permissions().
"""
if self.can_retrieve(user):
if self.check_permissions(user):
data = full_data
else:
data = None
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 channels import Channel, Group
from channels.auth import channel_session_user, channel_session_user_from_http
from django.apps import apps
from django.db import transaction
from django.utils import timezone
from ..core.config import config
from ..core.models import Projector
from ..users.auth import AnonymousUser
from ..users.models import User
from .access_permissions import BaseAccessPermissions
from .collection import CollectionElement
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()
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.
The argument projector has to be a projector instance.
"""
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
output = []
for requirement in projector.get_all_requirements():
required_collection_element = CollectionElement.from_instance(requirement)
element_dict = required_collection_element.as_autoupdate_for_projector()
if element_dict is not None:
output.append(element_dict)
return output
# Connected to websocket.connect
@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.
@ -61,45 +48,107 @@ def ws_add(message):
Group('user-{}'.format(message.user.id)).add(message.reply_channel)
# Connected to websocket.disconnect
@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)
@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):
"""
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():
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)
collection_element = CollectionElement.from_values(**message)
# 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': 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.
continue
output['data'] = data
channel.send({'text': json.dumps(output)})
output = collection_element.as_autoupdate_for_user(user)
if output is None:
# There are no data for the user so he can't see the object. Skip him.
continue
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):
@ -109,23 +158,20 @@ def inform_changed_data(instance, is_deleted=False):
# Instance has no method get_root_rest_element. Just ignore it.
pass
else:
message_dict = {
'collection_string': root_instance.get_collection_string(),
'pk': root_instance.pk,
'is_deleted': is_deleted and instance == root_instance,
'dispatch_uid': root_instance.get_access_permissions().get_dispatch_uid(),
}
collection_element = CollectionElement.from_instance(
root_instance,
is_deleted=is_deleted and instance == root_instance)
# If currently there is an open database transaction, then the following
# function is only called, when the transaction is commited. If there
# is currently no transaction, then the function is called immediately.
def send_autoupdate(message):
def send_autoupdate():
try:
Channel('autoupdate.send_data').send(message)
Channel('autoupdate.send_data').send(collection_element.as_channels_message())
except ChannelLayer.ChannelFull:
pass
transaction.on_commit(lambda: send_autoupdate(message_dict))
transaction.on_commit(send_autoupdate)
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
def get_access_permissions(self):
@classmethod
def get_access_permissions(cls):
"""
Returns a container to handle access permissions for this model and
its corresponding viewset.
"""
return self.access_permissions
return cls.access_permissions
@classmethod
def get_collection_string(cls):

View File

@ -68,36 +68,7 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
def get_requirements(self, config_entry):
"""
Returns an iterable of ProjectorRequirement instances to setup
which views should be accessable for projector clients if the
projector element is active. The config_entry has to be given.
Returns an iterable of instances that are required for this projector
element. The config_entry has to be given.
"""
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.
The methods check_view_permissions or check_projector_requirements are
evaluated. If both return False self.permission_denied() is called.
Django REST Framework's permission system is disabled.
The method check_view_permissions is evaluated. If it returns False
self.permission_denied() is called. Django REST Framework's permission
system is disabled.
Also connects container to handle access permissions for model and
viewset.
@ -106,12 +106,12 @@ class PermissionMixin:
def get_permissions(self):
"""
Overridden method to check view and projector permissions. Returns an
empty iterable so Django REST framework won't do any other
permission checks by evaluating Django REST framework style permission
classes and the request passes.
Overridden method to check view permissions. Returns an empty
iterable so Django REST framework won't do any other permission
checks by evaluating Django REST framework style permission classes
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)
return ()
@ -120,25 +120,11 @@ class PermissionMixin:
Override this and return True if the requesting user should be able to
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
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):
"""
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
# Path to the directory for user specific data files
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
# Path to the directory for user specific data files
OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))