New API for widgets using a metaclass.
It is now possible to define a default state and to submit extra stylefiles and javascript files when defining a widget in an app. This is done by a new metaclass in openslides.utils.dispatch. Also fixed some other tests.
This commit is contained in:
parent
2f75202c9b
commit
1fb1f17d15
@ -8,6 +8,9 @@ Version 1.6 (unreleased)
|
|||||||
========================
|
========================
|
||||||
[https://github.com/OpenSlides/OpenSlides/issues?milestone=14]
|
[https://github.com/OpenSlides/OpenSlides/issues?milestone=14]
|
||||||
|
|
||||||
|
Other:
|
||||||
|
- Changed widget api. Used new metaclass.
|
||||||
|
|
||||||
|
|
||||||
Version 1.5.1 (unreleased)
|
Version 1.5.1 (unreleased)
|
||||||
==========================
|
==========================
|
||||||
|
2
fabfile.py
vendored
2
fabfile.py
vendored
@ -25,7 +25,7 @@ def coverage_report_plain():
|
|||||||
Runs all tests and prints the coverage report.
|
Runs all tests and prints the coverage report.
|
||||||
"""
|
"""
|
||||||
test()
|
test()
|
||||||
local('coverage report -m --fail-under=75')
|
local('coverage report -m --fail-under=76')
|
||||||
|
|
||||||
|
|
||||||
def coverage():
|
def coverage():
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import widgets # noqa
|
@ -1,6 +1,9 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
{% trans 'I am on the list of speakers of the following items:' %}
|
{% trans 'I am on the list of speakers of the following items:' %}
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
@ -25,7 +28,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if config_motion_min_supporters %}
|
{% if 'motion_min_supporters'|get_config %}
|
||||||
<hr />
|
<hr />
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
{% trans 'I support the following motions:' %}
|
{% trans 'I support the following motions:' %}
|
||||||
@ -55,3 +58,4 @@
|
|||||||
<li><i>{% trans 'None' %}</i></li>
|
<li><i>{% trans 'None' %}</i></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endblock %}
|
@ -1,66 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
|
|
||||||
from openslides.config.api import config
|
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
"""
|
|
||||||
Returns the widgets of the account app. It is only the personal_info_widget.
|
|
||||||
"""
|
|
||||||
if not isinstance(request.user, AnonymousUser):
|
|
||||||
return [get_personal_info_widget(request)]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def get_personal_info_widget(request):
|
|
||||||
"""
|
|
||||||
Provides a widget for personal info. It shows your submitted and supported
|
|
||||||
motions, where you are on the list of speakers and where you are supporter
|
|
||||||
or candidate. If one of the modules agenda, motion or assignment does
|
|
||||||
not exist, it is not loaded. If all does not exist, the widget disapears.
|
|
||||||
"""
|
|
||||||
personal_info_context = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
from openslides.agenda.models import Item
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
personal_info_context.update({
|
|
||||||
'items': Item.objects.filter(
|
|
||||||
speaker__person=request.user,
|
|
||||||
speaker__begin_time=None)})
|
|
||||||
try:
|
|
||||||
from openslides.motion.models import Motion
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
personal_info_context.update({
|
|
||||||
'submitted_motions': Motion.objects.filter(submitter__person=request.user),
|
|
||||||
'config_motion_min_supporters': config['motion_min_supporters'],
|
|
||||||
'supported_motions': Motion.objects.filter(supporter__person=request.user)})
|
|
||||||
try:
|
|
||||||
from openslides.assignment.models import Assignment
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
personal_info_context.update({
|
|
||||||
'assignments': Assignment.objects.filter(
|
|
||||||
assignmentcandidate__person=request.user,
|
|
||||||
assignmentcandidate__blocked=False)})
|
|
||||||
|
|
||||||
if personal_info_context:
|
|
||||||
return Widget(
|
|
||||||
request,
|
|
||||||
name='personal_info',
|
|
||||||
display_name=_('My items, motions and elections'),
|
|
||||||
template='account/personal_info_widget.html',
|
|
||||||
context=personal_info_context,
|
|
||||||
permission_required=None,
|
|
||||||
default_column=1,
|
|
||||||
default_weight=80)
|
|
75
openslides/account/widgets.py
Normal file
75
openslides/account/widgets.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalInfoWidget(Widget):
|
||||||
|
"""
|
||||||
|
Provides a widget for personal info. It shows your submitted and supported
|
||||||
|
motions, where you are on the list of speakers and where you are supporter
|
||||||
|
or candidate. If one of the modules agenda, motion or assignment does
|
||||||
|
not exist, it is not loaded. If all does not exist, the widget disapears.
|
||||||
|
"""
|
||||||
|
name = 'personal_info'
|
||||||
|
verbose_name = ugettext_lazy('My items, motions and elections')
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 80
|
||||||
|
template_name = 'account/widget_personal_info.html'
|
||||||
|
|
||||||
|
def check_permission(self):
|
||||||
|
"""
|
||||||
|
The widget is disabled for anonymous users.
|
||||||
|
"""
|
||||||
|
return not isinstance(self.request.user, AnonymousUser)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
"""
|
||||||
|
The widget is disabled if there can neither the agenda app, nor the
|
||||||
|
motion app nor the assignment app be found.
|
||||||
|
"""
|
||||||
|
for module in ('agenda', 'motion', 'assignment'):
|
||||||
|
try:
|
||||||
|
__import__('openslides.%s' % module)
|
||||||
|
except ImportError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
active = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
active = False
|
||||||
|
return active
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
"""
|
||||||
|
Adds the context to the widget.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from openslides.agenda.models import Item
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
context.update({
|
||||||
|
'items': Item.objects.filter(
|
||||||
|
speaker__person=self.request.user,
|
||||||
|
speaker__begin_time=None)})
|
||||||
|
try:
|
||||||
|
from openslides.motion.models import Motion
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
context.update({
|
||||||
|
'submitted_motions': Motion.objects.filter(submitter__person=self.request.user),
|
||||||
|
'supported_motions': Motion.objects.filter(supporter__person=self.request.user)})
|
||||||
|
try:
|
||||||
|
from openslides.assignment.models import Assignment
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
context.update({
|
||||||
|
'assignments': Assignment.objects.filter(
|
||||||
|
assignmentcandidate__person=self.request.user,
|
||||||
|
assignmentcandidate__blocked=False)})
|
||||||
|
return super(PersonalInfoWidget, self).get_context_data(**context)
|
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import signals, slides # noqa
|
from . import signals, slides, widgets # noqa
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% if perms.agenda.can_be_speaker %}
|
|
||||||
<p><a href="{% url 'agenda_add_to_current_list_of_speakers' %}" class="btn"><i class="icon icon-speaker"></i> {% trans 'Put me on the current list of speakers' %}</a></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if perms.agenda.can_manage_agenda %}
|
|
||||||
<p>
|
|
||||||
<a href="{% url 'agenda_next_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'Next speaker' %}</a>
|
|
||||||
<a href="{% url 'agenda_end_speach_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'End speach' %}</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<small><p class="text-right"><a href="{% url 'agenda_current_list_of_speakers' %}"> {% trans 'Go to current list of speakers' %}...</a></p></small>
|
|
@ -1,6 +1,9 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
<li class="{% if agenda_is_active %}activeline{% endif %}">
|
<li class="{% if agenda_is_active %}activeline{% endif %}">
|
||||||
<a href="{% url 'projector_activate_slide' 'agenda' %}"
|
<a href="{% url 'projector_activate_slide' 'agenda' %}"
|
||||||
@ -53,5 +56,4 @@
|
|||||||
<li>{% trans 'No items available.' %}</li>
|
<li>{% trans 'No items available.' %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
<small><p class="text-right"><a href="{% url 'item_overview' %}">{% trans "More..." %}</a></p></small>
|
|
@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if perms.agenda.can_be_speaker %}
|
||||||
|
<p><a href="{% url 'agenda_add_to_current_list_of_speakers' %}" class="btn"><i class="icon icon-speaker"></i> {% trans 'Put me on the current list of speakers' %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.agenda.can_manage_agenda %}
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'agenda_next_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'Next speaker' %}</a>
|
||||||
|
<a href="{% url 'agenda_end_speach_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'End speach' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<small><p class="text-right"><a href="{% url 'agenda_current_list_of_speakers' %}">{% trans 'Go to current list of speakers' %} ...</a></p></small>
|
||||||
|
{% endblock %}
|
@ -8,7 +8,7 @@ urlpatterns = patterns(
|
|||||||
'',
|
'',
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
views.Overview.as_view(),
|
views.Overview.as_view(),
|
||||||
name='item_overview'),
|
name='item_overview'), # TODO: Rename this to item_list
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/$',
|
url(r'^(?P<pk>\d+)/$',
|
||||||
views.AgendaItemView.as_view(),
|
views.AgendaItemView.as_view(),
|
||||||
|
@ -13,7 +13,6 @@ from reportlab.platypus import Paragraph
|
|||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.projector.api import get_active_slide, update_projector
|
from openslides.projector.api import get_active_slide, update_projector
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.pdf import stylesheet
|
from openslides.utils.pdf import stylesheet
|
||||||
from openslides.utils.template import Tab
|
from openslides.utils.template import Tab
|
||||||
@ -636,39 +635,3 @@ def register_tab(request):
|
|||||||
permission=(request.user.has_perm('agenda.can_see_agenda') or
|
permission=(request.user.has_perm('agenda.can_see_agenda') or
|
||||||
request.user.has_perm('agenda.can_manage_agenda')),
|
request.user.has_perm('agenda.can_manage_agenda')),
|
||||||
selected=selected)
|
selected=selected)
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
"""
|
|
||||||
Returns the agenda widget for the projector tab.
|
|
||||||
"""
|
|
||||||
active_slide = get_active_slide()
|
|
||||||
if active_slide['callback'] == 'agenda':
|
|
||||||
agenda_is_active = active_slide.get('pk', 'agenda') == 'agenda'
|
|
||||||
active_type = active_slide.get('type', 'text')
|
|
||||||
else:
|
|
||||||
agenda_is_active = None
|
|
||||||
active_type = None
|
|
||||||
|
|
||||||
return [
|
|
||||||
Widget(
|
|
||||||
request,
|
|
||||||
name='agenda',
|
|
||||||
display_name=_('Agenda'),
|
|
||||||
template='agenda/widget.html',
|
|
||||||
context={
|
|
||||||
'agenda_is_active': agenda_is_active,
|
|
||||||
'items': Item.objects.all(),
|
|
||||||
'active_type': active_type},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=20),
|
|
||||||
|
|
||||||
Widget(
|
|
||||||
request,
|
|
||||||
name='append_to_list_of_speakers',
|
|
||||||
display_name=_('List of speakers'),
|
|
||||||
template='agenda/speaker_widget.html',
|
|
||||||
permission_required='agenda.can_be_speaker',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=30)]
|
|
||||||
|
50
openslides/agenda/widgets.py
Normal file
50
openslides/agenda/widgets.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
from openslides.projector.api import get_active_slide
|
||||||
|
|
||||||
|
from .models import Item
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaWidget(Widget):
|
||||||
|
"""
|
||||||
|
Agenda widget.
|
||||||
|
"""
|
||||||
|
name = 'agenda'
|
||||||
|
verbose_name = ugettext_lazy('Agenda')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 20
|
||||||
|
template_name = 'agenda/widget_item.html'
|
||||||
|
more_link_pattern_name = 'item_overview'
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
active_slide = get_active_slide()
|
||||||
|
if active_slide['callback'] == 'agenda':
|
||||||
|
agenda_is_active = active_slide.get('pk', 'agenda') == 'agenda'
|
||||||
|
active_type = active_slide.get('type', 'text')
|
||||||
|
else:
|
||||||
|
agenda_is_active = None
|
||||||
|
active_type = None
|
||||||
|
context.update({
|
||||||
|
'agenda_is_active': agenda_is_active,
|
||||||
|
'items': Item.objects.all(),
|
||||||
|
'active_type': active_type})
|
||||||
|
return super(AgendaWidget, self).get_context_data(**context)
|
||||||
|
|
||||||
|
|
||||||
|
class ListOfSpeakersWidget(Widget):
|
||||||
|
"""
|
||||||
|
Widget to control the list of speakers.
|
||||||
|
"""
|
||||||
|
name = 'append_to_list_of_speakers'
|
||||||
|
verbose_name = ugettext_lazy('List of speakers')
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 30
|
||||||
|
template_name = 'agenda/widget_list_of_speakers.html'
|
||||||
|
|
||||||
|
def check_permission(self):
|
||||||
|
return (self.request.user.has_perm('agenda.can_manage_agenda') or
|
||||||
|
self.request.user.has_perm('agenda.can_be_speaker'))
|
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import signals, slides # noqa
|
from . import signals, slides, widgets # noqa
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load tags %}
|
|
||||||
|
|
||||||
<ul style="line-height: 180%">
|
|
||||||
{% for assignment in assignments %}
|
|
||||||
<li class="{% if assignment.is_active_slide %}activeline{% endif %}">
|
|
||||||
<a href="{{ assignment|absolute_url:'projector' }}" class="activate_link btn {% if assignment.active %}btn-primary{% endif %} btn-mini"
|
|
||||||
rel="tooltip" data-original-title="{% trans 'Show' %}">
|
|
||||||
<i class="icon-facetime-video {% if assignment.active %}icon-white{% endif %}"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ assignment|absolute_url:'update' }}"
|
|
||||||
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
|
|
||||||
<i class="icon-pencil"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ assignment|absolute_url:'projector_preview' }}"
|
|
||||||
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
|
||||||
<i class="icon-search"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ assignment|absolute_url }}">{{ assignment }}</a>
|
|
||||||
</li>
|
|
||||||
{% empty %}
|
|
||||||
<li>{% trans 'No elections available.' %}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<small><p class="text-right"><a href="{% url 'assignment_list' %}">{% trans "More..." %}</a></p></small>
|
|
@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<ul style="line-height: 180%">
|
||||||
|
{% for assignment in assignments %}
|
||||||
|
<li class="{% if assignment.is_active_slide %}activeline{% endif %}">
|
||||||
|
<a href="{{ assignment|absolute_url:'projector' }}" class="activate_link btn {% if assignment.is_active_slide %}btn-primary{% endif %} btn-mini"
|
||||||
|
rel="tooltip" data-original-title="{% trans 'Show' %}">
|
||||||
|
<i class="icon-facetime-video {% if assignment.is_active_slide %}icon-white{% endif %}"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ assignment|absolute_url:'update' }}"
|
||||||
|
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
|
||||||
|
<i class="icon-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ assignment|absolute_url:'projector_preview' }}"
|
||||||
|
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
||||||
|
<i class="icon-search"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ assignment|absolute_url }}">{{ assignment }}</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li>{% trans 'No elections available.' %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
@ -17,7 +17,6 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate
|
|||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.participant.models import Group, User
|
from openslides.participant.models import Group, User
|
||||||
from openslides.poll.views import PollFormView
|
from openslides.poll.views import PollFormView
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
from openslides.utils.pdf import stylesheet
|
from openslides.utils.pdf import stylesheet
|
||||||
from openslides.utils.person import get_person
|
from openslides.utils.person import get_person
|
||||||
from openslides.utils.template import Tab
|
from openslides.utils.template import Tab
|
||||||
@ -622,15 +621,3 @@ def register_tab(request):
|
|||||||
request.user.has_perm('assignment.can_manage_assignment')),
|
request.user.has_perm('assignment.can_manage_assignment')),
|
||||||
selected=selected,
|
selected=selected,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
return [Widget(
|
|
||||||
request,
|
|
||||||
name='assignments',
|
|
||||||
display_name=_('Elections'),
|
|
||||||
template='assignment/widget.html',
|
|
||||||
context={'assignments': Assignment.objects.all()},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=50)]
|
|
||||||
|
25
openslides/assignment/widgets.py
Normal file
25
openslides/assignment/widgets.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
from .models import Assignment
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentWidget(Widget):
|
||||||
|
"""
|
||||||
|
Assignment widget.
|
||||||
|
"""
|
||||||
|
name = 'assignment'
|
||||||
|
verbose_name = ugettext_lazy('Elections')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 50
|
||||||
|
template_name = 'assignment/widget_assignment.html'
|
||||||
|
more_link_pattern_name = 'assignment_list'
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
return super(AssignmentWidget, self).get_context_data(
|
||||||
|
assignments=Assignment.objects.all(),
|
||||||
|
**context)
|
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import signals # noqa
|
from . import signals, widgets # noqa
|
||||||
|
3
openslides/core/static/styles/core.css
Normal file
3
openslides/core/static/styles/core.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.icon-welcome {
|
||||||
|
background-position: 0 -24px;
|
||||||
|
}
|
40
openslides/core/templates/core/widget.html
Normal file
40
openslides/core/templates/core/widget.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="widget" id="widget_{{ widget.name }}">
|
||||||
|
<div class="widget-header">
|
||||||
|
{% block header %}
|
||||||
|
<h3>
|
||||||
|
<i class="{{ widget.get_icon_css_class }}"></i>
|
||||||
|
<div class="collapsebutton btn-group right" data-toggle="buttons-checkbox">
|
||||||
|
<button type="button" class="btn btn-mini"
|
||||||
|
data-toggle="collapse" data-target="#widgetcontent_{{ widget.name }}"
|
||||||
|
title="{% trans 'Collapse widget content' %}">
|
||||||
|
_</button>
|
||||||
|
</div>
|
||||||
|
<div class="fixbutton btn-group right" data-toggle="buttons-checkbox">
|
||||||
|
<button type="button" class="btn btn-mini custom-btn-mini"
|
||||||
|
title="{% trans 'Fix widget position' %}">
|
||||||
|
<i class="icon-pushpin"></i></button>
|
||||||
|
</div>
|
||||||
|
{{ widget.get_verbose_name }}
|
||||||
|
</h3>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div class="widget-content collapse in" id="widgetcontent_{{ widget.name }}">
|
||||||
|
{% block content-wrapper %}
|
||||||
|
<div class="widget-content-inner">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div> <!-- widget-content-footer -->
|
||||||
|
{% block footer %}
|
||||||
|
{% if widget.get_url_for_more %}
|
||||||
|
<small>
|
||||||
|
<p class="text-right"><a href="{{ widget.get_url_for_more }}">{% trans 'More ...' %}</a></p>
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
12
openslides/core/templates/core/widget_welcome.html
Normal file
12
openslides/core/templates/core/widget_welcome.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with 'welcome_text'|get_config as welcometext %}
|
||||||
|
{% if welcometext %}
|
||||||
|
<p>{{ welcometext|safe|linebreaks }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
20
openslides/core/widgets.py
Normal file
20
openslides/core/widgets.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from openslides.config.api import config
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeWidget(Widget):
|
||||||
|
"""
|
||||||
|
Welcome widget with static info for all users.
|
||||||
|
"""
|
||||||
|
name = 'welcome'
|
||||||
|
permission_required = 'projector.can_see_dashboard'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 10
|
||||||
|
template_name = 'core/widget_welcome.html'
|
||||||
|
stylesheets = ['styles/core.css']
|
||||||
|
|
||||||
|
def get_verbose_name(self):
|
||||||
|
return config['welcome_title']
|
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import slides # noqa
|
from . import slides, widgets # noqa
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<form action="{% url 'target_pdf_page' %}" method="GET" class="set-page-form">
|
<form action="{% url 'target_pdf_page' %}" method="GET" class="set-page-form">
|
||||||
<div class="input-prepend" style="margin-bottom:0;">
|
<div class="input-prepend" style="margin-bottom:0;">
|
||||||
<a class="btn go-first-page"
|
<a class="btn go-first-page"
|
||||||
@ -16,27 +19,22 @@
|
|||||||
<i class="icon-forward"></i>
|
<i class="icon-forward"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-append" style="margin-bottom:0;">
|
<div class="input-append" style="margin-bottom:0;">
|
||||||
<a class="btn pdf-toggle-fullscreen {%if pdf_fullscreen %}btn-primary{% endif %}" href="{% url 'toggle_fullscreen' %}"
|
<a class="btn pdf-toggle-fullscreen {% if 'pdf_fullscreen'|get_config %}btn-primary{% endif %}" href="{% url 'toggle_fullscreen' %}"
|
||||||
rel="tooltip" data-original-title="{% trans 'Fullscreen' %}">
|
rel="tooltip" data-original-title="{% trans 'Fullscreen' %}">
|
||||||
<i class="icon-fullscreen {%if pdf_fullscreen %}icon-white{% endif %}"></i>
|
<i class="icon-fullscreen {% if 'pdf_fullscreen'|get_config %}icon-white{% endif %}"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-append input-prepend" style="margin-bottom:0;">
|
<div class="input-append input-prepend" style="margin-bottom:0;">
|
||||||
<span class="add-on">{% trans "Page" %}:</span>
|
<span class="add-on">{% trans "Page" %}:</span>
|
||||||
<input id="page_num" name="page_num" type="number" style="width: 22px;" value="{{ current_page }}">
|
<input id="page_num" name="page_num" type="number" style="width: 22px;" value="{{ current_page }}">
|
||||||
|
|
||||||
<button type="submit" id="go_to_page" class="btn tooltip-bottom"
|
<button type="submit" id="go_to_page" class="btn tooltip-bottom"
|
||||||
rel="tooltip" data-original-title="{% trans 'Apply' %}">
|
rel="tooltip" data-original-title="{% trans 'Apply' %}">
|
||||||
<i class="icon-refresh"></i>
|
<i class="icon-refresh"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
{% for pdf in pdfs %}
|
{% for pdf in pdfs %}
|
||||||
<li class="{% if pdf.is_active_slide %}activeline{% endif %}">
|
<li class="{% if pdf.is_active_slide %}activeline{% endif %}">
|
||||||
@ -48,6 +46,4 @@
|
|||||||
<li>{% trans 'No PDFs available.' %}</li>
|
<li>{% trans 'No PDFs available.' %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
<small><p class="text-right"><a href="{% url 'mediafile_list' %}">{% trans "More..." %}</a></p></small>
|
|
||||||
|
|
@ -6,7 +6,6 @@ from django.utils.translation import ugettext as _
|
|||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.projector.api import get_active_slide
|
from openslides.projector.api import get_active_slide
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
from openslides.utils.template import Tab
|
from openslides.utils.template import Tab
|
||||||
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
||||||
from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
|
from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
|
||||||
@ -190,32 +189,6 @@ class PdfToggleFullscreenView(RedirectView):
|
|||||||
return {'fullscreen': config['pdf_fullscreen']}
|
return {'fullscreen': config['pdf_fullscreen']}
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
"""
|
|
||||||
Return the widgets of the projector app
|
|
||||||
"""
|
|
||||||
widgets = []
|
|
||||||
|
|
||||||
# PDF-Presentation widget
|
|
||||||
pdfs = Mediafile.objects.filter(
|
|
||||||
filetype__in=Mediafile.PRESENTABLE_FILE_TYPES,
|
|
||||||
is_presentable=True
|
|
||||||
)
|
|
||||||
current_page = get_active_slide().get('page_num', 1)
|
|
||||||
widgets.append(Widget(
|
|
||||||
request,
|
|
||||||
name='presentations',
|
|
||||||
display_name=_('Presentations'),
|
|
||||||
template='mediafile/pdfs_widget.html',
|
|
||||||
context={'pdfs': pdfs, 'current_page': current_page,
|
|
||||||
'pdf_fullscreen': config['pdf_fullscreen']},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=75))
|
|
||||||
|
|
||||||
return widgets
|
|
||||||
|
|
||||||
|
|
||||||
def register_tab(request):
|
def register_tab(request):
|
||||||
"""
|
"""
|
||||||
Inserts a new Tab to the views for files.
|
Inserts a new Tab to the views for files.
|
||||||
|
32
openslides/mediafile/widgets.py
Normal file
32
openslides/mediafile/widgets.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
from openslides.projector.api import get_active_slide
|
||||||
|
|
||||||
|
from .models import Mediafile
|
||||||
|
|
||||||
|
|
||||||
|
class PDFPresentationWidget(Widget):
|
||||||
|
"""
|
||||||
|
Widget for presentable PDF files.
|
||||||
|
"""
|
||||||
|
name = 'presentations'
|
||||||
|
verbose_name = ugettext_lazy('Presentations')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 75
|
||||||
|
template_name = 'mediafile/widget_pdfpresentation.html'
|
||||||
|
more_link_pattern_name = 'mediafile_list'
|
||||||
|
#javascript_files = None # TODO: Add pdf.js stuff here.
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
pdfs = Mediafile.objects.filter(
|
||||||
|
filetype__in=Mediafile.PRESENTABLE_FILE_TYPES,
|
||||||
|
is_presentable=True)
|
||||||
|
current_page = get_active_slide().get('page_num', 1)
|
||||||
|
return super(PDFPresentationWidget, self).get_context_data(
|
||||||
|
pdfs=pdfs,
|
||||||
|
current_page=current_page,
|
||||||
|
**context)
|
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import signals, slides # noqa
|
from . import signals, slides, widgets # noqa
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load tags %}
|
|
||||||
|
|
||||||
<ul style="line-height: 180%">
|
|
||||||
{% for motion in motions %}
|
|
||||||
<li class="{% if motion.is_active_slide %}activeline{% endif %}">
|
|
||||||
<a href="{{ motion|absolute_url:'projector' }}" class="activate_link btn {% if motion.is_active_slide %}btn-primary{% endif %} btn-mini"
|
|
||||||
rel="tooltip" data-original-title="{% trans 'Show' %}">
|
|
||||||
<i class="icon-facetime-video {% if motion.is_active_slide %}icon-white{% endif %}"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ motion|absolute_url:'update' }}"
|
|
||||||
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
|
|
||||||
<i class="icon-pencil"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ motion|absolute_url:'projector_preview' }}"
|
|
||||||
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
|
||||||
<i class="icon-search"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ motion|absolute_url }}">
|
|
||||||
{{ motion.identifier|add:' | '|default:'' }}
|
|
||||||
{{ motion }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% empty %}
|
|
||||||
<li>{% trans 'No motions available.' %}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<small><p class="text-right"><a href="{% url 'motion_list' %}">{% trans "More..." %}</a></p></small>
|
|
31
openslides/motion/templates/motion/widget_motion.html
Normal file
31
openslides/motion/templates/motion/widget_motion.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<ul style="line-height: 180%">
|
||||||
|
{% for motion in motions %}
|
||||||
|
<li class="{% if motion.is_active_slide %}activeline{% endif %}">
|
||||||
|
<a href="{{ motion|absolute_url:'projector' }}" class="activate_link btn {% if motion.is_active_slide %}btn-primary{% endif %} btn-mini"
|
||||||
|
rel="tooltip" data-original-title="{% trans 'Show' %}">
|
||||||
|
<i class="icon-facetime-video {% if motion.is_active_slide %}icon-white{% endif %}"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ motion|absolute_url:'update' }}"
|
||||||
|
rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini right">
|
||||||
|
<i class="icon-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ motion|absolute_url:'projector_preview' }}"
|
||||||
|
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
||||||
|
<i class="icon-search"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ motion|absolute_url }}">
|
||||||
|
{{ motion.identifier|add:' | '|default:'' }}
|
||||||
|
{{ motion }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li>{% trans 'No motions available.' %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
@ -12,7 +12,6 @@ from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelate
|
|||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.poll.views import PollFormView
|
from openslides.poll.views import PollFormView
|
||||||
from openslides.projector.api import get_active_slide, update_projector
|
from openslides.projector.api import get_active_slide, update_projector
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
from openslides.utils.template import Tab
|
from openslides.utils.template import Tab
|
||||||
from openslides.utils.utils import html_strong, htmldiff
|
from openslides.utils.utils import html_strong, htmldiff
|
||||||
from openslides.utils.views import (CreateView, DeleteView, DetailView,
|
from openslides.utils.views import (CreateView, DeleteView, DetailView,
|
||||||
@ -824,20 +823,3 @@ def register_tab(request):
|
|||||||
url=reverse('motion_list'),
|
url=reverse('motion_list'),
|
||||||
permission=request.user.has_perm('motion.can_see_motion'),
|
permission=request.user.has_perm('motion.can_see_motion'),
|
||||||
selected=request.path.startswith('/motion/'))
|
selected=request.path.startswith('/motion/'))
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
"""
|
|
||||||
Return the motion widgets for the dashboard.
|
|
||||||
|
|
||||||
There is only one widget. It shows all motions.
|
|
||||||
"""
|
|
||||||
return [Widget(
|
|
||||||
request,
|
|
||||||
name='motions',
|
|
||||||
display_name=_('Motions'),
|
|
||||||
template='motion/widget.html',
|
|
||||||
context={'motions': Motion.objects.all()},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=40)]
|
|
||||||
|
25
openslides/motion/widgets.py
Normal file
25
openslides/motion/widgets.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
from .models import Motion
|
||||||
|
|
||||||
|
|
||||||
|
class MotionWidget(Widget):
|
||||||
|
"""
|
||||||
|
Motion widget.
|
||||||
|
"""
|
||||||
|
name = 'motion'
|
||||||
|
verbose_name = ugettext_lazy('Motions')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 40
|
||||||
|
template_name = 'motion/widget_motion.html'
|
||||||
|
more_link_pattern_name = 'motion_list'
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
return super(MotionWidget, self).get_context_data(
|
||||||
|
motions=Motion.objects.all(),
|
||||||
|
**context)
|
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import signals, slides # noqa
|
from . import signals, slides, widgets # noqa
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
<li class="{% if group.is_active_slide %}activeline{% endif %}">
|
<li class="{% if group.is_active_slide %}activeline{% endif %}">
|
||||||
@ -20,5 +23,4 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
<small><p class="text-right"><a href="{% url 'user_group_overview' %}">{% trans "More..." %}</a></p></small>
|
|
@ -1,5 +1,9 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<li class="{% if user.is_active_slide %}activeline{% endif %}">
|
<li class="{% if user.is_active_slide %}activeline{% endif %}">
|
||||||
@ -21,5 +25,4 @@
|
|||||||
<li>{% trans 'No participants available.' %}</li>
|
<li>{% trans 'No participants available.' %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
<small><p class="text-right"><a href="{% url 'user_overview' %}">{% trans "More..." %}</a></p></small>
|
|
@ -9,7 +9,7 @@ urlpatterns = patterns(
|
|||||||
# User
|
# User
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
views.UserOverview.as_view(),
|
views.UserOverview.as_view(),
|
||||||
name='user_overview'),
|
name='user_overview'), # TODO: Rename this to user_list
|
||||||
|
|
||||||
url(r'^new/$',
|
url(r'^new/$',
|
||||||
views.UserCreateView.as_view(),
|
views.UserCreateView.as_view(),
|
||||||
@ -53,7 +53,7 @@ urlpatterns = patterns(
|
|||||||
# Group
|
# Group
|
||||||
url(r'^group/$',
|
url(r'^group/$',
|
||||||
views.GroupOverview.as_view(),
|
views.GroupOverview.as_view(),
|
||||||
name='user_group_overview'),
|
name='user_group_overview'), # TODO: Rename this to group_list
|
||||||
|
|
||||||
url(r'^group/new/$',
|
url(r'^group/new/$',
|
||||||
views.GroupCreateView.as_view(),
|
views.GroupCreateView.as_view(),
|
||||||
|
@ -10,7 +10,6 @@ from django.utils.translation import ugettext as _
|
|||||||
from django.utils.translation import activate, ugettext_lazy
|
from django.utils.translation import activate, ugettext_lazy
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
from openslides.utils.template import Tab
|
from openslides.utils.template import Tab
|
||||||
from openslides.utils.utils import (delete_default_permissions, html_strong,
|
from openslides.utils.utils import (delete_default_permissions, html_strong,
|
||||||
template)
|
template)
|
||||||
@ -445,43 +444,3 @@ def register_tab(request):
|
|||||||
request.user.has_perm('participant.can_see_participant') or
|
request.user.has_perm('participant.can_see_participant') or
|
||||||
request.user.has_perm('participant.can_manage_participant')),
|
request.user.has_perm('participant.can_manage_participant')),
|
||||||
selected=selected)
|
selected=selected)
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
"""
|
|
||||||
Returns all widgets of the participant app. This is a user_widget
|
|
||||||
and a group_widget.
|
|
||||||
"""
|
|
||||||
return [get_user_widget(request), get_group_widget(request)]
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_widget(request):
|
|
||||||
"""
|
|
||||||
Provides a widget with all users. This is for short activation of
|
|
||||||
user slides.
|
|
||||||
"""
|
|
||||||
return Widget(
|
|
||||||
request,
|
|
||||||
name='user',
|
|
||||||
display_name=_('Participants'),
|
|
||||||
template='participant/user_widget.html',
|
|
||||||
context={'users': User.objects.all()},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=60)
|
|
||||||
|
|
||||||
|
|
||||||
def get_group_widget(request):
|
|
||||||
"""
|
|
||||||
Provides a widget with all groups. This is for short activation of
|
|
||||||
group slides.
|
|
||||||
"""
|
|
||||||
return Widget(
|
|
||||||
request,
|
|
||||||
name='group',
|
|
||||||
display_name=_('Groups'),
|
|
||||||
template='participant/group_widget.html',
|
|
||||||
context={'groups': Group.objects.all()},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=70)
|
|
||||||
|
45
openslides/participant/widgets.py
Normal file
45
openslides/participant/widgets.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
from .models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
class UserWidget(Widget):
|
||||||
|
"""
|
||||||
|
Provides a widget with all users. This is for short activation of
|
||||||
|
user slides.
|
||||||
|
"""
|
||||||
|
name = 'user'
|
||||||
|
verbose_name = ugettext_lazy('Participants')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 60
|
||||||
|
template_name = 'participant/widget_user.html'
|
||||||
|
more_link_pattern_name = 'user_overview'
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
return super(UserWidget, self).get_context_data(
|
||||||
|
users=User.objects.all(),
|
||||||
|
**context)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupWidget(Widget):
|
||||||
|
"""
|
||||||
|
Provides a widget with all groups. This is for short activation of
|
||||||
|
group slides.
|
||||||
|
"""
|
||||||
|
name = 'group'
|
||||||
|
verbose_name = ugettext_lazy('Groups')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 70
|
||||||
|
template_name = 'participant/widget_group.html'
|
||||||
|
more_link_pattern_name = 'user_group_overview'
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
return super(GroupWidget, self).get_context_data(
|
||||||
|
groups=Group.objects.all(),
|
||||||
|
**context)
|
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import signals, slides # noqa
|
from . import signals, slides, widgets # noqa
|
||||||
|
@ -3,10 +3,7 @@
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.datastructures import SortedDict
|
|
||||||
from django.utils.importlib import import_module
|
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
||||||
@ -224,39 +221,6 @@ def get_active_object():
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def get_all_widgets(request, session=False):
|
|
||||||
"""
|
|
||||||
Collects the widgets from all apps and returns the Widget objects as sorted
|
|
||||||
dictionary.
|
|
||||||
|
|
||||||
The session flag decides whether to return only the widgets which are
|
|
||||||
active, that means that they are mentioned in the session.
|
|
||||||
"""
|
|
||||||
all_module_widgets = []
|
|
||||||
# TODO: Do not touch the filesystem on any request
|
|
||||||
for app in settings.INSTALLED_APPS:
|
|
||||||
try:
|
|
||||||
mod = import_module(app + '.views')
|
|
||||||
except ImportError:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
mod_get_widgets = mod.get_widgets
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
module_widgets = mod_get_widgets(request)
|
|
||||||
all_module_widgets.extend(module_widgets)
|
|
||||||
all_module_widgets.sort(key=lambda widget: widget.default_weight)
|
|
||||||
session_widgets = request.session.get('widgets', {})
|
|
||||||
widgets = SortedDict()
|
|
||||||
for widget in all_module_widgets:
|
|
||||||
if (widget.permission_required is None or
|
|
||||||
request.user.has_perm(widget.permission_required)):
|
|
||||||
if not session or session_widgets.get(widget.get_name(), True):
|
|
||||||
widgets[widget.get_name()] = widget
|
|
||||||
return widgets
|
|
||||||
|
|
||||||
|
|
||||||
def start_countdown():
|
def start_countdown():
|
||||||
"""
|
"""
|
||||||
Starts the countdown
|
Starts the countdown
|
||||||
|
@ -1,62 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template import RequestContext
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
|
||||||
|
|
||||||
|
|
||||||
class Widget(object):
|
|
||||||
"""
|
|
||||||
Class for a Widget for the Projector-Tab.
|
|
||||||
"""
|
|
||||||
def __init__(self, request, name, html=None, template=None, context=None,
|
|
||||||
permission_required=None, display_name=None, default_column=1,
|
|
||||||
default_weight=0):
|
|
||||||
self.name = name
|
|
||||||
if display_name is None:
|
|
||||||
self.display_name = name.capitalize()
|
|
||||||
else:
|
|
||||||
self.display_name = display_name
|
|
||||||
|
|
||||||
if html is not None:
|
|
||||||
self.html = html
|
|
||||||
elif template is not None:
|
|
||||||
self.html = render_to_string(
|
|
||||||
template_name=template,
|
|
||||||
dictionary=context or {},
|
|
||||||
context_instance=RequestContext(request))
|
|
||||||
else:
|
|
||||||
raise OpenSlidesError('A Widget must have either a html or a template argument.')
|
|
||||||
self.permission_required = permission_required
|
|
||||||
self.default_column = default_column
|
|
||||||
self.default_weight = default_weight
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
"""
|
|
||||||
Returns the lower case of the widget name.
|
|
||||||
"""
|
|
||||||
return self.name.lower()
|
|
||||||
|
|
||||||
def get_html(self):
|
|
||||||
"""
|
|
||||||
Returns the html code of the widget.
|
|
||||||
"""
|
|
||||||
return self.html
|
|
||||||
|
|
||||||
def get_title(self):
|
|
||||||
"""
|
|
||||||
Returns the title of the widget.
|
|
||||||
"""
|
|
||||||
return self.display_name
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return repr(self.display_name)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return unicode(self.display_name)
|
|
||||||
|
|
||||||
|
|
||||||
class Overlay(object):
|
class Overlay(object):
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{% block header %}
|
{% block header %}
|
||||||
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/dashboard.css' %}" />
|
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/dashboard.css' %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript" src="{% static 'javascript/jquery-ui.custom.min.js' %}"></script>
|
<script type="text/javascript" src="{% static 'javascript/jquery-ui.custom.min.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'javascript/jquery.cookie.js' %}"></script>
|
<script type="text/javascript" src="{% static 'javascript/jquery.cookie.js' %}"></script>
|
||||||
@ -13,71 +14,31 @@
|
|||||||
<script type="text/javascript" src="{% static 'javascript/dashboard.js' %}"></script>
|
<script type="text/javascript" src="{% static 'javascript/dashboard.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% trans "Dashboard" %} – {{ block.super }}{% endblock %}
|
{% block title %}{% trans 'Dashboard' %} – {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans 'Dashboard' %}
|
<h1>{% trans 'Dashboard' %}
|
||||||
<small class="pull-right">
|
<small class="pull-right">
|
||||||
<a href="{% url 'projector_select_widgets' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage widgets' %}"><i class="icon-th-large"></i> {% trans 'Widgets' %}</a>
|
<a href="{% url 'projector_select_widgets' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage widgets' %}">
|
||||||
|
<i class="icon-th-large"></i>
|
||||||
|
{% trans 'Widgets' %}
|
||||||
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span6 column" id="col1">
|
<div class="span6 column" id="col1">
|
||||||
{% for name, widget in widgets.items %}
|
{% for widget in widgets %}
|
||||||
{% if widget.default_column == 1 %}
|
{% if widget.default_column == 1 %}
|
||||||
<div class="widget" id="widget_{{ widget.get_name }}">
|
{{ widget.get_html }}
|
||||||
<div class="widget-header">
|
{% endif %}
|
||||||
<h3>
|
{% endfor %}
|
||||||
<i class="{% if widget %}icon-{{widget.name}}{% else %}icon-star{% endif %}"></i>
|
</div>
|
||||||
<div class="collapsebutton btn-group right" data-toggle="buttons-checkbox">
|
<div class="span6 column" id="col2">
|
||||||
<button type="button" class="btn btn-mini"
|
{% for widget in widgets %}
|
||||||
data-toggle="collapse" data-target="#widgetcontent_{{ widget.get_name }}"
|
{% if widget.default_column == 2 %}
|
||||||
title="{% trans 'Collapse widget content' %}">
|
{{ widget.get_html }}
|
||||||
_</button>
|
{% endif %}
|
||||||
</div>
|
{% endfor %}
|
||||||
<div class="fixbutton btn-group right" data-toggle="buttons-checkbox">
|
</div>
|
||||||
<button type="button" class="btn btn-mini custom-btn-mini"
|
|
||||||
title="{% trans 'Fix widget position' %}">
|
|
||||||
<i class="icon-pushpin"></i></button>
|
|
||||||
</div>
|
|
||||||
{% trans widget.get_title %}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="widget-content collapse in" id="widgetcontent_{{ widget.get_name }}">
|
|
||||||
<div class="widget-content-inner">{{ widget.html }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div> <!-- end column-->
|
|
||||||
|
|
||||||
<div class="span6 column" id="col2">
|
|
||||||
{% for name, widget in widgets.items %}
|
|
||||||
{% if widget.default_column == 2 %}
|
|
||||||
<div class="widget" id="widget_{{ widget.get_name }}">
|
|
||||||
<div class="widget-header">
|
|
||||||
<h3>
|
|
||||||
<i class="{% if widget %}icon-{{widget.name}}{% else %}icon-star{% endif %}"></i>
|
|
||||||
<div class="collapsebutton btn-group right" data-toggle="buttons-checkbox">
|
|
||||||
<button type="button" class="btn btn-mini"
|
|
||||||
data-toggle="collapse" data-target="#widgetcontent_{{ widget.get_name }}"
|
|
||||||
title="{% trans 'Collapse widget content' %}">
|
|
||||||
_</button>
|
|
||||||
</div>
|
|
||||||
<div class="fixbutton btn-group right" data-toggle="buttons-checkbox">
|
|
||||||
<button type="button" class="btn btn-mini custom-btn-mini"
|
|
||||||
title="{% trans 'Fix widget position' %}">
|
|
||||||
<i class="icon-pushpin"></i></button>
|
|
||||||
</div>
|
|
||||||
{% trans widget.get_title %}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="widget-content collapse in" id="widgetcontent_{{ widget.get_name }}">
|
|
||||||
<div class="widget-content-inner">{{ widget.html }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div> <!-- end column -->
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
<ul class="unstyled">
|
<ul class="unstyled">
|
||||||
{% for widget_name, widget in widgets.items %}
|
{% for widget in widgets %}
|
||||||
<li>
|
<li>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
{{ widget.form.widget }} {{ widget }}
|
{{ widget.form.widget }} {{ widget }}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<ul style="line-height: 180%">
|
<ul style="line-height: 180%">
|
||||||
<li class="{% if welcomepage_is_active %}activeline{% endif %}">
|
<li class="{% if welcomepage_is_active %}activeline{% endif %}">
|
||||||
<a href="{% url 'projector_activate_slide' 'default' %}" class="activate_link btn {% if welcomepage_is_active %}btn-primary{% endif %} btn-mini"
|
<a href="{% url 'projector_activate_slide' 'default' %}" class="activate_link btn {% if welcomepage_is_active %}btn-primary{% endif %} btn-mini"
|
||||||
@ -11,7 +14,7 @@
|
|||||||
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
||||||
<i class="icon-search"></i>
|
<i class="icon-search"></i>
|
||||||
</a>
|
</a>
|
||||||
{% get_config 'welcome_title' %}
|
{{ 'welcome_title'|get_config }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
@ -43,3 +46,4 @@
|
|||||||
<a href="{% url 'customslide_new' %}" class="btn btn-mini right" style="margin: 10px 0;">
|
<a href="{% url 'customslide_new' %}" class="btn btn-mini right" style="margin: 10px 0;">
|
||||||
<i class="icon-plus"></i>{% trans 'New' %}
|
<i class="icon-plus"></i>{% trans 'New' %}
|
||||||
</a>
|
</a>
|
||||||
|
{% endblock %}
|
@ -1,6 +1,10 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
<!-- projector live view -->
|
<!-- projector live view -->
|
||||||
<a href="{% url 'projector_show' %}" target="_blank">
|
<a href="{% url 'projector_show' %}" target="_blank">
|
||||||
<div id="iframewrapper">
|
<div id="iframewrapper">
|
||||||
@ -37,3 +41,5 @@
|
|||||||
{% trans "Scroll level" %}: <span id="scroll_level">{{ 'projector_scroll'|get_config }}</span>
|
{% trans "Scroll level" %}: <span id="scroll_level">{{ 'projector_scroll'|get_config }}</span>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1,5 +1,8 @@
|
|||||||
|
{% extends 'core/widget.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
<ul class="overlay_list">
|
<ul class="overlay_list">
|
||||||
{% for overlay in overlays %}
|
{% for overlay in overlays %}
|
||||||
<li>
|
<li>
|
||||||
@ -19,3 +22,4 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endblock %}
|
@ -1,39 +1,42 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.context_processors import csrf
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
|
from openslides.mediafile.models import Mediafile
|
||||||
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
||||||
from openslides.utils.template import Tab
|
from openslides.utils.template import Tab
|
||||||
from openslides.utils.views import (AjaxMixin, CreateView, DeleteView,
|
from openslides.utils.views import (AjaxMixin, CreateView, DeleteView,
|
||||||
RedirectView, TemplateView, UpdateView)
|
RedirectView, TemplateView, UpdateView)
|
||||||
from openslides.mediafile.models import Mediafile
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
from .api import (call_on_projector, get_active_slide, get_all_widgets,
|
from .api import (call_on_projector, get_active_slide,
|
||||||
get_overlays, get_projector_content, get_projector_overlays,
|
get_overlays, get_projector_content, get_projector_overlays,
|
||||||
get_projector_overlays_js, reset_countdown, set_active_slide,
|
get_projector_overlays_js, reset_countdown, set_active_slide,
|
||||||
start_countdown, stop_countdown, update_projector_overlay)
|
start_countdown, stop_countdown, update_projector_overlay)
|
||||||
from .forms import SelectWidgetsForm
|
from .forms import SelectWidgetsForm
|
||||||
from .models import ProjectorSlide
|
from .models import ProjectorSlide
|
||||||
from .projector import Widget
|
|
||||||
from .signals import projector_overlays
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(AjaxMixin, TemplateView):
|
class DashboardView(AjaxMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Overview over all possible slides, the overlays and a liveview.
|
Overview over all possible slides, the overlays and a live view.
|
||||||
"""
|
"""
|
||||||
template_name = 'projector/dashboard.html'
|
template_name = 'projector/dashboard.html'
|
||||||
permission_required = 'projector.can_see_dashboard'
|
permission_required = 'projector.can_see_dashboard'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(DashboardView, self).get_context_data(**kwargs)
|
context = super(DashboardView, self).get_context_data(**kwargs)
|
||||||
|
widgets = []
|
||||||
context['widgets'] = get_all_widgets(self.request, session=True)
|
for widget in Widget.get_all(self.request):
|
||||||
|
if widget.is_active():
|
||||||
|
widgets.append(widget)
|
||||||
|
context['extra_stylefiles'].extend(widget.get_stylesheets())
|
||||||
|
context['extra_javascript'].extend(widget.get_javascript_files())
|
||||||
|
context['widgets'] = widgets
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -101,16 +104,16 @@ class SelectWidgetsView(TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
|
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
|
||||||
widgets = get_all_widgets(self.request)
|
|
||||||
activated_widgets = self.request.session.get('widgets', {})
|
widgets = Widget.get_all(self.request)
|
||||||
for name, widget in widgets.items():
|
for widget in widgets:
|
||||||
initial = {'widget': activated_widgets.get(name, True)}
|
initial = {'widget': widget.is_active()}
|
||||||
|
prefix = widget.name
|
||||||
if self.request.method == 'POST':
|
if self.request.method == 'POST':
|
||||||
widget.form = SelectWidgetsForm(self.request.POST, prefix=name,
|
widget.form = SelectWidgetsForm(self.request.POST, prefix=prefix,
|
||||||
initial=initial)
|
initial=initial)
|
||||||
else:
|
else:
|
||||||
widget.form = SelectWidgetsForm(prefix=name, initial=initial)
|
widget.form = SelectWidgetsForm(prefix=prefix, initial=initial)
|
||||||
|
|
||||||
context['widgets'] = widgets
|
context['widgets'] = widgets
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -119,16 +122,15 @@ class SelectWidgetsView(TemplateView):
|
|||||||
Activates or deactivates the widgets in a post request.
|
Activates or deactivates the widgets in a post request.
|
||||||
"""
|
"""
|
||||||
context = self.get_context_data(**kwargs)
|
context = self.get_context_data(**kwargs)
|
||||||
activated_widgets = self.request.session.get('widgets', {})
|
session_widgets = self.request.session.get('widgets', {})
|
||||||
|
for widget in context['widgets']:
|
||||||
for name, widget in context['widgets'].items():
|
|
||||||
if widget.form.is_valid():
|
if widget.form.is_valid():
|
||||||
activated_widgets[name] = widget.form.cleaned_data['widget']
|
session_widgets[widget.name] = widget.form.cleaned_data['widget']
|
||||||
else:
|
else:
|
||||||
messages.error(request, _('Errors in the form.'))
|
messages.error(request, _('Errors in the form.'))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.request.session['widgets'] = activated_widgets
|
self.request.session['widgets'] = session_widgets
|
||||||
return redirect(reverse('dashboard'))
|
return redirect(reverse('dashboard'))
|
||||||
|
|
||||||
|
|
||||||
@ -291,64 +293,3 @@ def register_tab(request):
|
|||||||
permission=request.user.has_perm('projector.can_see_dashboard'),
|
permission=request.user.has_perm('projector.can_see_dashboard'),
|
||||||
selected=selected,
|
selected=selected,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_widgets(request):
|
|
||||||
"""
|
|
||||||
Return the widgets of the projector app
|
|
||||||
"""
|
|
||||||
widgets = []
|
|
||||||
|
|
||||||
# Welcome widget
|
|
||||||
widgets.append(Widget(
|
|
||||||
request,
|
|
||||||
name='welcome',
|
|
||||||
display_name=config['welcome_title'],
|
|
||||||
template='projector/welcome_widget.html',
|
|
||||||
context={'welcometext': config['welcome_text']},
|
|
||||||
permission_required='projector.can_see_dashboard',
|
|
||||||
default_column=1,
|
|
||||||
default_weight=10))
|
|
||||||
|
|
||||||
# Projector live view widget
|
|
||||||
widgets.append(Widget(
|
|
||||||
request,
|
|
||||||
name='live_view',
|
|
||||||
display_name=_('Projector live view'),
|
|
||||||
template='projector/live_view_widget.html',
|
|
||||||
permission_required='projector.can_see_projector',
|
|
||||||
default_column=2,
|
|
||||||
default_weight=10))
|
|
||||||
|
|
||||||
# Overlay widget
|
|
||||||
overlays = []
|
|
||||||
for receiver, overlay in projector_overlays.send(sender='overlay_widget', request=request):
|
|
||||||
if overlay.widget_html_callback is not None:
|
|
||||||
overlays.append(overlay)
|
|
||||||
context = {'overlays': overlays}
|
|
||||||
context.update(csrf(request))
|
|
||||||
widgets.append(Widget(
|
|
||||||
request,
|
|
||||||
name='overlays',
|
|
||||||
display_name=_('Overlays'),
|
|
||||||
template='projector/overlay_widget.html',
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=2,
|
|
||||||
default_weight=20,
|
|
||||||
context=context))
|
|
||||||
|
|
||||||
# Custom slide widget
|
|
||||||
welcomepage_is_active = get_active_slide().get('callback', 'default') == 'default'
|
|
||||||
widgets.append(Widget(
|
|
||||||
request,
|
|
||||||
name='custom_slide',
|
|
||||||
display_name=_('Custom Slides'),
|
|
||||||
template='projector/custom_slide_widget.html',
|
|
||||||
context={
|
|
||||||
'slides': ProjectorSlide.objects.all().order_by('weight'),
|
|
||||||
'welcomepage_is_active': welcomepage_is_active},
|
|
||||||
permission_required='projector.can_manage_projector',
|
|
||||||
default_column=2,
|
|
||||||
default_weight=30))
|
|
||||||
|
|
||||||
return widgets
|
|
||||||
|
65
openslides/projector/widgets.py
Normal file
65
openslides/projector/widgets.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.core.context_processors import csrf
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
|
from openslides.projector.api import get_active_slide
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
from .models import ProjectorSlide
|
||||||
|
from .signals import projector_overlays
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorLiveWidget(Widget):
|
||||||
|
"""
|
||||||
|
Widget with a live view of the projector.
|
||||||
|
"""
|
||||||
|
name = 'live_view'
|
||||||
|
verbose_name = ugettext_lazy('Projector live view')
|
||||||
|
permission_required = 'projector.can_see_projector'
|
||||||
|
default_column = 2
|
||||||
|
default_weight = 10
|
||||||
|
template_name = 'projector/widget_live_view.html'
|
||||||
|
|
||||||
|
|
||||||
|
class OverlayWidget(Widget):
|
||||||
|
"""
|
||||||
|
Widget to control all overlays.
|
||||||
|
"""
|
||||||
|
name = 'overlays' # TODO: Use singular here
|
||||||
|
verbose_name = ugettext_lazy('Overlays')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 2
|
||||||
|
default_weight = 20
|
||||||
|
template_name = 'projector/widget_overlay.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
"""
|
||||||
|
Inserts all overlays into the context. The overlays are collected by
|
||||||
|
the projector_overlays signal.
|
||||||
|
"""
|
||||||
|
overlays = [overlay for __, overlay in projector_overlays.send(sender='overlay_widget', request=self.request)
|
||||||
|
if overlay.widget_html_callback is not None]
|
||||||
|
context.update(csrf(self.request))
|
||||||
|
return super(OverlayWidget, self).get_context_data(
|
||||||
|
overlays=overlays,
|
||||||
|
**context)
|
||||||
|
|
||||||
|
|
||||||
|
class CustonSlideWidget(Widget):
|
||||||
|
"""
|
||||||
|
Widget to control custom slides.
|
||||||
|
"""
|
||||||
|
name = 'custom_slide'
|
||||||
|
verbose_name = ugettext_lazy('Custom Slides')
|
||||||
|
permission_required = 'projector.can_manage_projector'
|
||||||
|
default_column = 2
|
||||||
|
default_weight = 30
|
||||||
|
template_name = 'projector/widget_custom_slide.html'
|
||||||
|
context = None
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
return super(CustonSlideWidget, self).get_context_data(
|
||||||
|
slides=ProjectorSlide.objects.all().order_by('weight'),
|
||||||
|
welcomepage_is_active=get_active_slide().get('callback', 'default') == 'default',
|
||||||
|
**context)
|
@ -304,9 +304,6 @@ legend + .control-group {
|
|||||||
.icon-config {
|
.icon-config {
|
||||||
background-position: -432px 0px;
|
background-position: -432px 0px;
|
||||||
}
|
}
|
||||||
.icon-welcome {
|
|
||||||
background-position: 0 -24px;
|
|
||||||
}
|
|
||||||
.icon-live_view {
|
.icon-live_view {
|
||||||
background-position: -432px -48px;
|
background-position: -432px -48px;
|
||||||
}
|
}
|
||||||
|
86
openslides/utils/dispatch.py
Normal file
86
openslides/utils/dispatch.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
class SignalConnectMetaClass(type):
|
||||||
|
"""
|
||||||
|
Metaclass to connect the children of a base class to a Django signal.
|
||||||
|
|
||||||
|
Classes must have a signal argument and a get_dispatch_uid classmethod.
|
||||||
|
The signal argument must be the Django signal the class should be
|
||||||
|
connected to. The get_dispatch_uid classmethod must return a unique
|
||||||
|
value for each child class and None for base classes which will not be
|
||||||
|
connected.
|
||||||
|
|
||||||
|
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, on 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 you want to filter some children.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
class Base(object):
|
||||||
|
__metaclass__ = SignalConnectMetaClass
|
||||||
|
signal = django.dispatch.Signal()
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
if not cls.__name__ == 'Base':
|
||||||
|
return cls.__name__
|
||||||
|
|
||||||
|
class Child(Base):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
child = Base.get_all(request)[0]
|
||||||
|
assert Child == type(child)
|
||||||
|
"""
|
||||||
|
def __new__(metaclass, class_name, class_parents, class_attributes):
|
||||||
|
"""
|
||||||
|
Creates the class and connects it to the signal if so.
|
||||||
|
"""
|
||||||
|
class_attributes['get_all'] = get_all_objects
|
||||||
|
new_class = super(SignalConnectMetaClass, metaclass).__new__(
|
||||||
|
metaclass, class_name, class_parents, class_attributes)
|
||||||
|
try:
|
||||||
|
dispatch_uid = new_class.get_dispatch_uid()
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError('Your class %s must have a get_dispatch_uid classmethod.' % class_name)
|
||||||
|
if dispatch_uid is not None:
|
||||||
|
try:
|
||||||
|
signal = new_class.signal
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError('Your class %s must have a signal argument, which must be a Django Signal instance.' % class_name)
|
||||||
|
else:
|
||||||
|
signal.connect(new_class, dispatch_uid=dispatch_uid)
|
||||||
|
if not hasattr(new_class, 'check_permission'):
|
||||||
|
setattr(new_class, 'check_permission', check_permission)
|
||||||
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_objects(cls, request):
|
||||||
|
"""
|
||||||
|
Collects all objects of the class created by the SignalConnectMetaClass
|
||||||
|
from all apps via signal. If they have a default weight, they are sorted.
|
||||||
|
Does not return objects where check_permission returns False.
|
||||||
|
|
||||||
|
Expects a request object.
|
||||||
|
|
||||||
|
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()]
|
||||||
|
if hasattr(cls, 'get_default_weight'):
|
||||||
|
all_objects.sort(key=lambda obj: obj.get_default_weight())
|
||||||
|
return all_objects
|
||||||
|
|
||||||
|
|
||||||
|
def check_permission(self):
|
||||||
|
"""
|
||||||
|
Returns True by default. Override this to filter some children on runtime.
|
||||||
|
|
||||||
|
This method is added to every instance of classes using the
|
||||||
|
SignalConnectMetaClass.
|
||||||
|
"""
|
||||||
|
return True
|
@ -54,6 +54,7 @@ class PermissionMixin(object):
|
|||||||
"""
|
"""
|
||||||
permission_required = NO_PERMISSION_REQUIRED
|
permission_required = NO_PERMISSION_REQUIRED
|
||||||
|
|
||||||
|
# TODO: Rename this to check_permission
|
||||||
def has_permission(self, request, *args, **kwargs):
|
def has_permission(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Checks if the user has the required permission.
|
Checks if the user has the required permission.
|
||||||
|
139
openslides/utils/widgets.py
Normal file
139
openslides/utils/widgets.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.dispatch import Signal
|
||||||
|
from django.template import RequestContext
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
from .dispatch import SignalConnectMetaClass
|
||||||
|
|
||||||
|
|
||||||
|
class Widget(object):
|
||||||
|
"""
|
||||||
|
Base class for a widget for the dashboard.
|
||||||
|
|
||||||
|
Every app which wants to add a widget to the dashboard has to create a
|
||||||
|
widget class subclassing from this base class. The name attribute has to
|
||||||
|
be set. The __metaclass__ attribute (SignalConnectMetaClass) does the
|
||||||
|
rest of the magic.
|
||||||
|
|
||||||
|
For the appearance of the widget there are some more attributes like
|
||||||
|
verbose_name, permission_required, default_column, default_weight,
|
||||||
|
default_active, template_name, context, icon, more_link_pattern_name,
|
||||||
|
stylesheets and javascript_files.
|
||||||
|
"""
|
||||||
|
__metaclass__ = SignalConnectMetaClass
|
||||||
|
signal = Signal(providing_args=['request'])
|
||||||
|
name = None
|
||||||
|
verbose_name = None
|
||||||
|
permission_required = None
|
||||||
|
default_column = 1
|
||||||
|
default_weight = 0
|
||||||
|
default_active = True
|
||||||
|
template_name = None
|
||||||
|
context = None
|
||||||
|
icon_css_class = None
|
||||||
|
more_link_pattern_name = None
|
||||||
|
stylesheets = None
|
||||||
|
javascript_files = None
|
||||||
|
|
||||||
|
def __init__(self, sender, request, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize the widget instance. This is done when the signal is sent.
|
||||||
|
|
||||||
|
Only the required request argument is used. Because of Django's signal
|
||||||
|
API, we have to take also a sender argument and wildcard keyword
|
||||||
|
arguments. But they are not used here.
|
||||||
|
"""
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.get_verbose_name())
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return unicode(self.get_verbose_name())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
"""
|
||||||
|
Returns the name as a unique string for each class. Returns None for
|
||||||
|
the base class so it will not be connected to the signal.
|
||||||
|
"""
|
||||||
|
return cls.name
|
||||||
|
|
||||||
|
def get_verbose_name(self):
|
||||||
|
"""
|
||||||
|
Returns a human readable name of the widget.
|
||||||
|
"""
|
||||||
|
return self.verbose_name or self.name.capitalize()
|
||||||
|
|
||||||
|
def check_permission(self):
|
||||||
|
"""
|
||||||
|
Returns True if the request user is allowed to see the widget.
|
||||||
|
"""
|
||||||
|
return self.permission_required is None or self.request.user.has_perm(self.permission_required)
|
||||||
|
|
||||||
|
def get_default_weight(self):
|
||||||
|
"""
|
||||||
|
Returns the default weight of the widget.
|
||||||
|
"""
|
||||||
|
return self.default_weight
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
"""
|
||||||
|
Returns True if the widget is active to be displayed.
|
||||||
|
"""
|
||||||
|
session_widgets = self.request.session.get('widgets', {})
|
||||||
|
return session_widgets.get(self.name, self.default_active)
|
||||||
|
|
||||||
|
def get_html(self):
|
||||||
|
"""
|
||||||
|
Returns the html code of the widget.
|
||||||
|
|
||||||
|
This method also adds the widget itself to the context.
|
||||||
|
"""
|
||||||
|
if self.template_name is not None:
|
||||||
|
html = render_to_string(
|
||||||
|
template_name=self.template_name,
|
||||||
|
dictionary=self.get_context_data(widget=self),
|
||||||
|
context_instance=RequestContext(self.request))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('A widget class must define either a get_html '
|
||||||
|
'method or have template_name argument.')
|
||||||
|
return html
|
||||||
|
|
||||||
|
def get_context_data(self, **context):
|
||||||
|
"""
|
||||||
|
Returns the context data for the widget template.
|
||||||
|
"""
|
||||||
|
return_context = self.context or {}
|
||||||
|
return_context.update(context)
|
||||||
|
return return_context
|
||||||
|
|
||||||
|
def get_icon_css_class(self):
|
||||||
|
"""
|
||||||
|
Returns the css class name of the icon.
|
||||||
|
"""
|
||||||
|
return self.icon_css_class or 'icon-%s' % self.name
|
||||||
|
|
||||||
|
def get_url_for_more(self):
|
||||||
|
"""
|
||||||
|
Returns the url for the link 'More ...' in the base template.
|
||||||
|
"""
|
||||||
|
if self.more_link_pattern_name is not None:
|
||||||
|
url = reverse(self.more_link_pattern_name)
|
||||||
|
else:
|
||||||
|
url = None
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_stylesheets(self):
|
||||||
|
"""
|
||||||
|
Returns an interable of stylesheets to be loaded.
|
||||||
|
"""
|
||||||
|
return iter(self.stylesheets or [])
|
||||||
|
|
||||||
|
def get_javascript_files(self):
|
||||||
|
"""
|
||||||
|
Returns an interable of javascript files to be loaded.
|
||||||
|
"""
|
||||||
|
return iter(self.javascript_files or [])
|
51
tests/core/test_widgets.py
Normal file
51
tests/core/test_widgets.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetObject(TestCase):
|
||||||
|
request_factory = RequestFactory()
|
||||||
|
|
||||||
|
def get_widget(self, name):
|
||||||
|
request = self.request_factory.get('/')
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
for widget in Widget.get_all(request):
|
||||||
|
if widget.name == name:
|
||||||
|
value = widget
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
value = False
|
||||||
|
return value
|
||||||
|
|
||||||
|
def test_connecting_signal(self):
|
||||||
|
|
||||||
|
class TestWidgetOne(Widget):
|
||||||
|
name = 'test_case_widget_begae7poh1Ahshohfi1r'
|
||||||
|
|
||||||
|
self.assertTrue(self.get_widget('test_case_widget_begae7poh1Ahshohfi1r'))
|
||||||
|
|
||||||
|
def test_not_connecting_signal(self):
|
||||||
|
|
||||||
|
class TestWidgetTwo(Widget):
|
||||||
|
name = 'test_case_widget_zuRietaewiCii9mahDah'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.assertFalse(self.get_widget('test_case_widget_zuRietaewiCii9mahDah'))
|
||||||
|
|
||||||
|
def test_missing_template(self):
|
||||||
|
|
||||||
|
class TestWidgetThree(Widget):
|
||||||
|
name = 'test_widget_raiLaiPhahQuahngeer4'
|
||||||
|
|
||||||
|
widget = self.get_widget('test_widget_raiLaiPhahQuahngeer4')
|
||||||
|
self.assertRaisesMessage(
|
||||||
|
NotImplementedError,
|
||||||
|
'A widget class must define either a get_html method or have template_name argument.',
|
||||||
|
widget.get_html)
|
@ -51,8 +51,8 @@ class ActivateViewTest(TestCase):
|
|||||||
|
|
||||||
view.pre_redirect(view.request, callback='some_callback')
|
view.pre_redirect(view.request, callback='some_callback')
|
||||||
|
|
||||||
mock_set_active_slide.called_with('some_callback',
|
mock_set_active_slide.assert_called_with('some_callback',
|
||||||
{'some_key': 'some_value'})
|
**{'some_key': 'some_value'})
|
||||||
mock_config.get_default.assert_has_calls([call('projector_scroll'),
|
mock_config.get_default.assert_has_calls([call('projector_scroll'),
|
||||||
call('projector_scale')])
|
call('projector_scale')])
|
||||||
self.assertEqual(mock_config.__setitem__.call_count, 2)
|
self.assertEqual(mock_config.__setitem__.call_count, 2)
|
||||||
@ -64,30 +64,31 @@ class SelectWidgetsViewTest(TestCase):
|
|||||||
|
|
||||||
@patch('openslides.projector.views.SelectWidgetsForm')
|
@patch('openslides.projector.views.SelectWidgetsForm')
|
||||||
@patch('openslides.projector.views.TemplateView.get_context_data')
|
@patch('openslides.projector.views.TemplateView.get_context_data')
|
||||||
@patch('openslides.projector.views.get_all_widgets')
|
@patch('openslides.projector.views.Widget')
|
||||||
def test_get_context_data(self, mock_get_all_widgets, mock_get_context_data,
|
def test_get_context_data(self, mock_Widget, mock_get_context_data,
|
||||||
mock_SelectWidgetsForm):
|
mock_SelectWidgetsForm):
|
||||||
view = views.SelectWidgetsView()
|
view = views.SelectWidgetsView()
|
||||||
view.request = self.rf.get('/')
|
view.request = self.rf.get('/')
|
||||||
view.request.session = MagicMock()
|
view.request.session = MagicMock()
|
||||||
widget = MagicMock()
|
widget = MagicMock()
|
||||||
widget.name.return_value = 'some_widget'
|
widget.name = 'some_widget_Bohsh1Pa0eeziRaihu8O'
|
||||||
mock_get_all_widgets.return_value = {'some_widget': widget}
|
widget.is_active.return_value = True
|
||||||
|
mock_Widget.get_all.return_value = [widget]
|
||||||
mock_get_context_data.return_value = {}
|
mock_get_context_data.return_value = {}
|
||||||
|
|
||||||
# Test get
|
# Test get
|
||||||
context = view.get_context_data()
|
context = view.get_context_data()
|
||||||
self.assertIn('widgets', context)
|
self.assertIn('widgets', context)
|
||||||
self.assertIn('some_widget', context['widgets'])
|
self.assertIn(widget, context['widgets'])
|
||||||
mock_SelectWidgetsForm.called_with(
|
mock_SelectWidgetsForm.assert_called_with(
|
||||||
prefix='some_widget', initial={'widget': True})
|
prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
|
||||||
|
|
||||||
# Test post
|
# Test post
|
||||||
view.request = self.rf.post('/')
|
view.request = self.rf.post('/')
|
||||||
view.request.session = MagicMock()
|
view.request.session = MagicMock()
|
||||||
context = view.get_context_data()
|
context = view.get_context_data()
|
||||||
mock_SelectWidgetsForm.called_with(
|
mock_SelectWidgetsForm.assert_called_with(
|
||||||
view.request.POST, prefix='some_widget', initial={'widget': True})
|
view.request.POST, prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
|
||||||
|
|
||||||
@patch('openslides.projector.views.messages')
|
@patch('openslides.projector.views.messages')
|
||||||
def test_post(self, mock_messages):
|
def test_post(self, mock_messages):
|
||||||
@ -95,14 +96,14 @@ class SelectWidgetsViewTest(TestCase):
|
|||||||
view.request = self.rf.post('/')
|
view.request = self.rf.post('/')
|
||||||
view.request.session = {}
|
view.request.session = {}
|
||||||
widget = MagicMock()
|
widget = MagicMock()
|
||||||
widget.name.return_value = 'some_widget'
|
widget.name = 'some_widget_ahgaeree8JeReichue8u'
|
||||||
context = {'widgets': {'some_widget': widget}}
|
context = {'widgets': [widget]}
|
||||||
mock_context_data = MagicMock(return_value=context)
|
mock_context_data = MagicMock(return_value=context)
|
||||||
|
|
||||||
with patch('openslides.projector.views.SelectWidgetsView.get_context_data', mock_context_data):
|
with patch('openslides.projector.views.SelectWidgetsView.get_context_data', mock_context_data):
|
||||||
widget.form.is_valid.return_value = True
|
widget.form.is_valid.return_value = True
|
||||||
view.post(view.request)
|
view.post(view.request)
|
||||||
self.assertIn('some_widget', view.request.session['widgets'])
|
self.assertIn('some_widget_ahgaeree8JeReichue8u', view.request.session['widgets'])
|
||||||
|
|
||||||
# Test with errors in form
|
# Test with errors in form
|
||||||
widget.form.is_valid.return_value = False
|
widget.form.is_valid.return_value = False
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from django.http import HttpRequest
|
|
||||||
|
|
||||||
from openslides.projector.projector import Widget
|
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class WidgetObject(TestCase):
|
|
||||||
def test_error(self):
|
|
||||||
with self.assertRaises(OpenSlidesError):
|
|
||||||
Widget(HttpRequest(), name='chahghuyeim8ie0Noong')
|
|
||||||
|
|
||||||
def test_repr(self):
|
|
||||||
w = Widget(HttpRequest(), name='abcdefgäöüß', html='<strong>html</strong>')
|
|
||||||
self.assertEqual(repr(w), repr('Abcdefgäöüß'))
|
|
80
tests/utils/test_dispatch.py
Normal file
80
tests/utils/test_dispatch.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.dispatch import Signal
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from openslides.utils.dispatch import SignalConnectMetaClass
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseOne(object):
|
||||||
|
__metaclass__ = SignalConnectMetaClass
|
||||||
|
signal = Signal()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
if not cls.__name__ == 'TestBaseOne':
|
||||||
|
return 'test_vieM1eingi6luish5Sei'
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseTwo(object):
|
||||||
|
__metaclass__ = SignalConnectMetaClass
|
||||||
|
signal = Signal()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestSignalConnectMetaClass(TestCase):
|
||||||
|
request_factory = RequestFactory()
|
||||||
|
|
||||||
|
@patch('tests.utils.test_dispatch.TestBaseOne.signal')
|
||||||
|
def test_call_signal_send(self, mock_signal):
|
||||||
|
TestBaseOne.get_all(self.request_factory.request)
|
||||||
|
self.assertTrue(mock_signal.send.called)
|
||||||
|
|
||||||
|
@patch('tests.utils.test_dispatch.TestBaseOne.signal')
|
||||||
|
def test_call_signal_connect(self, mock_signal):
|
||||||
|
class TestChildOne(TestBaseOne):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertTrue(mock_signal.connect.called)
|
||||||
|
self.assertEqual(mock_signal.connect.call_args[0][0], TestChildOne)
|
||||||
|
self.assertEqual(mock_signal.connect.call_args[1], dict(dispatch_uid='test_vieM1eingi6luish5Sei'))
|
||||||
|
|
||||||
|
def test_bad_base_class(self):
|
||||||
|
def wrapper():
|
||||||
|
class BadClass1(object):
|
||||||
|
__metaclass__ = SignalConnectMetaClass
|
||||||
|
self.assertRaisesMessage(
|
||||||
|
NotImplementedError,
|
||||||
|
'Your class BadClass1 must have a get_dispatch_uid classmethod.',
|
||||||
|
wrapper)
|
||||||
|
|
||||||
|
def test_bad_base_class_without_signal(self):
|
||||||
|
def wrapper():
|
||||||
|
class BadClass2(object):
|
||||||
|
__metaclass__ = SignalConnectMetaClass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(cls):
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.assertRaisesMessage(
|
||||||
|
NotImplementedError,
|
||||||
|
'Your class BadClass2 must have a signal argument, which must be a Django Signal instance.',
|
||||||
|
wrapper)
|
||||||
|
|
||||||
|
def test_receive_signal(self):
|
||||||
|
class TestChildTwo(TestBaseTwo):
|
||||||
|
def __init__(self, sender, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_dispatch_uid(self):
|
||||||
|
return 'test_leeve5eighahT3zooxe5'
|
||||||
|
|
||||||
|
childtwo = TestBaseTwo.get_all(self.request_factory.request)[0]
|
||||||
|
self.assertEqual(type(childtwo), TestChildTwo)
|
Loading…
Reference in New Issue
Block a user