Merge pull request #1506 from normanjaeckel/DjangoRESTFrameworkProjector
Added REST API for projector. Introduced new projector API.
This commit is contained in:
commit
80588610f0
26
CHANGELOG
26
CHANGELOG
@ -11,33 +11,41 @@ Version 2.0.0 (unreleased)
|
|||||||
Agenda:
|
Agenda:
|
||||||
- Updated the tests and changed only small internal parts of method of the
|
- Updated the tests and changed only small internal parts of method of the
|
||||||
agenda model. No API changes.
|
agenda model. No API changes.
|
||||||
|
- Deprecated mptt.
|
||||||
Assignments:
|
Assignments:
|
||||||
- Massive refactoring and cleanup of assignments app.
|
- Renamed app from assignment to assignments.
|
||||||
- Renamed app from assignment to assignments
|
- Massive refactoring and cleanup of the app.
|
||||||
Motions:
|
Motions:
|
||||||
- Renamed app from motion to motions
|
- Renamed app from motion to motions.
|
||||||
Mediafiles:
|
Mediafiles:
|
||||||
- Renamed app from mediafile to mediafiles
|
- Renamed app from mediafile to mediafiles.
|
||||||
Users:
|
Users:
|
||||||
- Massive refactoring of the participant app. Now called 'users'.
|
- Massive refactoring of the participant app. Now called 'users'.
|
||||||
- Used new anonymous user object instead of an authentification backend.
|
- Used new anonymous user object instead of an authentification backend. Used
|
||||||
|
special authentication class for REST requests.
|
||||||
- Used authentication frontend via AngularJS.
|
- Used authentication frontend via AngularJS.
|
||||||
Other:
|
Other:
|
||||||
- New OpenSlides logo.
|
- New OpenSlides logo.
|
||||||
- Changed supported Python version to >= 3.3.
|
- Changed supported Python version to >= 3.3.
|
||||||
- Used Django 1.7 as lowest requirement.
|
- Used Django 1.7 as lowest requirement.
|
||||||
- Added Django's application configuration. Refactored loading of signals,
|
- Added Django's application configuration. Refactored loading of signals,
|
||||||
template signals and slides.
|
template signals and projector elements/slides.
|
||||||
- Added API using Django REST Framework 3.x. Added several views and mixins for
|
- Setup migrations.
|
||||||
generic views in OpenSlides apps.
|
- Added API using Django REST Framework 3.x. Added several views and mixins
|
||||||
|
for generic views in OpenSlides apps.
|
||||||
|
- Refactored projector API using metaclasses now.
|
||||||
|
- Renamed SignalConnectMetaClass classmethod get_all_objects to get_all
|
||||||
|
(private API).
|
||||||
- Used AngularJS with additional libraries for single page frontend.
|
- Used AngularJS with additional libraries for single page frontend.
|
||||||
- Removed use of 'django.views.i18n.javascript_catalog'.
|
- Removed use of 'django.views.i18n.javascript_catalog'. Used angular-gettext
|
||||||
|
now.
|
||||||
- Updated to Bootstrap 3.
|
- Updated to Bootstrap 3.
|
||||||
- Used SockJS for automatic update of AngularJS driven single page frontend.
|
- Used SockJS for automatic update of AngularJS driven single page frontend.
|
||||||
- Refactored start script and management commands.
|
- Refactored start script and management commands.
|
||||||
- Refactored tests.
|
- Refactored tests.
|
||||||
- Used Bower and gulp to manage third party JavaScript and Cascading Style
|
- Used Bower and gulp to manage third party JavaScript and Cascading Style
|
||||||
Sheets libraries.
|
Sheets libraries.
|
||||||
|
- Used setup.cfg for development tools.
|
||||||
- Fixed bug in LocalizedModelMultipleChoiceField.
|
- Fixed bug in LocalizedModelMultipleChoiceField.
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,27 +6,23 @@ class CoreAppConfig(AppConfig):
|
|||||||
verbose_name = 'OpenSlides Core'
|
verbose_name = 'OpenSlides Core'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# Load main menu entry and widgets.
|
# Load main menu entry, projector elements and widgets.
|
||||||
# Do this by just importing all from these files.
|
# Do this by just importing all from these files.
|
||||||
from . import main_menu, widgets # noqa
|
from . import main_menu, projector, widgets # noqa
|
||||||
|
|
||||||
# Import all required stuff.
|
# Import all required stuff.
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from openslides.config.signals import config_signal
|
from openslides.config.signals import config_signal
|
||||||
from openslides.projector.api import register_slide_model
|
|
||||||
from openslides.utils.autoupdate import inform_changed_data_receiver
|
from openslides.utils.autoupdate import inform_changed_data_receiver
|
||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from .signals import setup_general_config
|
from .signals import setup_general_config
|
||||||
from .views import CustomSlideViewSet, TagViewSet
|
from .views import CustomSlideViewSet, ProjectorViewSet, TagViewSet
|
||||||
|
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
config_signal.connect(setup_general_config, dispatch_uid='setup_general_config')
|
config_signal.connect(setup_general_config, dispatch_uid='setup_general_config')
|
||||||
|
|
||||||
# Register slides.
|
|
||||||
CustomSlide = self.get_model('CustomSlide')
|
|
||||||
register_slide_model(CustomSlide, 'core/customslide_slide.html')
|
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
|
router.register('core/projector', ProjectorViewSet)
|
||||||
router.register('core/customslide', CustomSlideViewSet)
|
router.register('core/customslide', CustomSlideViewSet)
|
||||||
router.register('core/tag', TagViewSet)
|
router.register('core/tag', TagViewSet)
|
||||||
|
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorException(OpenSlidesError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TagException(OpenSlidesError):
|
class TagException(OpenSlidesError):
|
||||||
pass
|
pass
|
||||||
|
75
openslides/core/migrations/0001_initial.py
Normal file
75
openslides/core/migrations/0001_initial.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import jsonfield.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
import openslides.utils.rest_api
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_projector(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Adds default projector, welcome slide and activates clock and welcome
|
||||||
|
slide.
|
||||||
|
"""
|
||||||
|
# We get the model from the versioned app registry;
|
||||||
|
# if we directly import it, it will be the wrong version.
|
||||||
|
CustomSlide = apps.get_model('core', 'CustomSlide')
|
||||||
|
custom_slide = CustomSlide.objects.create(
|
||||||
|
title='Welcome to OpenSlides',
|
||||||
|
weight=-500)
|
||||||
|
Projector = apps.get_model('core', 'Projector')
|
||||||
|
projector_config = [
|
||||||
|
{'name': 'core/clock'},
|
||||||
|
{'name': 'core/customslide', 'id': custom_slide.id}]
|
||||||
|
Projector.objects.create(config=projector_config)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomSlide',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
|
('title', models.CharField(verbose_name='Title', max_length=256)),
|
||||||
|
('text', models.TextField(verbose_name='Text', blank=True)),
|
||||||
|
('weight', models.IntegerField(verbose_name='Weight', default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('weight', 'title'),
|
||||||
|
},
|
||||||
|
bases=(openslides.utils.rest_api.RESTModelMixin, openslides.utils.models.AbsoluteUrlMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Projector',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
|
('config', jsonfield.fields.JSONField()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'permissions': (
|
||||||
|
('can_see_projector', 'Can see the projector'),
|
||||||
|
('can_manage_projector', 'Can manage the projector'),
|
||||||
|
('can_see_dashboard', 'Can see the dashboard'),
|
||||||
|
('can_use_chat', 'Can use the chat')),
|
||||||
|
},
|
||||||
|
bases=(openslides.utils.rest_api.RESTModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tag',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')),
|
||||||
|
('name', models.CharField(verbose_name='Tag', unique=True, max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'permissions': (('can_manage_tags', 'Can manage tags'),),
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
bases=(openslides.utils.rest_api.RESTModelMixin, openslides.utils.models.AbsoluteUrlMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
add_default_projector,
|
||||||
|
),
|
||||||
|
]
|
0
openslides/core/migrations/__init__.py
Normal file
0
openslides/core/migrations/__init__.py
Normal file
@ -1,62 +1,105 @@
|
|||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||||
# TODO: activate the following line after using the apploader
|
from jsonfield import JSONField
|
||||||
# from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from openslides.projector.models import SlideMixin
|
|
||||||
from openslides.utils.models import AbsoluteUrlMixin
|
from openslides.utils.models import AbsoluteUrlMixin
|
||||||
|
from openslides.utils.projector import ProjectorElement
|
||||||
from openslides.utils.rest_api import RESTModelMixin
|
from openslides.utils.rest_api import RESTModelMixin
|
||||||
|
|
||||||
# Imports the default user so that other apps can import it from here.
|
from .exceptions import ProjectorException
|
||||||
# TODO: activate this with the new apploader
|
|
||||||
# User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSlide(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
|
class Projector(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for Slides, only for the projector.
|
Model for all projectors. At the moment we support only one projector,
|
||||||
"""
|
the default projector (pk=1).
|
||||||
slide_callback_name = 'customslide'
|
|
||||||
|
|
||||||
title = models.CharField(max_length=256, verbose_name=ugettext_lazy('Title'))
|
If the config field is empty or invalid the projector shows a default
|
||||||
text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy('Text'))
|
slide. To activate a slide and extra projector elements, save valid
|
||||||
weight = models.IntegerField(default=0, verbose_name=ugettext_lazy('Weight'))
|
JSON to the config field.
|
||||||
|
|
||||||
|
Example: [{"name": "core/customslide", "id": 2},
|
||||||
|
{"name": "core/countdown", "countdown_time": 20, "status": "stop"},
|
||||||
|
{"name": "core/clock", "stable": true}]
|
||||||
|
|
||||||
|
This can be done using the REST API with POST requests on e. g. the URL
|
||||||
|
/rest/core/projector/1/activate_projector_elements/. The data have to be
|
||||||
|
a list of dictionaries. Every dictionary must have at least the
|
||||||
|
property "name". The property "stable" is to set whether this element
|
||||||
|
should disappear on prune or clear requests.
|
||||||
|
"""
|
||||||
|
config = JSONField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""
|
"""
|
||||||
General permissions that can not be placed at a specific app.
|
Contains general permissions that can not be placed in a specific app.
|
||||||
"""
|
"""
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_manage_projector', ugettext_noop('Can manage the projector')),
|
|
||||||
('can_see_projector', ugettext_noop('Can see the projector')),
|
('can_see_projector', ugettext_noop('Can see the projector')),
|
||||||
|
('can_manage_projector', ugettext_noop('Can manage the projector')),
|
||||||
('can_see_dashboard', ugettext_noop('Can see the dashboard')),
|
('can_see_dashboard', ugettext_noop('Can see the dashboard')),
|
||||||
('can_use_chat', ugettext_noop('Can use the chat')),
|
('can_use_chat', ugettext_noop('Can use the chat')))
|
||||||
)
|
|
||||||
|
@property
|
||||||
|
def projector_elements(self):
|
||||||
|
"""
|
||||||
|
A generator to retrieve all projector elements given in the config
|
||||||
|
field. For every element the method get_data() is called and its
|
||||||
|
result returned.
|
||||||
|
"""
|
||||||
|
elements = {}
|
||||||
|
for element in ProjectorElement.get_all():
|
||||||
|
elements[element.name] = element
|
||||||
|
for config_entry in self.config:
|
||||||
|
name = config_entry.get('name')
|
||||||
|
element = elements.get(name)
|
||||||
|
data = {'name': name}
|
||||||
|
if element is None:
|
||||||
|
data['error'] = _('Projector element does not exist.')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data.update(element.get_data(
|
||||||
|
projector_object=self,
|
||||||
|
config_entry=config_entry))
|
||||||
|
except ProjectorException as e:
|
||||||
|
data['error'] = str(e)
|
||||||
|
yield data
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSlide(RESTModelMixin, AbsoluteUrlMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for slides with custom content.
|
||||||
|
"""
|
||||||
|
title = models.CharField(
|
||||||
|
verbose_name=ugettext_lazy('Title'),
|
||||||
|
max_length=256)
|
||||||
|
text = models.TextField(
|
||||||
|
verbose_name=ugettext_lazy('Text'),
|
||||||
|
blank=True)
|
||||||
|
weight = models.IntegerField(
|
||||||
|
verbose_name=ugettext_lazy('Weight'),
|
||||||
|
default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('weight', 'title', )
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def get_absolute_url(self, link='update'):
|
|
||||||
if link == 'update':
|
|
||||||
url = reverse('customslide_update', args=[str(self.pk)])
|
|
||||||
elif link == 'delete':
|
|
||||||
url = reverse('customslide_delete', args=[str(self.pk)])
|
|
||||||
else:
|
|
||||||
url = super().get_absolute_url(link)
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(RESTModelMixin, AbsoluteUrlMixin, models.Model):
|
class Tag(RESTModelMixin, AbsoluteUrlMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model to save tags.
|
Model for tags. This tags can be used for other models like agenda items,
|
||||||
|
motions or assignments.
|
||||||
"""
|
"""
|
||||||
|
name = models.CharField(
|
||||||
name = models.CharField(max_length=255, unique=True,
|
verbose_name=ugettext_lazy('Tag'),
|
||||||
verbose_name=ugettext_lazy('Tag'))
|
max_length=255,
|
||||||
|
unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ('name',)
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_manage_tags', ugettext_noop('Can manage tags')), )
|
('can_manage_tags', ugettext_noop('Can manage tags')), )
|
||||||
|
|
||||||
|
81
openslides/core/projector.py
Normal file
81
openslides/core/projector.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from openslides.utils.projector import ProjectorElement
|
||||||
|
|
||||||
|
from .exceptions import ProjectorException
|
||||||
|
from .models import CustomSlide
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSlideSlide(ProjectorElement):
|
||||||
|
"""
|
||||||
|
Slide definitions for custom slide model.
|
||||||
|
"""
|
||||||
|
name = 'core/customslide'
|
||||||
|
scripts = 'core/customslide_slide.js'
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
pk = self.config_entry.get('id')
|
||||||
|
if not CustomSlide.objects.filter(pk=pk).exists():
|
||||||
|
raise ProjectorException(_('Custom slide does not exist.'))
|
||||||
|
return [{
|
||||||
|
'collection': 'core/customslide',
|
||||||
|
'id': pk}]
|
||||||
|
|
||||||
|
|
||||||
|
class Clock(ProjectorElement):
|
||||||
|
"""
|
||||||
|
Clock on the projector.
|
||||||
|
"""
|
||||||
|
name = 'core/clock'
|
||||||
|
scripts = 'core/clock.js'
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
return {'server_time': now().timestamp()}
|
||||||
|
|
||||||
|
|
||||||
|
class Countdown(ProjectorElement):
|
||||||
|
"""
|
||||||
|
Countdown on the projector.
|
||||||
|
|
||||||
|
To start the countdown write into the config field:
|
||||||
|
|
||||||
|
{
|
||||||
|
"countdown_time": <timestamp>,
|
||||||
|
"status": "go"
|
||||||
|
}
|
||||||
|
|
||||||
|
The timestamp is a POSIX timestamp (seconds) calculated from server
|
||||||
|
time, server time offset and countdown duration (countdown_time = now -
|
||||||
|
serverTimeOffset + duration).
|
||||||
|
|
||||||
|
To stop the countdown set the countdown time to the actual value of the
|
||||||
|
countdown (countdown_time = countdown_time - now + serverTimeOffset)
|
||||||
|
and set status to "stop".
|
||||||
|
|
||||||
|
To reset the countdown (it is not a reset in a functional way) just
|
||||||
|
change the countdown_time. The status value remain 'stop'.
|
||||||
|
|
||||||
|
To hide a running countdown add {"hidden": true}.
|
||||||
|
"""
|
||||||
|
name = 'core/countdown'
|
||||||
|
scripts = 'core/countdown.js'
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
if self.config_entry.get('countdown_time') is None:
|
||||||
|
raise ProjectorException(_('No countdown time given.'))
|
||||||
|
if self.config_entry.get('status') is None:
|
||||||
|
raise ProjectorException(_('No status given.'))
|
||||||
|
return {'server_time': now().timestamp()}
|
||||||
|
|
||||||
|
|
||||||
|
class Message(ProjectorElement):
|
||||||
|
"""
|
||||||
|
Short message on the projector. Rendered as overlay.
|
||||||
|
"""
|
||||||
|
name = 'core/message'
|
||||||
|
scripts = 'core/message.js'
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
if self.config_entry.get('message') is None:
|
||||||
|
raise ProjectorException(_('No message given.'))
|
@ -1,6 +1,39 @@
|
|||||||
from openslides.utils.rest_api import ModelSerializer
|
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
|
||||||
|
|
||||||
from .models import CustomSlide, Tag
|
from .models import CustomSlide, Projector, Tag
|
||||||
|
|
||||||
|
|
||||||
|
class JSONSerializerField(Field):
|
||||||
|
"""
|
||||||
|
Serializer for projector's JSONField.
|
||||||
|
"""
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
"""
|
||||||
|
Checks that data is a list of dictionaries. Every dictionary must have
|
||||||
|
a key 'name'.
|
||||||
|
"""
|
||||||
|
if type(data) is not list:
|
||||||
|
raise ValidationError('Data must be a list of dictionaries.')
|
||||||
|
for element in data:
|
||||||
|
if type(element) is not dict:
|
||||||
|
raise ValidationError('Data must be a list of dictionaries.')
|
||||||
|
elif element.get('name') is None:
|
||||||
|
raise ValidationError("Every dictionary must have a key 'name'.")
|
||||||
|
return data
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for core.models.Projector objects.
|
||||||
|
"""
|
||||||
|
config = JSONSerializerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Projector
|
||||||
|
fields = ('config', 'projector_elements', )
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideSerializer(ModelSerializer):
|
class CustomSlideSerializer(ModelSerializer):
|
||||||
@ -9,7 +42,7 @@ class CustomSlideSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomSlide
|
model = CustomSlide
|
||||||
fields = ('id', 'title', 'text', 'weight',)
|
fields = ('id', 'title', 'text', 'weight', )
|
||||||
|
|
||||||
|
|
||||||
class TagSerializer(ModelSerializer):
|
class TagSerializer(ModelSerializer):
|
||||||
@ -18,4 +51,4 @@ class TagSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('id', 'name',)
|
fields = ('id', 'name', )
|
||||||
|
@ -21,14 +21,24 @@ from openslides.utils.plugins import (
|
|||||||
get_plugin_verbose_name,
|
get_plugin_verbose_name,
|
||||||
get_plugin_version,
|
get_plugin_version,
|
||||||
)
|
)
|
||||||
from openslides.utils.rest_api import ModelViewSet
|
from openslides.utils.rest_api import (
|
||||||
|
ModelViewSet,
|
||||||
|
ReadOnlyModelViewSet,
|
||||||
|
Response,
|
||||||
|
ValidationError,
|
||||||
|
detail_route
|
||||||
|
)
|
||||||
from openslides.utils.signals import template_manipulation
|
from openslides.utils.signals import template_manipulation
|
||||||
from openslides.utils.widgets import Widget
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
from .exceptions import TagException
|
from .exceptions import TagException
|
||||||
from .forms import SelectWidgetsForm
|
from .forms import SelectWidgetsForm
|
||||||
from .models import CustomSlide, Tag
|
from .models import CustomSlide, Projector, Tag
|
||||||
from .serializers import CustomSlideSerializer, TagSerializer
|
from .serializers import (
|
||||||
|
CustomSlideSerializer,
|
||||||
|
ProjectorSerializer,
|
||||||
|
TagSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IndexView(utils_views.CSRFMixin, utils_views.View):
|
class IndexView(utils_views.CSRFMixin, utils_views.View):
|
||||||
@ -46,6 +56,192 @@ class IndexView(utils_views.CSRFMixin, utils_views.View):
|
|||||||
return HttpResponse(content)
|
return HttpResponse(content)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint to list, retrieve and update the projector slide info.
|
||||||
|
"""
|
||||||
|
queryset = Projector.objects.all()
|
||||||
|
serializer_class = ProjectorSerializer
|
||||||
|
|
||||||
|
def check_permissions(self, request):
|
||||||
|
"""
|
||||||
|
Calls self.permission_denied() if the requesting user has not the
|
||||||
|
permission to see the projector and in case of an update request the
|
||||||
|
permission to manage the projector.
|
||||||
|
"""
|
||||||
|
manage_methods = (
|
||||||
|
'activate_elements',
|
||||||
|
'prune_elements',
|
||||||
|
'deactivate_elements',
|
||||||
|
'clear_elements')
|
||||||
|
if (not request.user.has_perm('core.can_see_projector') or
|
||||||
|
(self.action in manage_methods and
|
||||||
|
not request.user.has_perm('core.can_manage_projector'))):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def activate_elements(self, request, pk):
|
||||||
|
"""
|
||||||
|
REST API operation to activate projector elements. It expects a POST
|
||||||
|
request to /rest/core/projector/<pk>/activate_elements/ with a list
|
||||||
|
of dictionaries to append to the projector config entry.
|
||||||
|
"""
|
||||||
|
# Get config entry from projector model, add new elements and try to
|
||||||
|
# serialize. This raises ValidationErrors if the data is invalid.
|
||||||
|
projector_instance = self.get_object()
|
||||||
|
projector_config = projector_instance.config
|
||||||
|
for projector_element in request.data:
|
||||||
|
projector_config.append(projector_element)
|
||||||
|
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def prune_elements(self, request, pk):
|
||||||
|
"""
|
||||||
|
REST API operation to activate projector elements. It expects a POST
|
||||||
|
request to /rest/core/projector/<pk>/prune_elements/ with a list of
|
||||||
|
dictionaries to write them to the projector config entry. All old
|
||||||
|
entries are deleted but not entries with stable == True.
|
||||||
|
"""
|
||||||
|
# Get config entry from projector model, delete old and add new
|
||||||
|
# elements and try to serialize. This raises ValidationErrors if the
|
||||||
|
# data is invalid. Do not filter 'stable' elements.
|
||||||
|
projector_instance = self.get_object()
|
||||||
|
projector_config = [element for element in projector_instance.config if element.get('stable')]
|
||||||
|
projector_config.extend(request.data)
|
||||||
|
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def deactivate_elements(self, request, pk):
|
||||||
|
"""
|
||||||
|
REST API operation to deactivate projector elements. It expects a
|
||||||
|
POST request to /rest/core/projector/<pk>/deactivate_elements/ with
|
||||||
|
a list of dictionaries. These are exactly the projector_elements in
|
||||||
|
the config that should be deleted.
|
||||||
|
"""
|
||||||
|
# Check the data. It must be a list of dictionaries. Get config
|
||||||
|
# entry from projector model. Pop out the entries that should be
|
||||||
|
# deleted and try to serialize. This raises ValidationErrors if the
|
||||||
|
# data is invalid.
|
||||||
|
if not isinstance(request.data, list) or list(filter(lambda item: not isinstance(item, dict), request.data)):
|
||||||
|
raise ValidationError({'config': ['Data must be a list of dictionaries.']})
|
||||||
|
|
||||||
|
projector_instance = self.get_object()
|
||||||
|
projector_config = projector_instance.config
|
||||||
|
for entry_to_be_deleted in request.data:
|
||||||
|
try:
|
||||||
|
projector_config.remove(entry_to_be_deleted)
|
||||||
|
except ValueError:
|
||||||
|
# The entry that should be deleted is not on the projector.
|
||||||
|
pass
|
||||||
|
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def clear_elements(self, request, pk):
|
||||||
|
"""
|
||||||
|
REST API operation to deactivate all projector elements but not
|
||||||
|
entries with stable == True. It expects a POST request to
|
||||||
|
/rest/core/projector/<pk>/clear_elements/.
|
||||||
|
"""
|
||||||
|
# Get config entry from projector model. Then clear the config field
|
||||||
|
# and try to serialize. Do not remove 'stable' elements.
|
||||||
|
projector_instance = self.get_object()
|
||||||
|
projector_config = [element for element in projector_instance.config if element.get('stable')]
|
||||||
|
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSlideViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint to list, retrieve, create, update and destroy custom slides.
|
||||||
|
"""
|
||||||
|
queryset = CustomSlide.objects.all()
|
||||||
|
serializer_class = CustomSlideSerializer
|
||||||
|
|
||||||
|
def check_permissions(self, request):
|
||||||
|
"""
|
||||||
|
Calls self.permission_denied() if the requesting user has not the
|
||||||
|
permission to manage projector.
|
||||||
|
"""
|
||||||
|
if not request.user.has_perm('core.can_manage_projector'):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
|
||||||
|
class TagViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint to list, retrieve, create, update and destroy tags.
|
||||||
|
"""
|
||||||
|
queryset = Tag.objects.all()
|
||||||
|
serializer_class = TagSerializer
|
||||||
|
|
||||||
|
def check_permissions(self, request):
|
||||||
|
"""
|
||||||
|
Calls self.permission_denied() if the requesting user has not the
|
||||||
|
permission to manage tags and it is a create, update or detroy request.
|
||||||
|
Users without permissions are able to list and retrieve tags.
|
||||||
|
"""
|
||||||
|
if (self.action in ('create', 'update', 'destroy') and
|
||||||
|
not request.user.has_perm('core.can_manage_tags')):
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
|
||||||
|
class UrlPatternsView(utils_views.APIView):
|
||||||
|
"""
|
||||||
|
Returns a dictionary with all url patterns as json. The patterns kwargs
|
||||||
|
are transformed using a colon.
|
||||||
|
"""
|
||||||
|
URL_KWARGS_REGEX = re.compile(r'%\((\w*)\)s')
|
||||||
|
http_method_names = ['get']
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
result = {}
|
||||||
|
url_dict = get_resolver(None).reverse_dict
|
||||||
|
for pattern_name in filter(lambda key: isinstance(key, str), url_dict.keys()):
|
||||||
|
normalized_regex_bits, p_pattern, pattern_default_args = url_dict[pattern_name]
|
||||||
|
url, url_kwargs = normalized_regex_bits[0]
|
||||||
|
result[pattern_name] = self.URL_KWARGS_REGEX.sub(r':\1', url)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorView(utils_views.View):
|
||||||
|
"""
|
||||||
|
View for Http 403, 404 and 500 error pages.
|
||||||
|
"""
|
||||||
|
status_code = None
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
http_error_strings = {
|
||||||
|
403: {'name': _('Forbidden'),
|
||||||
|
'description': _('Sorry, you have no permission to see this page.'),
|
||||||
|
'status_code': '403'},
|
||||||
|
404: {'name': _('Not Found'),
|
||||||
|
'description': _('Sorry, the requested page could not be found.'),
|
||||||
|
'status_code': '404'},
|
||||||
|
500: {'name': _('Internal Server Error'),
|
||||||
|
'description': _('Sorry, there was an unknown error. Please contact the event manager.'),
|
||||||
|
'status_code': '500'}}
|
||||||
|
context = {}
|
||||||
|
context['http_error'] = http_error_strings[self.status_code]
|
||||||
|
template_manipulation.send(sender=self.__class__, request=request, context=context)
|
||||||
|
response = render_to_response(
|
||||||
|
'core/error.html',
|
||||||
|
context_instance=RequestContext(request, context))
|
||||||
|
response.status_code = self.status_code
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Remove the following classes one by one.
|
||||||
|
|
||||||
class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView):
|
class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView):
|
||||||
"""
|
"""
|
||||||
Overview over all possible slides, the overlays and a live view: the
|
Overview over all possible slides, the overlays and a live view: the
|
||||||
@ -175,33 +371,6 @@ class SearchView(_SearchView):
|
|||||||
return models
|
return models
|
||||||
|
|
||||||
|
|
||||||
class ErrorView(utils_views.View):
|
|
||||||
"""
|
|
||||||
View for Http 403, 404 and 500 error pages.
|
|
||||||
"""
|
|
||||||
status_code = None
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
http_error_strings = {
|
|
||||||
403: {'name': _('Forbidden'),
|
|
||||||
'description': _('Sorry, you have no permission to see this page.'),
|
|
||||||
'status_code': '403'},
|
|
||||||
404: {'name': _('Not Found'),
|
|
||||||
'description': _('Sorry, the requested page could not be found.'),
|
|
||||||
'status_code': '404'},
|
|
||||||
500: {'name': _('Internal Server Error'),
|
|
||||||
'description': _('Sorry, there was an unknown error. Please contact the event manager.'),
|
|
||||||
'status_code': '500'}}
|
|
||||||
context = {}
|
|
||||||
context['http_error'] = http_error_strings[self.status_code]
|
|
||||||
template_manipulation.send(sender=self.__class__, request=request, context=context)
|
|
||||||
response = render_to_response(
|
|
||||||
'core/error.html',
|
|
||||||
context_instance=RequestContext(request, context))
|
|
||||||
response.status_code = self.status_code
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideViewMixin(object):
|
class CustomSlideViewMixin(object):
|
||||||
"""
|
"""
|
||||||
Mixin for for CustomSlide Views.
|
Mixin for for CustomSlide Views.
|
||||||
@ -235,22 +404,6 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CustomSlideViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve, create, update and destroy custom slides.
|
|
||||||
"""
|
|
||||||
queryset = CustomSlide.objects.all()
|
|
||||||
serializer_class = CustomSlideSerializer
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
"""
|
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
|
||||||
permission to manage projector.
|
|
||||||
"""
|
|
||||||
if not request.user.has_perm('core.can_manage_projector'):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
|
|
||||||
class TagListView(utils_views.AjaxMixin, utils_views.ListView):
|
class TagListView(utils_views.AjaxMixin, utils_views.ListView):
|
||||||
"""
|
"""
|
||||||
View to list and manipulate tags.
|
View to list and manipulate tags.
|
||||||
@ -327,37 +480,3 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView):
|
|||||||
action=getattr(self, 'action', None),
|
action=getattr(self, 'action', None),
|
||||||
error=getattr(self, 'error', None),
|
error=getattr(self, 'error', None),
|
||||||
**context)
|
**context)
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint to list, retrieve, create, update and destroy tags.
|
|
||||||
"""
|
|
||||||
queryset = Tag.objects.all()
|
|
||||||
serializer_class = TagSerializer
|
|
||||||
|
|
||||||
def check_permissions(self, request):
|
|
||||||
"""
|
|
||||||
Calls self.permission_denied() if the requesting user has not the
|
|
||||||
permission to manage tags and it is a create, update or detroy request.
|
|
||||||
"""
|
|
||||||
if (self.action in ('create', 'update', 'destroy') and
|
|
||||||
not request.user.has_perm('core.can_manage_tags')):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
|
|
||||||
class UrlPatternsView(utils_views.APIView):
|
|
||||||
"""
|
|
||||||
Returns a dictonary with all url patterns as json.
|
|
||||||
"""
|
|
||||||
URL_KWARGS_REGEX = re.compile(r'%\((\w*)\)s')
|
|
||||||
http_method_names = ['get']
|
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
|
||||||
result = {}
|
|
||||||
url_dict = get_resolver(None).reverse_dict
|
|
||||||
for pattern_name in filter(lambda key: isinstance(key, str), url_dict.keys()):
|
|
||||||
url = url_dict[pattern_name][0][0][0]
|
|
||||||
result[pattern_name] = self.URL_KWARGS_REGEX.sub(r':\1', url)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
@ -8,28 +8,23 @@ class UsersAppConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
# Load main menu entry and widgets.
|
# Load main menu entry and widgets.
|
||||||
# Do this by just importing all from these files.
|
# Do this by just importing all from these files.
|
||||||
from . import main_menu, widgets # noqa
|
from . import main_menu, projector, widgets # noqa
|
||||||
|
|
||||||
# Import all required stuff.
|
# Import all required stuff.
|
||||||
from openslides.config.signals import config_signal
|
from openslides.config.signals import config_signal
|
||||||
from openslides.core.signals import post_permission_creation
|
from openslides.core.signals import post_permission_creation
|
||||||
from openslides.projector.api import register_slide_model
|
|
||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from .signals import create_builtin_groups_and_admin, setup_users_config
|
from .signals import create_builtin_groups_and_admin, setup_users_config
|
||||||
from .views import GroupViewSet, UserViewSet
|
from .views import GroupViewSet, UserViewSet
|
||||||
|
|
||||||
# Load User model.
|
|
||||||
User = self.get_model('User')
|
|
||||||
|
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
config_signal.connect(setup_users_config, dispatch_uid='setup_users_config')
|
config_signal.connect(
|
||||||
|
setup_users_config,
|
||||||
|
dispatch_uid='setup_users_config')
|
||||||
post_permission_creation.connect(
|
post_permission_creation.connect(
|
||||||
create_builtin_groups_and_admin,
|
create_builtin_groups_and_admin,
|
||||||
dispatch_uid='create_builtin_groups_and_admin')
|
dispatch_uid='create_builtin_groups_and_admin')
|
||||||
|
|
||||||
# Register slides.
|
|
||||||
register_slide_model(User, 'participant/user_slide.html')
|
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register('users/user', UserViewSet)
|
router.register('users/user', UserViewSet)
|
||||||
router.register('users/group', GroupViewSet)
|
router.register('users/group', GroupViewSet)
|
||||||
|
29
openslides/users/projector.py
Normal file
29
openslides/users/projector.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from openslides.core.exceptions import ProjectorException
|
||||||
|
from openslides.utils.projector import ProjectorElement
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserSlide(ProjectorElement):
|
||||||
|
"""
|
||||||
|
Slide definitions for user model.
|
||||||
|
"""
|
||||||
|
name = 'users/user'
|
||||||
|
scripts = 'users/user_slide.js'
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
pk = self.config_entry.get('id')
|
||||||
|
try:
|
||||||
|
user = User.objects.get(pk=pk)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise ProjectorException(_('User does not exist.'))
|
||||||
|
result = [{
|
||||||
|
'collection': 'users/user',
|
||||||
|
'id': pk}]
|
||||||
|
for group in user.groups.all():
|
||||||
|
result.append({
|
||||||
|
'collection': 'users/group',
|
||||||
|
'id': group.pk})
|
||||||
|
return result
|
@ -8,10 +8,9 @@ class SignalConnectMetaClass(type):
|
|||||||
value for each child class and None for base classes because they will
|
value for each child class and None for base classes because they will
|
||||||
not be connected to the signal.
|
not be connected to the signal.
|
||||||
|
|
||||||
The classmethod get_all_objects is added as get_all classmethod to every
|
The classmethod get_all is added to every class using this metaclass.
|
||||||
class using this metaclass. Calling this on a base class or on child
|
Calling this on a base class or on child classes will retrieve all
|
||||||
classes will retrieve all connected children, one instance for each
|
connected children, one instance for each child class.
|
||||||
child class.
|
|
||||||
|
|
||||||
These instances will have a check_permission method which returns True
|
These instances will have a check_permission method which returns True
|
||||||
by default. You can override this method to return False on runtime if
|
by default. You can override this method to return False on runtime if
|
||||||
@ -29,13 +28,16 @@ class SignalConnectMetaClass(type):
|
|||||||
|
|
||||||
class Base(object, metaclass=SignalConnectMetaClass):
|
class Base(object, metaclass=SignalConnectMetaClass):
|
||||||
signal = django.dispatch.Signal()
|
signal = django.dispatch.Signal()
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_dispatch_uid(cls):
|
def get_dispatch_uid(cls):
|
||||||
if not cls.__name__ == 'Base':
|
if not cls.__name__ == 'Base':
|
||||||
return cls.__name__
|
return cls.__name__
|
||||||
|
|
||||||
class Child(Base):
|
class Child(Base):
|
||||||
def __init__(self, **kwargs):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
child = Base.get_all(request)[0]
|
child = Base.get_all(request)[0]
|
||||||
@ -46,7 +48,7 @@ class SignalConnectMetaClass(type):
|
|||||||
Creates the class and connects it to the signal if so. Adds all
|
Creates the class and connects it to the signal if so. Adds all
|
||||||
default attributes and methods.
|
default attributes and methods.
|
||||||
"""
|
"""
|
||||||
class_attributes['get_all'] = get_all_objects
|
class_attributes['get_all'] = get_all
|
||||||
new_class = super(SignalConnectMetaClass, metaclass).__new__(
|
new_class = super(SignalConnectMetaClass, metaclass).__new__(
|
||||||
metaclass, class_name, class_parents, class_attributes)
|
metaclass, class_name, class_parents, class_attributes)
|
||||||
try:
|
try:
|
||||||
@ -70,18 +72,21 @@ class SignalConnectMetaClass(type):
|
|||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_objects(cls, request):
|
def get_all(cls, request=None):
|
||||||
"""
|
"""
|
||||||
Collects all objects of the class created by the SignalConnectMetaClass
|
Collects all objects of the class created by the SignalConnectMetaClass
|
||||||
from all apps via signal. They are sorted using the get_default_weight
|
from all apps via signal. They are sorted using the get_default_weight
|
||||||
method. Does not return objects where check_permission returns False.
|
method. Does not return objects where check_permission returns False.
|
||||||
|
|
||||||
Expects a django.http.HttpRequest object.
|
A django.http.HttpRequest object can optionally be given.
|
||||||
|
|
||||||
This classmethod is added as get_all classmethod to every class using
|
This classmethod is added as get_all classmethod to every class using
|
||||||
the SignalConnectMetaClass.
|
the SignalConnectMetaClass.
|
||||||
"""
|
"""
|
||||||
all_objects = [obj for __, obj in cls.signal.send(sender=cls, request=request) if obj.check_permission()]
|
kwargs = {'sender': cls}
|
||||||
|
if request is not None:
|
||||||
|
kwargs['request'] = request
|
||||||
|
all_objects = [obj for __, obj in cls.signal.send(**kwargs) if obj.check_permission()]
|
||||||
all_objects.sort(key=lambda obj: obj.get_default_weight())
|
all_objects.sort(key=lambda obj: obj.get_default_weight())
|
||||||
return all_objects
|
return all_objects
|
||||||
|
|
||||||
|
66
openslides/utils/projector.py
Normal file
66
openslides/utils/projector.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from django.dispatch import Signal
|
||||||
|
|
||||||
|
from .dispatch import SignalConnectMetaClass
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorElement(object, metaclass=SignalConnectMetaClass):
|
||||||
|
"""
|
||||||
|
Base class for an element on the projector.
|
||||||
|
|
||||||
|
Every app which wants to add projector elements has to create classes
|
||||||
|
subclassing from this base class with different names. The name and
|
||||||
|
scripts attributes have to be set. The metaclass
|
||||||
|
(SignalConnectMetaClass) does the rest of the magic.
|
||||||
|
"""
|
||||||
|
signal = Signal()
|
||||||
|
name = None
|
||||||
|
scripts = None
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Initializes the projector element instance. This is done when the
|
||||||
|
signal is sent.
|
||||||
|
|
||||||
|
Because of Django's signal API, we have to take wildcard keyword
|
||||||
|
arguments. But they are not used here.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
"""
|
||||||
|
Returns the classname as a unique string for each class. Returns None
|
||||||
|
for the base class so it will not be connected to the signal.
|
||||||
|
"""
|
||||||
|
if not cls.__name__ == 'ProjectorElement':
|
||||||
|
return cls.__name__
|
||||||
|
|
||||||
|
def get_data(self, projector_object, config_entry):
|
||||||
|
"""
|
||||||
|
Returns all data to be sent to the client. The projector object and
|
||||||
|
the config entry have to be given.
|
||||||
|
"""
|
||||||
|
self.projector_object = projector_object
|
||||||
|
self.config_entry = config_entry
|
||||||
|
assert self.config_entry.get('name') == self.name, (
|
||||||
|
'To get data of a projector element, the correct config entry has to be given.')
|
||||||
|
return {
|
||||||
|
'scripts': self.get_scripts(),
|
||||||
|
'context': self.get_context()}
|
||||||
|
|
||||||
|
def get_scripts(self):
|
||||||
|
"""
|
||||||
|
Returns ...?
|
||||||
|
"""
|
||||||
|
# TODO: Write docstring
|
||||||
|
if self.scripts is None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'A projector element class must define either a '
|
||||||
|
'get_scripts method or have a scripts argument.')
|
||||||
|
return self.scripts
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
"""
|
||||||
|
Returns the context of the projector element.
|
||||||
|
"""
|
||||||
|
return None
|
@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse
|
|||||||
from rest_framework.decorators import detail_route # noqa
|
from rest_framework.decorators import detail_route # noqa
|
||||||
from rest_framework.serializers import ( # noqa
|
from rest_framework.serializers import ( # noqa
|
||||||
CharField,
|
CharField,
|
||||||
|
Field,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
ListSerializer,
|
ListSerializer,
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
@ -15,7 +16,7 @@ from rest_framework.serializers import ( # noqa
|
|||||||
ValidationError)
|
ValidationError)
|
||||||
from rest_framework.response import Response # noqa
|
from rest_framework.response import Response # noqa
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from rest_framework.viewsets import ModelViewSet, ViewSet # noqa
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet # noqa
|
||||||
from rest_framework.decorators import list_route # noqa
|
from rest_framework.decorators import list_route # noqa
|
||||||
|
|
||||||
from .exceptions import OpenSlidesError
|
from .exceptions import OpenSlidesError
|
||||||
|
0
tests/integration/core/__init__.py
Normal file
0
tests/integration/core/__init__.py
Normal file
46
tests/integration/core/test_views.py
Normal file
46
tests/integration/core/test_views.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
from openslides.core.models import CustomSlide, Projector
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorAPI(TestCase):
|
||||||
|
"""
|
||||||
|
Tests requests from the anonymous user.
|
||||||
|
"""
|
||||||
|
def test_slide_on_default_projector(self):
|
||||||
|
self.client.login(username='admin', password='admin')
|
||||||
|
customslide = CustomSlide.objects.create(title='title_que1olaish5Wei7que6i', text='text_aishah8Eh7eQuie5ooji')
|
||||||
|
default_projector = Projector.objects.get(pk=1)
|
||||||
|
default_projector.config = [{'name': 'core/customslide', 'id': customslide.id}]
|
||||||
|
default_projector.save()
|
||||||
|
|
||||||
|
response = self.client.get(reverse('projector-detail', args=['1']))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(json.loads(response.content.decode()), {
|
||||||
|
'config': [{'name': 'core/customslide', 'id': customslide.id}],
|
||||||
|
'projector_elements': [
|
||||||
|
{'name': 'core/customslide',
|
||||||
|
'scripts': 'core/customslide_slide.js',
|
||||||
|
'context': [
|
||||||
|
{'collection': 'core/customslide',
|
||||||
|
'id': customslide.id}]}]})
|
||||||
|
|
||||||
|
def test_invalid_slide_on_default_projector(self):
|
||||||
|
self.client.login(username='admin', password='admin')
|
||||||
|
default_projector = Projector.objects.get(pk=1)
|
||||||
|
default_projector.config = [{'name': 'invalid_slide'}]
|
||||||
|
default_projector.save()
|
||||||
|
|
||||||
|
response = self.client.get(reverse('projector-detail', args=['1']))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(json.loads(response.content.decode()), {
|
||||||
|
'config': [{'name': 'invalid_slide'}],
|
||||||
|
'projector_elements': [
|
||||||
|
{'name': 'invalid_slide',
|
||||||
|
'error': 'Projector element does not exist.'}]})
|
@ -1,12 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class TestModel(models.Model):
|
|
||||||
name = models.CharField(max_length='255')
|
|
||||||
|
|
||||||
def get_absolute_url(self, link='detail'):
|
|
||||||
if link == 'detail':
|
|
||||||
return 'detail-url-here'
|
|
||||||
if link == 'delete':
|
|
||||||
return 'delete-url-here'
|
|
||||||
raise ValueError('No URL for %s' % link)
|
|
@ -3,8 +3,6 @@ from django.template import Context, Template
|
|||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
from .models import TestModel
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigTagAndFilter(TestCase):
|
class ConfigTagAndFilter(TestCase):
|
||||||
def test_config_tag(self):
|
def test_config_tag(self):
|
||||||
@ -29,32 +27,3 @@ class ConfigTagAndFilter(TestCase):
|
|||||||
template = Template(template_code)
|
template = Template(template_code)
|
||||||
self.assertTrue('FdgfkR04jtg9f8bq' in template.render(Context({})))
|
self.assertTrue('FdgfkR04jtg9f8bq' in template.render(Context({})))
|
||||||
self.assertFalse('bad_e0fvkfHFD' in template.render(Context({})))
|
self.assertFalse('bad_e0fvkfHFD' in template.render(Context({})))
|
||||||
|
|
||||||
|
|
||||||
class AbsoluteUrlFilter(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.model = TestModel.objects.create(name='test_model')
|
|
||||||
|
|
||||||
def test_default_argument(self):
|
|
||||||
"""
|
|
||||||
Test to call absolute_url without an argument.
|
|
||||||
"""
|
|
||||||
t = Template("{% load tags %}URL: {{ model|absolute_url }}")
|
|
||||||
html = t.render(Context({'model': self.model}))
|
|
||||||
self.assertEqual(html, 'URL: detail-url-here')
|
|
||||||
|
|
||||||
def test_with_argument(self):
|
|
||||||
"""
|
|
||||||
Test to call absolute_url with an argument.
|
|
||||||
"""
|
|
||||||
t = Template("{% load tags %}URL: {{ model|absolute_url:'delete' }}")
|
|
||||||
html = t.render(Context({'model': self.model}))
|
|
||||||
self.assertEqual(html, 'URL: delete-url-here')
|
|
||||||
|
|
||||||
def test_wrong_argument(self):
|
|
||||||
"""
|
|
||||||
Test to call absolute_url with a non existing argument.
|
|
||||||
"""
|
|
||||||
t = Template("{% load tags %}URL: {{ model|absolute_url:'wrong' }}")
|
|
||||||
html = t.render(Context({'model': self.model}))
|
|
||||||
self.assertEqual(html, 'URL: ')
|
|
||||||
|
@ -121,8 +121,8 @@ class CustomSlidesTest(TestCase):
|
|||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
# Setup
|
# Setup
|
||||||
url = '/customslide/1/edit/'
|
custom_slide = CustomSlide.objects.create(title='test_title_jeeDeB3aedei8ahceeso')
|
||||||
CustomSlide.objects.create(title='test_title_jeeDeB3aedei8ahceeso')
|
url = '/customslide/%d/edit/' % custom_slide.pk
|
||||||
# Test
|
# Test
|
||||||
response = self.admin_client.get(url)
|
response = self.admin_client.get(url)
|
||||||
self.assertTemplateUsed(response, 'core/customslide_update.html')
|
self.assertTemplateUsed(response, 'core/customslide_update.html')
|
||||||
@ -131,20 +131,9 @@ class CustomSlidesTest(TestCase):
|
|||||||
url,
|
url,
|
||||||
{'title': 'test_title_ai8Ooboh5bahr6Ee7goo', 'weight': '0'})
|
{'title': 'test_title_ai8Ooboh5bahr6Ee7goo', 'weight': '0'})
|
||||||
self.assertRedirects(response, '/dashboard/')
|
self.assertRedirects(response, '/dashboard/')
|
||||||
self.assertEqual(CustomSlide.objects.get(pk=1).title,
|
self.assertEqual(CustomSlide.objects.get(pk=custom_slide.pk).title,
|
||||||
'test_title_ai8Ooboh5bahr6Ee7goo')
|
'test_title_ai8Ooboh5bahr6Ee7goo')
|
||||||
|
|
||||||
def test_delete(self):
|
|
||||||
# Setup
|
|
||||||
url = '/customslide/1/del/'
|
|
||||||
CustomSlide.objects.create(title='test_title_oyie0em1chieM7YohX4H')
|
|
||||||
# Test
|
|
||||||
response = self.admin_client.get(url)
|
|
||||||
self.assertRedirects(response, '/customslide/1/edit/')
|
|
||||||
response = self.admin_client.post(url, {'yes': 'true'})
|
|
||||||
self.assertRedirects(response, '/dashboard/')
|
|
||||||
self.assertFalse(CustomSlide.objects.exists())
|
|
||||||
|
|
||||||
|
|
||||||
class TagListViewTest(TestCase):
|
class TagListViewTest(TestCase):
|
||||||
def test_get_tag_queryset(self):
|
def test_get_tag_queryset(self):
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from openslides.core import views
|
from openslides.core import views
|
||||||
|
from openslides.utils.rest_api import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class TestUrlPatternsView(TestCase):
|
class TestUrlPatternsView(TestCase):
|
||||||
@patch('openslides.core.views.get_resolver')
|
@patch('openslides.core.views.get_resolver')
|
||||||
def test_get_context_data(self, mock_resolver):
|
def test_get_context_data(self, mock_resolver):
|
||||||
mock_resolver().reverse_dict = {
|
mock_resolver().reverse_dict = {
|
||||||
'url_pattern1': [[['my_url1']]],
|
'url_pattern1': ([['my_url1', [None]]], None, None),
|
||||||
'url_pattern2': [[['my_url2/%(kwarg)s/']]],
|
'url_pattern2': ([['my_url2/%(kwarg)s/', ['kwargs']]], None, None),
|
||||||
('not_a_str', ): [[['not_a_str']]]}
|
('not_a_str', ): [[['not_a_str']]]}
|
||||||
view = views.UrlPatternsView()
|
view = views.UrlPatternsView()
|
||||||
|
|
||||||
@ -19,3 +20,127 @@ class TestUrlPatternsView(TestCase):
|
|||||||
context,
|
context,
|
||||||
{'url_pattern1': 'my_url1',
|
{'url_pattern1': 'my_url1',
|
||||||
'url_pattern2': 'my_url2/:kwarg/'})
|
'url_pattern2': 'my_url2/:kwarg/'})
|
||||||
|
|
||||||
|
|
||||||
|
@patch('openslides.core.views.ProjectorViewSet.get_object')
|
||||||
|
class ProjectorAPI(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.viewset = views.ProjectorViewSet()
|
||||||
|
self.viewset.format_kwarg = None
|
||||||
|
|
||||||
|
def test_activate_elements(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_Du4tie7foosahnoofahg',
|
||||||
|
'test_key_Eek8eipeingulah3aech': 'test_value_quuupaephuY7eoLohbee'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{'name': 'new_test_projector_element_el9UbeeT9quucesoyusu'}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.activate_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 2)
|
||||||
|
|
||||||
|
def test_activate_elements_no_list(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_ahshaiTie8xie3eeThu9',
|
||||||
|
'test_key_ohwa7ooze2angoogieM9': 'test_value_raiL2ohsheij1seiqua5'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = {'name': 'new_test_projector_element_buuDohphahWeeR2eeQu0'}
|
||||||
|
self.viewset.request = request
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.viewset.activate_elements(request=request, pk=MagicMock())
|
||||||
|
|
||||||
|
def test_activate_elements_bad_element(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_ieroa7eu3aechaip3eeD',
|
||||||
|
'test_key_mie3Eeroh9rooKeinga6': 'test_value_gee1Uitae6aithaiphoo'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{'bad_quangah1ahoo6oKaeBai': 'value_doh8ahwe0Zooc1eefu0o'}]
|
||||||
|
self.viewset.request = request
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.viewset.activate_elements(request=request, pk=MagicMock())
|
||||||
|
|
||||||
|
def test_prune_elements(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_Oc7OhXeeg0poThoh8boo',
|
||||||
|
'test_key_ahNei1ke4uCio6uareef': 'test_value_xieSh4yeemaen9oot6ki'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{
|
||||||
|
'name': 'test_projector_element_bohb1phiebah5TeCei1N',
|
||||||
|
'test_key_gahSh9otu6aeghaiquie': 'test_value_aeNgee2Yeeph4Ohru2Oo'}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.prune_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 1)
|
||||||
|
|
||||||
|
def test_prune_elements_with_stable(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_aegh2aichee9nooWohRu',
|
||||||
|
'test_key_wahlaelahwaeNg6fooH7': 'test_value_taePie9Ohxohja4ugisa',
|
||||||
|
'stable': True}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{
|
||||||
|
'name': 'test_projector_element_yei1Aim6Aed1po8eegh2',
|
||||||
|
'test_key_mud1shoo8moh6eiXoong': 'test_value_shugieJier6agh1Ehie3'}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.prune_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 2)
|
||||||
|
|
||||||
|
def test_deactivate_elements(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
||||||
|
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{
|
||||||
|
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
||||||
|
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 0)
|
||||||
|
|
||||||
|
def test_deactivate_elements_wrong_element(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_c6oohooxugiphuuM6Wee',
|
||||||
|
'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = [{'name': 'wrong name'}]
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 1)
|
||||||
|
|
||||||
|
def test_deactivate_elements_no_list(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_Au1ce9nevaeX7zo4ye2w',
|
||||||
|
'test_key_we9biiZ7bah4Sha2haS5': 'test_value_eehoipheik6aiNgeegor'}]
|
||||||
|
request = MagicMock()
|
||||||
|
request.data = 'bad_value_no_list_ohchohWee1fie0SieTha'
|
||||||
|
self.viewset.request = request
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
||||||
|
|
||||||
|
def test_deactivate_elements_bad_list(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_teibaeRaim1heiCh6Ohv',
|
||||||
|
'test_key_uk7wai7eiZieQu0ief3': 'test_value_eeghisei3ieGh3ieb6ae'}]
|
||||||
|
request = MagicMock()
|
||||||
|
# Value 1 is not an dictionary so we expect ValidationError.
|
||||||
|
request.data = [1]
|
||||||
|
self.viewset.request = request
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.viewset.deactivate_elements(request=request, pk=MagicMock())
|
||||||
|
|
||||||
|
def test_clear_elements(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_iphuuM6Weec6oohooxug',
|
||||||
|
'test_key_bi7ur1UoB1eehiloh7mi': 'test_value_jieTh6aiwoo8eig1AeSa'}]
|
||||||
|
request = MagicMock()
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.clear_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 0)
|
||||||
|
|
||||||
|
def test_clear_elements_with_stable(self, mock_object):
|
||||||
|
mock_object.return_value.config = [{
|
||||||
|
'name': 'test_projector_element_6oohooxugiphuuM6Weec',
|
||||||
|
'test_key_bi7B1eehiloh7miur1Uo': 'test_value_jiSaeTh6aiwoo8eig1Ae',
|
||||||
|
'stable': True}]
|
||||||
|
request = MagicMock()
|
||||||
|
self.viewset.request = request
|
||||||
|
self.viewset.clear_elements(request=request, pk=MagicMock())
|
||||||
|
self.assertEqual(len(mock_object.return_value.config), 1)
|
||||||
|
Loading…
Reference in New Issue
Block a user