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:
|
||||
- Updated the tests and changed only small internal parts of method of the
|
||||
agenda model. No API changes.
|
||||
- Deprecated mptt.
|
||||
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:
|
||||
- Renamed app from motion to motions
|
||||
- Renamed app from motion to motions.
|
||||
Mediafiles:
|
||||
- Renamed app from mediafile to mediafiles
|
||||
- Renamed app from mediafile to mediafiles.
|
||||
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.
|
||||
Other:
|
||||
- New OpenSlides logo.
|
||||
- Changed supported Python version to >= 3.3.
|
||||
- Used Django 1.7 as lowest requirement.
|
||||
- Added Django's application configuration. Refactored loading of signals,
|
||||
template signals and slides.
|
||||
- Added API using Django REST Framework 3.x. Added several views and mixins for
|
||||
generic views in OpenSlides apps.
|
||||
template signals and projector elements/slides.
|
||||
- Setup migrations.
|
||||
- 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.
|
||||
- Removed use of 'django.views.i18n.javascript_catalog'.
|
||||
- Removed use of 'django.views.i18n.javascript_catalog'. Used angular-gettext
|
||||
now.
|
||||
- Updated to Bootstrap 3.
|
||||
- Used SockJS for automatic update of AngularJS driven single page frontend.
|
||||
- Refactored start script and management commands.
|
||||
- Refactored tests.
|
||||
- Used Bower and gulp to manage third party JavaScript and Cascading Style
|
||||
Sheets libraries.
|
||||
- Used setup.cfg for development tools.
|
||||
- Fixed bug in LocalizedModelMultipleChoiceField.
|
||||
|
||||
|
||||
|
@ -6,27 +6,23 @@ class CoreAppConfig(AppConfig):
|
||||
verbose_name = 'OpenSlides Core'
|
||||
|
||||
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.
|
||||
from . import main_menu, widgets # noqa
|
||||
from . import main_menu, projector, widgets # noqa
|
||||
|
||||
# Import all required stuff.
|
||||
from django.db.models import signals
|
||||
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.rest_api import router
|
||||
from .signals import setup_general_config
|
||||
from .views import CustomSlideViewSet, TagViewSet
|
||||
from .views import CustomSlideViewSet, ProjectorViewSet, TagViewSet
|
||||
|
||||
# Connect signals.
|
||||
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.
|
||||
router.register('core/projector', ProjectorViewSet)
|
||||
router.register('core/customslide', CustomSlideViewSet)
|
||||
router.register('core/tag', TagViewSet)
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
|
||||
|
||||
class ProjectorException(OpenSlidesError):
|
||||
pass
|
||||
|
||||
|
||||
class TagException(OpenSlidesError):
|
||||
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.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||
# TODO: activate the following line after using the apploader
|
||||
# from django.contrib.auth import get_user_model
|
||||
from jsonfield import JSONField
|
||||
|
||||
from openslides.projector.models import SlideMixin
|
||||
from openslides.utils.models import AbsoluteUrlMixin
|
||||
from openslides.utils.projector import ProjectorElement
|
||||
from openslides.utils.rest_api import RESTModelMixin
|
||||
|
||||
# Imports the default user so that other apps can import it from here.
|
||||
# TODO: activate this with the new apploader
|
||||
# User = get_user_model()
|
||||
from .exceptions import ProjectorException
|
||||
|
||||
|
||||
class CustomSlide(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
|
||||
class Projector(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for Slides, only for the projector.
|
||||
"""
|
||||
slide_callback_name = 'customslide'
|
||||
Model for all projectors. At the moment we support only one projector,
|
||||
the default projector (pk=1).
|
||||
|
||||
title = models.CharField(max_length=256, verbose_name=ugettext_lazy('Title'))
|
||||
text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy('Text'))
|
||||
weight = models.IntegerField(default=0, verbose_name=ugettext_lazy('Weight'))
|
||||
If the config field is empty or invalid the projector shows a default
|
||||
slide. To activate a slide and extra projector elements, save valid
|
||||
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:
|
||||
"""
|
||||
General permissions that can not be placed at a specific app.
|
||||
Contains general permissions that can not be placed in a specific app.
|
||||
"""
|
||||
permissions = (
|
||||
('can_manage_projector', ugettext_noop('Can manage 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_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):
|
||||
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):
|
||||
"""
|
||||
Model to save tags.
|
||||
Model for tags. This tags can be used for other models like agenda items,
|
||||
motions or assignments.
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=255, unique=True,
|
||||
verbose_name=ugettext_lazy('Tag'))
|
||||
name = models.CharField(
|
||||
verbose_name=ugettext_lazy('Tag'),
|
||||
max_length=255,
|
||||
unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
ordering = ('name',)
|
||||
permissions = (
|
||||
('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):
|
||||
@ -9,7 +42,7 @@ class CustomSlideSerializer(ModelSerializer):
|
||||
"""
|
||||
class Meta:
|
||||
model = CustomSlide
|
||||
fields = ('id', 'title', 'text', 'weight',)
|
||||
fields = ('id', 'title', 'text', 'weight', )
|
||||
|
||||
|
||||
class TagSerializer(ModelSerializer):
|
||||
@ -18,4 +51,4 @@ class TagSerializer(ModelSerializer):
|
||||
"""
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ('id', 'name',)
|
||||
fields = ('id', 'name', )
|
||||
|
@ -21,14 +21,24 @@ from openslides.utils.plugins import (
|
||||
get_plugin_verbose_name,
|
||||
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.widgets import Widget
|
||||
|
||||
from .exceptions import TagException
|
||||
from .forms import SelectWidgetsForm
|
||||
from .models import CustomSlide, Tag
|
||||
from .serializers import CustomSlideSerializer, TagSerializer
|
||||
from .models import CustomSlide, Projector, Tag
|
||||
from .serializers import (
|
||||
CustomSlideSerializer,
|
||||
ProjectorSerializer,
|
||||
TagSerializer,
|
||||
)
|
||||
|
||||
|
||||
class IndexView(utils_views.CSRFMixin, utils_views.View):
|
||||
@ -46,6 +56,192 @@ class IndexView(utils_views.CSRFMixin, utils_views.View):
|
||||
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):
|
||||
"""
|
||||
Overview over all possible slides, the overlays and a live view: the
|
||||
@ -175,33 +371,6 @@ class SearchView(_SearchView):
|
||||
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):
|
||||
"""
|
||||
Mixin for for CustomSlide Views.
|
||||
@ -235,22 +404,6 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView):
|
||||
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):
|
||||
"""
|
||||
View to list and manipulate tags.
|
||||
@ -327,37 +480,3 @@ class TagListView(utils_views.AjaxMixin, utils_views.ListView):
|
||||
action=getattr(self, 'action', None),
|
||||
error=getattr(self, 'error', None),
|
||||
**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):
|
||||
# Load main menu entry and widgets.
|
||||
# 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.
|
||||
from openslides.config.signals import config_signal
|
||||
from openslides.core.signals import post_permission_creation
|
||||
from openslides.projector.api import register_slide_model
|
||||
from openslides.utils.rest_api import router
|
||||
from .signals import create_builtin_groups_and_admin, setup_users_config
|
||||
from .views import GroupViewSet, UserViewSet
|
||||
|
||||
# Load User model.
|
||||
User = self.get_model('User')
|
||||
|
||||
# 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(
|
||||
create_builtin_groups_and_admin,
|
||||
dispatch_uid='create_builtin_groups_and_admin')
|
||||
|
||||
# Register slides.
|
||||
register_slide_model(User, 'participant/user_slide.html')
|
||||
|
||||
# Register viewsets.
|
||||
router.register('users/user', UserViewSet)
|
||||
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
|
||||
not be connected to the signal.
|
||||
|
||||
The classmethod get_all_objects is added as get_all classmethod to every
|
||||
class using this metaclass. Calling this on a base class or on child
|
||||
classes will retrieve all connected children, one instance for each
|
||||
child class.
|
||||
The classmethod get_all is added to every class using this metaclass.
|
||||
Calling this on a base class or on child classes will retrieve all
|
||||
connected children, one instance for each child class.
|
||||
|
||||
These instances will have a check_permission method which returns True
|
||||
by default. You can override this method to return False on runtime if
|
||||
@ -29,14 +28,17 @@ class SignalConnectMetaClass(type):
|
||||
|
||||
class Base(object, metaclass=SignalConnectMetaClass):
|
||||
signal = django.dispatch.Signal()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_dispatch_uid(cls):
|
||||
if not cls.__name__ == 'Base':
|
||||
return cls.__name__
|
||||
|
||||
class Child(Base):
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
pass
|
||||
|
||||
child = Base.get_all(request)[0]
|
||||
assert Child == type(child)
|
||||
@ -46,7 +48,7 @@ class SignalConnectMetaClass(type):
|
||||
Creates the class and connects it to the signal if so. Adds all
|
||||
default attributes and methods.
|
||||
"""
|
||||
class_attributes['get_all'] = get_all_objects
|
||||
class_attributes['get_all'] = get_all
|
||||
new_class = super(SignalConnectMetaClass, metaclass).__new__(
|
||||
metaclass, class_name, class_parents, class_attributes)
|
||||
try:
|
||||
@ -70,18 +72,21 @@ class SignalConnectMetaClass(type):
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_all_objects(cls, request):
|
||||
def get_all(cls, request=None):
|
||||
"""
|
||||
Collects all objects of the class created by the SignalConnectMetaClass
|
||||
from all apps via signal. They are sorted using the get_default_weight
|
||||
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
|
||||
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())
|
||||
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.serializers import ( # noqa
|
||||
CharField,
|
||||
Field,
|
||||
IntegerField,
|
||||
ListSerializer,
|
||||
ModelSerializer,
|
||||
@ -15,7 +16,7 @@ from rest_framework.serializers import ( # noqa
|
||||
ValidationError)
|
||||
from rest_framework.response import Response # noqa
|
||||
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 .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.utils.test import TestCase
|
||||
|
||||
from .models import TestModel
|
||||
|
||||
|
||||
class ConfigTagAndFilter(TestCase):
|
||||
def test_config_tag(self):
|
||||
@ -29,32 +27,3 @@ class ConfigTagAndFilter(TestCase):
|
||||
template = Template(template_code)
|
||||
self.assertTrue('FdgfkR04jtg9f8bq' 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):
|
||||
# Setup
|
||||
url = '/customslide/1/edit/'
|
||||
CustomSlide.objects.create(title='test_title_jeeDeB3aedei8ahceeso')
|
||||
custom_slide = CustomSlide.objects.create(title='test_title_jeeDeB3aedei8ahceeso')
|
||||
url = '/customslide/%d/edit/' % custom_slide.pk
|
||||
# Test
|
||||
response = self.admin_client.get(url)
|
||||
self.assertTemplateUsed(response, 'core/customslide_update.html')
|
||||
@ -131,20 +131,9 @@ class CustomSlidesTest(TestCase):
|
||||
url,
|
||||
{'title': 'test_title_ai8Ooboh5bahr6Ee7goo', 'weight': '0'})
|
||||
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')
|
||||
|
||||
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):
|
||||
def test_get_tag_queryset(self):
|
||||
|
@ -1,15 +1,16 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from openslides.core import views
|
||||
from openslides.utils.rest_api import ValidationError
|
||||
|
||||
|
||||
class TestUrlPatternsView(TestCase):
|
||||
@patch('openslides.core.views.get_resolver')
|
||||
def test_get_context_data(self, mock_resolver):
|
||||
mock_resolver().reverse_dict = {
|
||||
'url_pattern1': [[['my_url1']]],
|
||||
'url_pattern2': [[['my_url2/%(kwarg)s/']]],
|
||||
'url_pattern1': ([['my_url1', [None]]], None, None),
|
||||
'url_pattern2': ([['my_url2/%(kwarg)s/', ['kwargs']]], None, None),
|
||||
('not_a_str', ): [[['not_a_str']]]}
|
||||
view = views.UrlPatternsView()
|
||||
|
||||
@ -19,3 +20,127 @@ class TestUrlPatternsView(TestCase):
|
||||
context,
|
||||
{'url_pattern1': 'my_url1',
|
||||
'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