Added REST API for projector. Introduced new projector API.
Added custom slide projector element class. Added welcome slide as custom slide. Added user slide projector element class. Added clock, countdown ans message projector elements. Renamed SignalConnectMetaClass classmethod get_all_objects to get_all (private API). Added migrations to core app. Fixed and wrote tests. Updated CHANGELOG.
This commit is contained in:
parent
9c51313a82
commit
dc7d27a985
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