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]
|
||||
|
||||
Other:
|
||||
- Changed widget api. Used new metaclass.
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
test()
|
||||
local('coverage report -m --fail-under=75')
|
||||
local('coverage report -m --fail-under=76')
|
||||
|
||||
|
||||
def coverage():
|
||||
|
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import widgets # noqa
|
@ -1,6 +1,9 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block content %}
|
||||
<ul style="line-height: 180%">
|
||||
{% trans 'I am on the list of speakers of the following items:' %}
|
||||
{% for item in items %}
|
||||
@ -25,7 +28,7 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if config_motion_min_supporters %}
|
||||
{% if 'motion_min_supporters'|get_config %}
|
||||
<hr />
|
||||
<ul style="line-height: 180%">
|
||||
{% trans 'I support the following motions:' %}
|
||||
@ -55,3 +58,4 @@
|
||||
<li><i>{% trans 'None' %}</i></li>
|
||||
{% endfor %}
|
||||
</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 -*-
|
||||
|
||||
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 tags %}
|
||||
|
||||
{% block content %}
|
||||
<ul style="line-height: 180%">
|
||||
<li class="{% if agenda_is_active %}activeline{% endif %}">
|
||||
<a href="{% url 'projector_activate_slide' 'agenda' %}"
|
||||
@ -53,5 +56,4 @@
|
||||
<li>{% trans 'No items available.' %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<small><p class="text-right"><a href="{% url 'item_overview' %}">{% trans "More..." %}</a></p></small>
|
||||
{% endblock %}
|
@ -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'^$',
|
||||
views.Overview.as_view(),
|
||||
name='item_overview'),
|
||||
name='item_overview'), # TODO: Rename this to item_list
|
||||
|
||||
url(r'^(?P<pk>\d+)/$',
|
||||
views.AgendaItemView.as_view(),
|
||||
|
@ -13,7 +13,6 @@ from reportlab.platypus import Paragraph
|
||||
|
||||
from openslides.config.api import config
|
||||
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.pdf import stylesheet
|
||||
from openslides.utils.template import Tab
|
||||
@ -636,39 +635,3 @@ def register_tab(request):
|
||||
permission=(request.user.has_perm('agenda.can_see_agenda') or
|
||||
request.user.has_perm('agenda.can_manage_agenda')),
|
||||
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 -*-
|
||||
|
||||
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.participant.models import Group, User
|
||||
from openslides.poll.views import PollFormView
|
||||
from openslides.projector.projector import Widget
|
||||
from openslides.utils.pdf import stylesheet
|
||||
from openslides.utils.person import get_person
|
||||
from openslides.utils.template import Tab
|
||||
@ -622,15 +621,3 @@ def register_tab(request):
|
||||
request.user.has_perm('assignment.can_manage_assignment')),
|
||||
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 -*-
|
||||
|
||||
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
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import slides # noqa
|
||||
from . import slides, widgets # noqa
|
||||
|
@ -1,6 +1,9 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{% url 'target_pdf_page' %}" method="GET" class="set-page-form">
|
||||
<div class="input-prepend" style="margin-bottom:0;">
|
||||
<a class="btn go-first-page"
|
||||
@ -16,27 +19,22 @@
|
||||
<i class="icon-forward"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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' %}">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="input-append input-prepend" style="margin-bottom:0;">
|
||||
<span class="add-on">{% trans "Page" %}:</span>
|
||||
<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"
|
||||
rel="tooltip" data-original-title="{% trans 'Apply' %}">
|
||||
<i class="icon-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<ul style="line-height: 180%">
|
||||
{% for pdf in pdfs %}
|
||||
<li class="{% if pdf.is_active_slide %}activeline{% endif %}">
|
||||
@ -48,6 +46,4 @@
|
||||
<li>{% trans 'No PDFs available.' %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<small><p class="text-right"><a href="{% url 'mediafile_list' %}">{% trans "More..." %}</a></p></small>
|
||||
|
||||
{% endblock %}
|
@ -6,7 +6,6 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from openslides.config.api import config
|
||||
from openslides.projector.api import get_active_slide
|
||||
from openslides.projector.projector import Widget
|
||||
from openslides.utils.template import Tab
|
||||
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
||||
from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView,
|
||||
@ -190,32 +189,6 @@ class PdfToggleFullscreenView(RedirectView):
|
||||
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):
|
||||
"""
|
||||
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 -*-
|
||||
|
||||
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.poll.views import PollFormView
|
||||
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.utils import html_strong, htmldiff
|
||||
from openslides.utils.views import (CreateView, DeleteView, DetailView,
|
||||
@ -824,20 +823,3 @@ def register_tab(request):
|
||||
url=reverse('motion_list'),
|
||||
permission=request.user.has_perm('motion.can_see_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 -*-
|
||||
|
||||
from . import signals, slides # noqa
|
||||
from . import signals, slides, widgets # noqa
|
||||
|
@ -1,6 +1,9 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block content %}
|
||||
<ul style="line-height: 180%">
|
||||
{% for group in groups %}
|
||||
<li class="{% if group.is_active_slide %}activeline{% endif %}">
|
||||
@ -20,5 +23,4 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<small><p class="text-right"><a href="{% url 'user_group_overview' %}">{% trans "More..." %}</a></p></small>
|
||||
{% endblock %}
|
@ -1,5 +1,9 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block content %}
|
||||
<ul style="line-height: 180%">
|
||||
{% for user in users %}
|
||||
<li class="{% if user.is_active_slide %}activeline{% endif %}">
|
||||
@ -21,5 +25,4 @@
|
||||
<li>{% trans 'No participants available.' %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<small><p class="text-right"><a href="{% url 'user_overview' %}">{% trans "More..." %}</a></p></small>
|
||||
{% endblock %}
|
@ -9,7 +9,7 @@ urlpatterns = patterns(
|
||||
# User
|
||||
url(r'^$',
|
||||
views.UserOverview.as_view(),
|
||||
name='user_overview'),
|
||||
name='user_overview'), # TODO: Rename this to user_list
|
||||
|
||||
url(r'^new/$',
|
||||
views.UserCreateView.as_view(),
|
||||
@ -53,7 +53,7 @@ urlpatterns = patterns(
|
||||
# Group
|
||||
url(r'^group/$',
|
||||
views.GroupOverview.as_view(),
|
||||
name='user_group_overview'),
|
||||
name='user_group_overview'), # TODO: Rename this to group_list
|
||||
|
||||
url(r'^group/new/$',
|
||||
views.GroupCreateView.as_view(),
|
||||
|
@ -10,7 +10,6 @@ from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import activate, ugettext_lazy
|
||||
|
||||
from openslides.config.api import config
|
||||
from openslides.projector.projector import Widget
|
||||
from openslides.utils.template import Tab
|
||||
from openslides.utils.utils import (delete_default_permissions, html_strong,
|
||||
template)
|
||||
@ -445,43 +444,3 @@ def register_tab(request):
|
||||
request.user.has_perm('participant.can_see_participant') or
|
||||
request.user.has_perm('participant.can_manage_participant')),
|
||||
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 -*-
|
||||
|
||||
from . import signals, slides # noqa
|
||||
from . import signals, slides, widgets # noqa
|
||||
|
@ -3,10 +3,7 @@
|
||||
from json import dumps
|
||||
from time import time
|
||||
|
||||
from django.conf import settings
|
||||
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.utils.tornado_webserver import ProjectorSocketHandler
|
||||
@ -224,39 +221,6 @@ def get_active_object():
|
||||
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():
|
||||
"""
|
||||
Starts the countdown
|
||||
|
@ -1,62 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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.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):
|
||||
|
@ -6,6 +6,7 @@
|
||||
{% block header %}
|
||||
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/dashboard.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript" src="{% static 'javascript/jquery-ui.custom.min.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>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% trans "Dashboard" %} – {{ block.super }}{% endblock %}
|
||||
{% block title %}{% trans 'Dashboard' %} – {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% trans 'Dashboard' %}
|
||||
<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>
|
||||
</h1>
|
||||
<div class="row-fluid">
|
||||
<div class="span6 column" id="col1">
|
||||
{% for name, widget in widgets.items %}
|
||||
{% if widget.default_column == 1 %}
|
||||
<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 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 class="span6 column" id="col1">
|
||||
{% for widget in widgets %}
|
||||
{% if widget.default_column == 1 %}
|
||||
{{ widget.get_html }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="span6 column" id="col2">
|
||||
{% for widget in widgets %}
|
||||
{% if widget.default_column == 2 %}
|
||||
{{ widget.get_html }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
<ul class="unstyled">
|
||||
{% for widget_name, widget in widgets.items %}
|
||||
{% for widget in widgets %}
|
||||
<li>
|
||||
<label class="checkbox">
|
||||
{{ widget.form.widget }} {{ widget }}
|
||||
|
@ -1,6 +1,9 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block content %}
|
||||
<ul style="line-height: 180%">
|
||||
<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"
|
||||
@ -11,7 +14,7 @@
|
||||
rel="tooltip" data-original-title="{% trans 'Preview' %}" class="btn btn-mini right">
|
||||
<i class="icon-search"></i>
|
||||
</a>
|
||||
{% get_config 'welcome_title' %}
|
||||
{{ 'welcome_title'|get_config }}
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
@ -43,3 +46,4 @@
|
||||
<a href="{% url 'customslide_new' %}" class="btn btn-mini right" style="margin: 10px 0;">
|
||||
<i class="icon-plus"></i>{% trans 'New' %}
|
||||
</a>
|
||||
{% endblock %}
|
@ -1,6 +1,10 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load tags %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- projector live view -->
|
||||
<a href="{% url 'projector_show' %}" target="_blank">
|
||||
<div id="iframewrapper">
|
||||
@ -37,3 +41,5 @@
|
||||
{% trans "Scroll level" %}: <span id="scroll_level">{{ 'projector_scroll'|get_config }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -1,5 +1,8 @@
|
||||
{% extends 'core/widget.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<ul class="overlay_list">
|
||||
{% for overlay in overlays %}
|
||||
<li>
|
||||
@ -19,3 +22,4 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
@ -1,39 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openslides.config.api import config
|
||||
from openslides.mediafile.models import Mediafile
|
||||
from openslides.utils.tornado_webserver import ProjectorSocketHandler
|
||||
from openslides.utils.template import Tab
|
||||
from openslides.utils.views import (AjaxMixin, CreateView, DeleteView,
|
||||
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_projector_overlays_js, reset_countdown, set_active_slide,
|
||||
start_countdown, stop_countdown, update_projector_overlay)
|
||||
from .forms import SelectWidgetsForm
|
||||
from .models import ProjectorSlide
|
||||
from .projector import Widget
|
||||
from .signals import projector_overlays
|
||||
|
||||
|
||||
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'
|
||||
permission_required = 'projector.can_see_dashboard'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DashboardView, self).get_context_data(**kwargs)
|
||||
|
||||
context['widgets'] = get_all_widgets(self.request, session=True)
|
||||
widgets = []
|
||||
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
|
||||
|
||||
|
||||
@ -101,16 +104,16 @@ class SelectWidgetsView(TemplateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
|
||||
widgets = get_all_widgets(self.request)
|
||||
activated_widgets = self.request.session.get('widgets', {})
|
||||
for name, widget in widgets.items():
|
||||
initial = {'widget': activated_widgets.get(name, True)}
|
||||
|
||||
widgets = Widget.get_all(self.request)
|
||||
for widget in widgets:
|
||||
initial = {'widget': widget.is_active()}
|
||||
prefix = widget.name
|
||||
if self.request.method == 'POST':
|
||||
widget.form = SelectWidgetsForm(self.request.POST, prefix=name,
|
||||
widget.form = SelectWidgetsForm(self.request.POST, prefix=prefix,
|
||||
initial=initial)
|
||||
else:
|
||||
widget.form = SelectWidgetsForm(prefix=name, initial=initial)
|
||||
|
||||
widget.form = SelectWidgetsForm(prefix=prefix, initial=initial)
|
||||
context['widgets'] = widgets
|
||||
return context
|
||||
|
||||
@ -119,16 +122,15 @@ class SelectWidgetsView(TemplateView):
|
||||
Activates or deactivates the widgets in a post request.
|
||||
"""
|
||||
context = self.get_context_data(**kwargs)
|
||||
activated_widgets = self.request.session.get('widgets', {})
|
||||
|
||||
for name, widget in context['widgets'].items():
|
||||
session_widgets = self.request.session.get('widgets', {})
|
||||
for widget in context['widgets']:
|
||||
if widget.form.is_valid():
|
||||
activated_widgets[name] = widget.form.cleaned_data['widget']
|
||||
session_widgets[widget.name] = widget.form.cleaned_data['widget']
|
||||
else:
|
||||
messages.error(request, _('Errors in the form.'))
|
||||
break
|
||||
else:
|
||||
self.request.session['widgets'] = activated_widgets
|
||||
self.request.session['widgets'] = session_widgets
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
|
||||
@ -291,64 +293,3 @@ def register_tab(request):
|
||||
permission=request.user.has_perm('projector.can_see_dashboard'),
|
||||
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 {
|
||||
background-position: -432px 0px;
|
||||
}
|
||||
.icon-welcome {
|
||||
background-position: 0 -24px;
|
||||
}
|
||||
.icon-live_view {
|
||||
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
|
||||
|
||||
# TODO: Rename this to check_permission
|
||||
def has_permission(self, request, *args, **kwargs):
|
||||
"""
|
||||
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')
|
||||
|
||||
mock_set_active_slide.called_with('some_callback',
|
||||
{'some_key': 'some_value'})
|
||||
mock_set_active_slide.assert_called_with('some_callback',
|
||||
**{'some_key': 'some_value'})
|
||||
mock_config.get_default.assert_has_calls([call('projector_scroll'),
|
||||
call('projector_scale')])
|
||||
self.assertEqual(mock_config.__setitem__.call_count, 2)
|
||||
@ -64,30 +64,31 @@ class SelectWidgetsViewTest(TestCase):
|
||||
|
||||
@patch('openslides.projector.views.SelectWidgetsForm')
|
||||
@patch('openslides.projector.views.TemplateView.get_context_data')
|
||||
@patch('openslides.projector.views.get_all_widgets')
|
||||
def test_get_context_data(self, mock_get_all_widgets, mock_get_context_data,
|
||||
@patch('openslides.projector.views.Widget')
|
||||
def test_get_context_data(self, mock_Widget, mock_get_context_data,
|
||||
mock_SelectWidgetsForm):
|
||||
view = views.SelectWidgetsView()
|
||||
view.request = self.rf.get('/')
|
||||
view.request.session = MagicMock()
|
||||
widget = MagicMock()
|
||||
widget.name.return_value = 'some_widget'
|
||||
mock_get_all_widgets.return_value = {'some_widget': widget}
|
||||
widget.name = 'some_widget_Bohsh1Pa0eeziRaihu8O'
|
||||
widget.is_active.return_value = True
|
||||
mock_Widget.get_all.return_value = [widget]
|
||||
mock_get_context_data.return_value = {}
|
||||
|
||||
# Test get
|
||||
context = view.get_context_data()
|
||||
self.assertIn('widgets', context)
|
||||
self.assertIn('some_widget', context['widgets'])
|
||||
mock_SelectWidgetsForm.called_with(
|
||||
prefix='some_widget', initial={'widget': True})
|
||||
self.assertIn(widget, context['widgets'])
|
||||
mock_SelectWidgetsForm.assert_called_with(
|
||||
prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
|
||||
|
||||
# Test post
|
||||
view.request = self.rf.post('/')
|
||||
view.request.session = MagicMock()
|
||||
context = view.get_context_data()
|
||||
mock_SelectWidgetsForm.called_with(
|
||||
view.request.POST, prefix='some_widget', initial={'widget': True})
|
||||
mock_SelectWidgetsForm.assert_called_with(
|
||||
view.request.POST, prefix='some_widget_Bohsh1Pa0eeziRaihu8O', initial={'widget': True})
|
||||
|
||||
@patch('openslides.projector.views.messages')
|
||||
def test_post(self, mock_messages):
|
||||
@ -95,14 +96,14 @@ class SelectWidgetsViewTest(TestCase):
|
||||
view.request = self.rf.post('/')
|
||||
view.request.session = {}
|
||||
widget = MagicMock()
|
||||
widget.name.return_value = 'some_widget'
|
||||
context = {'widgets': {'some_widget': widget}}
|
||||
widget.name = 'some_widget_ahgaeree8JeReichue8u'
|
||||
context = {'widgets': [widget]}
|
||||
mock_context_data = MagicMock(return_value=context)
|
||||
|
||||
with patch('openslides.projector.views.SelectWidgetsView.get_context_data', mock_context_data):
|
||||
widget.form.is_valid.return_value = True
|
||||
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
|
||||
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