Merge branch 'stable/1.6.x'
Conflicts: CHANGELOG openslides/users/signals.py
This commit is contained in:
commit
34b6ca80f3
@ -16,11 +16,14 @@ Other:
|
|||||||
template signals and slides.
|
template signals and slides.
|
||||||
|
|
||||||
|
|
||||||
Version 1.7.0 (unreleased)
|
Version 1.7 (unreleased)
|
||||||
==========================
|
========================
|
||||||
[https://github.com/OpenSlides/OpenSlides/milestones/1.7]
|
[https://github.com/OpenSlides/OpenSlides/milestones/1.7]
|
||||||
|
|
||||||
Motions:
|
Core:
|
||||||
|
- New feature to tag motions, agenda and assignments.
|
||||||
|
Motion:
|
||||||
|
- New Feature to create amendments, which are related to a parent motion.
|
||||||
- Added possibility to hide motions from non staff users in some states.
|
- Added possibility to hide motions from non staff users in some states.
|
||||||
Other:
|
Other:
|
||||||
- Cleaned up utils.views to increase performance when fetching single objects
|
- Cleaned up utils.views to increase performance when fetching single objects
|
||||||
|
@ -30,7 +30,7 @@ class ItemForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = ('item_number', 'title', 'text', 'comment', 'type', 'duration', 'parent', 'speaker_list_closed')
|
fields = ('item_number', 'title', 'text', 'comment', 'tags', 'type', 'duration', 'parent', 'speaker_list_closed')
|
||||||
widgets = {'text': CKEditorWidget(config_name='images')}
|
widgets = {'text': CKEditorWidget(config_name='images')}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
|
|||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
|
from openslides.core.models import Tag
|
||||||
from openslides.projector.api import (get_active_slide, reset_countdown,
|
from openslides.projector.api import (get_active_slide, reset_countdown,
|
||||||
start_countdown, stop_countdown,
|
start_countdown, stop_countdown,
|
||||||
update_projector, update_projector_overlay)
|
update_projector, update_projector_overlay)
|
||||||
@ -107,6 +108,11 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
|
|||||||
True, if the list of speakers is closed.
|
True, if the list of speakers is closed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
tags = models.ManyToManyField(Tag, blank=True)
|
||||||
|
"""
|
||||||
|
Tags to categorise agenda items.
|
||||||
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_see_agenda', ugettext_noop("Can see agenda")),
|
('can_see_agenda', ugettext_noop("Can see agenda")),
|
||||||
|
@ -34,8 +34,22 @@
|
|||||||
<h1>{% trans "Agenda" %}
|
<h1>{% trans "Agenda" %}
|
||||||
<small class="pull-right">
|
<small class="pull-right">
|
||||||
{% if perms.agenda.can_manage_agenda %}
|
{% if perms.agenda.can_manage_agenda %}
|
||||||
<a href="{% url 'item_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New item' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a>
|
<a href="{% url 'item_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New item' %}">
|
||||||
<a href="{% url 'item_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}"><i class="icon-import"></i> {% trans "Import" %}</a>
|
<i class="icon-plus icon-white"></i>
|
||||||
|
{% trans "New" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.core.can_manage_tags %}
|
||||||
|
<a href="{% url 'core_tag_list' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage tags' %}">
|
||||||
|
<i class="icon-th"></i>
|
||||||
|
<span class="optional-small"> {% trans 'Tags' %}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.agenda.can_manage_agenda %}
|
||||||
|
<a href="{% url 'item_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}">
|
||||||
|
<i class="icon-import"></i>
|
||||||
|
{% trans "Import" %}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'print_agenda' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print agenda as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a>
|
<a href="{% url 'print_agenda' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print agenda as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a>
|
||||||
{% if perms.core.can_see_projector %}
|
{% if perms.core.can_see_projector %}
|
||||||
|
@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _
|
|||||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||||
|
|
||||||
from openslides.agenda.models import Item, Speaker
|
from openslides.agenda.models import Item, Speaker
|
||||||
|
from openslides.core.models import Tag
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.poll.models import (BaseOption, BasePoll, BaseVote,
|
from openslides.poll.models import (BaseOption, BasePoll, BaseVote,
|
||||||
CollectDefaultVotesMixin,
|
CollectDefaultVotesMixin,
|
||||||
@ -56,6 +57,7 @@ class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
max_length=79, null=True, blank=True,
|
max_length=79, null=True, blank=True,
|
||||||
verbose_name=ugettext_lazy("Default comment on the ballot paper"))
|
verbose_name=ugettext_lazy("Default comment on the ballot paper"))
|
||||||
status = models.CharField(max_length=3, choices=STATUS, default='sea')
|
status = models.CharField(max_length=3, choices=STATUS, default='sea')
|
||||||
|
tags = models.ManyToManyField(Tag, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
|
@ -21,6 +21,12 @@
|
|||||||
{% if perms.assignment.can_manage_assignment %}
|
{% if perms.assignment.can_manage_assignment %}
|
||||||
<a href="{% url 'assignment_create' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New election' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a>
|
<a href="{% url 'assignment_create' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New election' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.core.can_manage_tags %}
|
||||||
|
<a href="{% url 'core_tag_list' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage tags' %}">
|
||||||
|
<i class="icon-th"></i>
|
||||||
|
<span class="optional-small"> {% trans 'Tags' %}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if perms.assignment.can_see_assignment %}
|
{% if perms.assignment.can_see_assignment %}
|
||||||
<a href="{% url 'assignment_list_pdf' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print all elections as PDF' %}" target="_blank"><i class="icon-print"></i><span class="optional-small"> PDF</span></a>
|
<a href="{% url 'assignment_list_pdf' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print all elections as PDF' %}" target="_blank"><i class="icon-print"></i><span class="optional-small"> PDF</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -152,6 +152,7 @@ class ConfigGroup(object):
|
|||||||
A simple object class representing a group of variables (tuple) with
|
A simple object class representing a group of variables (tuple) with
|
||||||
a special title.
|
a special title.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title, variables):
|
def __init__(self, title, variables):
|
||||||
self.title = title
|
self.title = title
|
||||||
self.variables = variables
|
self.variables = variables
|
||||||
|
5
openslides/core/exceptions.py
Normal file
5
openslides/core/exceptions.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
|
|
||||||
|
|
||||||
|
class TagException(OpenSlidesError):
|
||||||
|
pass
|
@ -44,3 +44,20 @@ class CustomSlide(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
else:
|
else:
|
||||||
url = super(CustomSlide, self).get_absolute_url(link)
|
url = super(CustomSlide, self).get_absolute_url(link)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(AbsoluteUrlMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model to save tags.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255, unique=True,
|
||||||
|
verbose_name=ugettext_lazy('Tag'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['name']
|
||||||
|
permissions = (
|
||||||
|
('can_manage_tags', ugettext_noop('Can manage tags')), )
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
111
openslides/core/static/js/config_tags.js
Normal file
111
openslides/core/static/js/config_tags.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Functions to alter the tags via ajax-commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
// The HTML-input field, in which the tag-name can be altered
|
||||||
|
var insert_element = $('#tag-edit');
|
||||||
|
|
||||||
|
// Boolean value to block second insert-ajax-requests before the pk was returned
|
||||||
|
var insert_is_blocked = false;
|
||||||
|
|
||||||
|
// Clears the HTML-input field to add new tags
|
||||||
|
$('#tag-save').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
insert_element.val('');
|
||||||
|
// This is the important line, where the name-attribute of the html-element is set to new
|
||||||
|
insert_element.attr('name', 'new');
|
||||||
|
insert_is_blocked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The same happens, if the enter key (keycode==13) was pressed inside the input element
|
||||||
|
insert_element.keypress(function(event) {
|
||||||
|
if ( event.which == 13 ) {
|
||||||
|
event.preventDefault();
|
||||||
|
$('#tag-save').trigger('click');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change the tag which will be updated
|
||||||
|
$('.tag-edit').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var edit_element = $(this);
|
||||||
|
insert_element.val(edit_element.parents('.tag-row').children('.tag-name').html());
|
||||||
|
// This is the important line, where the name-attribute of the html-elemtnt is set to edit
|
||||||
|
insert_element.attr('name', 'edit-' + edit_element.parents('.tag-row').attr('id'));
|
||||||
|
insert_is_blocked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Code when the delete button of a tag is clicked. Send the ajax-request and
|
||||||
|
// remove the tag-element from the DOM, when the request was a success.
|
||||||
|
$('.tag-del').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var delete_element = $(this);
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
name: 'delete-' + delete_element.parents('.tag-row').attr('id')},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.action == 'deleted') {
|
||||||
|
delete_element.parents('.tag-row').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the changed data, when new input is in the insert element.
|
||||||
|
// Use the 'input'-event instead of the 'change'-event, so new data is send
|
||||||
|
// event when the element does not loose the focus.
|
||||||
|
insert_element.on('input', function(event) {
|
||||||
|
// Only send data, if insert_is_blocked is false
|
||||||
|
if (!insert_is_blocked) {
|
||||||
|
// block the insert when a new tag is send to the server
|
||||||
|
if (insert_element.attr('name') == 'new') {
|
||||||
|
insert_is_blocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
// Sends the data to the current page
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
name: insert_element.attr('name'),
|
||||||
|
value: insert_element.val()},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.action == 'created') {
|
||||||
|
// If a new tag was created, use the hidden dummy-tag as template
|
||||||
|
// to create a new tag-line
|
||||||
|
// Known bug: the element is added at the end of the list and
|
||||||
|
// not in alphabetic order. This will be fixed with angular
|
||||||
|
var new_element = $('#dummy-tag').clone(withDataAndEvents=true);
|
||||||
|
new_element.attr('id', 'tag-' + data.pk);
|
||||||
|
new_element.children('.tag-name').html(insert_element.val());
|
||||||
|
new_element.appendTo('#tag-table');
|
||||||
|
new_element.slideDown();
|
||||||
|
|
||||||
|
// Set the insert-element to edit the new created tag and unblock the
|
||||||
|
// ajax-method
|
||||||
|
insert_element.attr('name', 'edit-tag-' + data.pk);
|
||||||
|
insert_is_blocked = false;
|
||||||
|
|
||||||
|
} else if (data.action == 'updated') {
|
||||||
|
// If a existing tag was altered, change it.
|
||||||
|
$('#tag-' + data.pk).children('.tag-name').html(insert_element.val());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
insert_element.parent().addClass('error');
|
||||||
|
if (insert_element.attr('name') == 'new') {
|
||||||
|
// Remove the block, even if an error happend, so we can send a
|
||||||
|
// new name for the tag
|
||||||
|
insert_is_blocked = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
insert_element.parent().removeClass('error');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -72,6 +72,21 @@ $(function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set the csrftoken to send post-data via ajax. See:
|
||||||
|
// https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
|
||||||
|
var csrftoken = $.cookie('csrftoken');
|
||||||
|
function csrfSafeMethod(method) {
|
||||||
|
// these HTTP methods do not require CSRF protection
|
||||||
|
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
|
||||||
|
}
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<link href="{% static 'css/base.css' %}" type="text/css" rel="stylesheet" />
|
<link href="{% static 'css/base.css' %}" type="text/css" rel="stylesheet" />
|
||||||
<link href="{% static 'css/chatbox.css' %}" type="text/css" rel="stylesheet" />
|
<link href="{% static 'css/chatbox.css' %}" type="text/css" rel="stylesheet" />
|
||||||
<link href="{% static 'img/favicon.png' %}" type="image/png" rel="shortcut icon" />
|
<link href="{% static 'img/favicon.png' %}" type="image/png" rel="shortcut icon" />
|
||||||
|
<link href="{% static 'css/jquery.bsmselect.css' %}" type="text/css" rel="stylesheet" />
|
||||||
{% for stylefile in extra_stylefiles %}
|
{% for stylefile in extra_stylefiles %}
|
||||||
<link href="{% static stylefile %}" type="text/css" rel="stylesheet" />
|
<link href="{% static stylefile %}" type="text/css" rel="stylesheet" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -132,6 +133,18 @@
|
|||||||
<script src="{% static 'js/utils.js' %}" type="text/javascript"></script>
|
<script src="{% static 'js/utils.js' %}" type="text/javascript"></script>
|
||||||
<script src="{% static 'js/chatbox.js' %}" type="text/javascript"></script>
|
<script src="{% static 'js/chatbox.js' %}" type="text/javascript"></script>
|
||||||
<script src="{% url 'django.views.i18n.javascript_catalog' %}" type="text/javascript"></script>
|
<script src="{% url 'django.views.i18n.javascript_catalog' %}" type="text/javascript"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'js/jquery/jquery.bsmselect.js' %}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// use jquery-bsmselect for all <select multiple> form elements
|
||||||
|
$("select[multiple]").bsmSelect({
|
||||||
|
removeLabel: '<sup><b>X</b></sup>',
|
||||||
|
containerClass: 'bsmContainer',
|
||||||
|
listClass: 'bsmList-custom',
|
||||||
|
listItemClass: 'bsmListItem-custom',
|
||||||
|
listItemLabelClass: 'bsmListItemLabel-custom',
|
||||||
|
removeClass: 'bsmListItemRemove-custom'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% for javascript in extra_javascript %}
|
{% for javascript in extra_javascript %}
|
||||||
<script src="{% static javascript %}" type="text/javascript"></script>
|
<script src="{% static javascript %}" type="text/javascript"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
54
openslides/core/templates/core/tag_list.html
Normal file
54
openslides/core/templates/core/tag_list.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Tags" %} – {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Tags" %}</h1>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="tag-edit">Name:</label>
|
||||||
|
<input id="tag-edit" name="new">
|
||||||
|
<a href="#" id="tag-save" class="btn btn-primary">{% trans 'Save' %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="tag-table" class="table table-striped table-bordered">
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Tag name" %}</th>
|
||||||
|
<th class="mini_width">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="dummy-tag" class="tag-row" style="display:none">
|
||||||
|
<td class="tag-name"></td>
|
||||||
|
<td>
|
||||||
|
<span style="width: 1px; white-space: nowrap;">
|
||||||
|
<a href="#" rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini tag-edit">
|
||||||
|
<i class="icon-pencil "></i>
|
||||||
|
</a>
|
||||||
|
<a href="#" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini tag-del">
|
||||||
|
<i class="icon-remove"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% for tag in tag_list %}
|
||||||
|
<tr id="tag-{{ tag.pk }}" class="tag-row">
|
||||||
|
<td class="tag-name">{{ tag }}</td>
|
||||||
|
<td>
|
||||||
|
<span style="width: 1px; white-space: nowrap;">
|
||||||
|
<a href="#" rel="tooltip" data-original-title="{% trans 'Edit' %}" class="btn btn-mini tag-edit">
|
||||||
|
<i class="icon-pencil "></i>
|
||||||
|
</a>
|
||||||
|
<a href="#" rel="tooltip" data-original-title="{% trans 'Delete' %}" class="btn btn-mini tag-del">
|
||||||
|
<i class="icon-remove"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'js/config_tags.js' %}" type="text/javascript"></script>
|
||||||
|
{% endblock %}
|
@ -39,4 +39,8 @@ urlpatterns = patterns(
|
|||||||
url(r'^customslide/(?P<pk>\d+)/del/$',
|
url(r'^customslide/(?P<pk>\d+)/del/$',
|
||||||
views.CustomSlideDeleteView.as_view(),
|
views.CustomSlideDeleteView.as_view(),
|
||||||
name='customslide_delete'),
|
name='customslide_delete'),
|
||||||
|
|
||||||
|
url(r'tags/$',
|
||||||
|
views.TagListView.as_view(),
|
||||||
|
name='core_tag_list'),
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,7 @@ from django.conf import settings
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.shortcuts import redirect, render_to_response
|
from django.shortcuts import redirect, render_to_response
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
@ -17,7 +18,8 @@ from openslides.utils import views as utils_views
|
|||||||
from openslides.utils.widgets import Widget
|
from openslides.utils.widgets import Widget
|
||||||
|
|
||||||
from .forms import SelectWidgetsForm
|
from .forms import SelectWidgetsForm
|
||||||
from .models import CustomSlide
|
from .models import CustomSlide, Tag
|
||||||
|
from .exceptions import TagException
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView):
|
class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView):
|
||||||
@ -30,7 +32,7 @@ class DashboardView(utils_views.AjaxMixin, utils_views.TemplateView):
|
|||||||
template_name = 'core/dashboard.html'
|
template_name = 'core/dashboard.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(DashboardView, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
widgets = []
|
widgets = []
|
||||||
for widget in Widget.get_all(self.request):
|
for widget in Widget.get_all(self.request):
|
||||||
if widget.is_active():
|
if widget.is_active():
|
||||||
@ -51,7 +53,7 @@ class SelectWidgetsView(utils_views.TemplateView):
|
|||||||
template_name = 'core/select_widgets.html'
|
template_name = 'core/select_widgets.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(SelectWidgetsView, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
widgets = Widget.get_all(self.request)
|
widgets = Widget.get_all(self.request)
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
initial = {'widget': widget.is_active()}
|
initial = {'widget': widget.is_active()}
|
||||||
@ -93,7 +95,7 @@ class VersionView(utils_views.TemplateView):
|
|||||||
"""
|
"""
|
||||||
Adds version strings to the context.
|
Adds version strings to the context.
|
||||||
"""
|
"""
|
||||||
context = super(VersionView, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# OpenSlides version. During development the git commit id is added.
|
# OpenSlides version. During development the git commit id is added.
|
||||||
if RELEASE:
|
if RELEASE:
|
||||||
description = ''
|
description = ''
|
||||||
@ -119,7 +121,7 @@ class SearchView(_SearchView):
|
|||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
if not request.user.is_authenticated() and not config['system_enable_anonymous']:
|
if not request.user.is_authenticated() and not config['system_enable_anonymous']:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return super(SearchView, self).__call__(request)
|
return super().__call__(request)
|
||||||
|
|
||||||
def extra_context(self):
|
def extra_context(self):
|
||||||
"""
|
"""
|
||||||
@ -211,3 +213,81 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView):
|
|||||||
Delete a custom slide.
|
Delete a custom slide.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TagListView(utils_views.AjaxMixin, utils_views.ListView):
|
||||||
|
"""
|
||||||
|
View to list and manipulate tags.
|
||||||
|
|
||||||
|
Shows all tags when requested via a GET-request. Manipulates tags with
|
||||||
|
POST-requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Tag
|
||||||
|
required_permission = 'core.can_manage_tags'
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
return self.ajax_get(*args, **kwargs)
|
||||||
|
|
||||||
|
def ajax_get(self, request, *args, **kwargs):
|
||||||
|
name, value = request.POST['name'], request.POST.get('value', None)
|
||||||
|
|
||||||
|
# Create a new tag
|
||||||
|
if name == 'new':
|
||||||
|
try:
|
||||||
|
tag = Tag.objects.create(name=value)
|
||||||
|
except IntegrityError:
|
||||||
|
# The name of the tag is already taken. It must be unique.
|
||||||
|
self.error = 'Tag name is already taken'
|
||||||
|
else:
|
||||||
|
self.pk = tag.pk
|
||||||
|
self.action = 'created'
|
||||||
|
|
||||||
|
# Update an existing tag
|
||||||
|
elif name.startswith('edit-tag-'):
|
||||||
|
try:
|
||||||
|
self.get_tag_queryset(name, 9).update(name=value)
|
||||||
|
except TagException as error:
|
||||||
|
self.error = str(error)
|
||||||
|
except IntegrityError:
|
||||||
|
self.error = 'Tag name is already taken'
|
||||||
|
except Tag.DoesNotExist:
|
||||||
|
self.error = 'Tag does not exist'
|
||||||
|
else:
|
||||||
|
self.action = 'updated'
|
||||||
|
|
||||||
|
# Delete a tag
|
||||||
|
elif name.startswith('delete-tag-'):
|
||||||
|
try:
|
||||||
|
self.get_tag_queryset(name, 11).delete()
|
||||||
|
except TagException as error:
|
||||||
|
self.error = str(error)
|
||||||
|
except Tag.DoesNotExist:
|
||||||
|
self.error = 'Tag does not exist'
|
||||||
|
else:
|
||||||
|
self.action = 'deleted'
|
||||||
|
return super().ajax_get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_tag_queryset(self, name, place_in_str):
|
||||||
|
"""
|
||||||
|
Get a django-tag-queryset from a string.
|
||||||
|
|
||||||
|
'name' is the string in which the pk is (at the end).
|
||||||
|
|
||||||
|
'place_in_str' is the place where to look for the pk. It has to be an int.
|
||||||
|
|
||||||
|
Returns a Tag QuerySet or raises TagException.
|
||||||
|
Also sets self.pk to the pk inside the name.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.pk = int(name[place_in_str:])
|
||||||
|
except ValueError:
|
||||||
|
raise TagException('Invalid name in request')
|
||||||
|
return Tag.objects.filter(pk=self.pk)
|
||||||
|
|
||||||
|
def get_ajax_context(self, **context):
|
||||||
|
return super().get_ajax_context(
|
||||||
|
pk=getattr(self, 'pk', None),
|
||||||
|
action=getattr(self, 'action', None),
|
||||||
|
error=getattr(self, 'error', None),
|
||||||
|
**context)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
from openslides.config.api import config
|
|
||||||
from openslides.mediafile.models import Mediafile
|
from openslides.mediafile.models import Mediafile
|
||||||
from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin,
|
from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin,
|
||||||
CSVImportForm, LocalizedModelChoiceField)
|
CSVImportForm, LocalizedModelChoiceField)
|
||||||
@ -9,7 +8,7 @@ from openslides.users.models import User
|
|||||||
|
|
||||||
from ckeditor.widgets import CKEditorWidget
|
from ckeditor.widgets import CKEditorWidget
|
||||||
|
|
||||||
from .models import Category, Motion, Workflow
|
from .models import Category, Motion, Workflow, Tag
|
||||||
|
|
||||||
|
|
||||||
class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
||||||
@ -47,6 +46,11 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
|||||||
Attachments of the motion.
|
Attachments of the motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
tags = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=ugettext_lazy('Tags'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Motion
|
model = Motion
|
||||||
fields = ()
|
fields = ()
|
||||||
@ -54,7 +58,7 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fill the FormFields related to the version data with initial data.
|
Fill the FormFields related to the version data with initial data.
|
||||||
Fill also the initial data for attachments.
|
Fill also the initial data for attachments and tags.
|
||||||
"""
|
"""
|
||||||
self.motion = kwargs.get('instance', None)
|
self.motion = kwargs.get('instance', None)
|
||||||
self.initial = kwargs.setdefault('initial', {})
|
self.initial = kwargs.setdefault('initial', {})
|
||||||
@ -64,8 +68,7 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
|||||||
self.initial['text'] = last_version.text
|
self.initial['text'] = last_version.text
|
||||||
self.initial['reason'] = last_version.reason
|
self.initial['reason'] = last_version.reason
|
||||||
self.initial['attachments'] = self.motion.attachments.all()
|
self.initial['attachments'] = self.motion.attachments.all()
|
||||||
else:
|
self.initial['tags'] = self.motion.tags.all()
|
||||||
self.initial['text'] = config['motion_preamble']
|
|
||||||
super(BaseMotionForm, self).__init__(*args, **kwargs)
|
super(BaseMotionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _
|
|||||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
|
from openslides.core.models import Tag
|
||||||
from openslides.mediafile.models import Mediafile
|
from openslides.mediafile.models import Mediafile
|
||||||
from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefaultVotesMixin)
|
from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefaultVotesMixin)
|
||||||
from openslides.projector.models import RelatedModelMixin, SlideMixin
|
from openslides.projector.models import RelatedModelMixin, SlideMixin
|
||||||
@ -69,8 +70,17 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
Many to many relation to mediafile objects.
|
Many to many relation to mediafile objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: proposal
|
parent = models.ForeignKey('self', null=True, blank=True, related_name='amendments')
|
||||||
# master = models.ForeignKey('self', null=True, blank=True)
|
"""
|
||||||
|
Field for amendments to reference to the motion that should be altered.
|
||||||
|
|
||||||
|
Null if the motion is not an amendment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tags = models.ManyToManyField(Tag)
|
||||||
|
"""
|
||||||
|
Tags to categorise motions.
|
||||||
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
@ -204,19 +214,31 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
|
|
||||||
def set_identifier(self):
|
def set_identifier(self):
|
||||||
"""
|
"""
|
||||||
Sets the motion identifier automaticly according to the config
|
Sets the motion identifier automaticly according to the config value if
|
||||||
value if it is not set yet.
|
it is not set yet.
|
||||||
"""
|
"""
|
||||||
|
# The identifier is already set or should be set manually
|
||||||
if config['motion_identifier'] == 'manually' or self.identifier:
|
if config['motion_identifier'] == 'manually' or self.identifier:
|
||||||
# Do not set an identifier.
|
# Do not set an identifier.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# The motion is an amendment
|
||||||
|
elif self.is_amendment():
|
||||||
|
motions = self.parent.amendments.all()
|
||||||
|
|
||||||
|
# The motions should be counted per category
|
||||||
elif config['motion_identifier'] == 'per_category':
|
elif config['motion_identifier'] == 'per_category':
|
||||||
motions = Motion.objects.filter(category=self.category)
|
motions = Motion.objects.filter(category=self.category)
|
||||||
else: # That means: config['motion_identifier'] == 'serially_numbered'
|
|
||||||
|
# The motions should be counted over all.
|
||||||
|
else:
|
||||||
motions = Motion.objects.all()
|
motions = Motion.objects.all()
|
||||||
|
|
||||||
number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0
|
number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0
|
||||||
if self.category is None or not self.category.prefix:
|
if self.is_amendment():
|
||||||
|
parent_identifier = self.parent.identifier or ''
|
||||||
|
prefix = '%s %s ' % (parent_identifier, config['motion_amendments_prefix'])
|
||||||
|
elif self.category is None or not self.category.prefix:
|
||||||
prefix = ''
|
prefix = ''
|
||||||
else:
|
else:
|
||||||
prefix = '%s ' % self.category.prefix
|
prefix = '%s ' % self.category.prefix
|
||||||
@ -521,6 +543,15 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
MotionLog.objects.create(motion=self, message_list=message_list, person=person)
|
MotionLog.objects.create(motion=self, message_list=message_list, person=person)
|
||||||
|
|
||||||
|
def is_amendment(self):
|
||||||
|
"""
|
||||||
|
Returns True if the motion is an amendment.
|
||||||
|
|
||||||
|
A motion is a amendment if amendments are activated in the config and
|
||||||
|
the motion has a parent.
|
||||||
|
"""
|
||||||
|
return config['motion_amendments_enabled'] and self.parent is not None
|
||||||
|
|
||||||
|
|
||||||
class MotionVersion(AbsoluteUrlMixin, models.Model):
|
class MotionVersion(AbsoluteUrlMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
@ -810,10 +841,12 @@ class State(models.Model):
|
|||||||
"""If true, new versions are not automaticly set active."""
|
"""If true, new versions are not automaticly set active."""
|
||||||
|
|
||||||
dont_set_identifier = models.BooleanField(default=False)
|
dont_set_identifier = models.BooleanField(default=False)
|
||||||
"""Decides if the motion gets an identifier.
|
"""
|
||||||
|
Decides if the motion gets an identifier.
|
||||||
|
|
||||||
If true, the motion does not get an identifier if the state change to
|
If true, the motion does not get an identifier if the state change to
|
||||||
this one, else it does."""
|
this one, else it does.
|
||||||
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Returns the name of the state."""
|
"""Returns the name of the state."""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
from django.utils.translation import ugettext_lazy, ugettext_noop, pgettext
|
||||||
|
|
||||||
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable
|
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable
|
||||||
from openslides.poll.models import PERCENT_BASE_CHOICES
|
from openslides.poll.models import PERCENT_BASE_CHOICES
|
||||||
@ -62,6 +62,25 @@ def setup_motion_config(sender, **kwargs):
|
|||||||
motion_stop_submitting,
|
motion_stop_submitting,
|
||||||
motion_allow_disable_versioning))
|
motion_allow_disable_versioning))
|
||||||
|
|
||||||
|
# Amendments
|
||||||
|
motion_amendments_enabled = ConfigVariable(
|
||||||
|
name='motion_amendments_enabled',
|
||||||
|
default_value=False,
|
||||||
|
form_field=forms.BooleanField(
|
||||||
|
label=ugettext_lazy('Activate amendments'),
|
||||||
|
required=False))
|
||||||
|
|
||||||
|
motion_amendments_prefix = ConfigVariable(
|
||||||
|
name='motion_amendments_prefix',
|
||||||
|
default_value=pgettext('Prefix for amendment', 'A'),
|
||||||
|
form_field=forms.CharField(
|
||||||
|
required=False,
|
||||||
|
label=ugettext_lazy('Prefix for the identifier for amendments')))
|
||||||
|
|
||||||
|
group_amendments = ConfigGroup(
|
||||||
|
title=ugettext_lazy('Amendments'),
|
||||||
|
variables=(motion_amendments_enabled, motion_amendments_prefix))
|
||||||
|
|
||||||
# Supporters
|
# Supporters
|
||||||
motion_min_supporters = ConfigVariable(
|
motion_min_supporters = ConfigVariable(
|
||||||
name='motion_min_supporters',
|
name='motion_min_supporters',
|
||||||
@ -143,7 +162,8 @@ def setup_motion_config(sender, **kwargs):
|
|||||||
title=ugettext_noop('Motion'),
|
title=ugettext_noop('Motion'),
|
||||||
url='motion',
|
url='motion',
|
||||||
weight=30,
|
weight=30,
|
||||||
groups=(group_general, group_supporters, group_ballot_papers, group_pdf))
|
groups=(group_general, group_amendments, group_supporters,
|
||||||
|
group_ballot_papers, group_pdf))
|
||||||
|
|
||||||
|
|
||||||
def create_builtin_workflows(sender, **kwargs):
|
def create_builtin_workflows(sender, **kwargs):
|
||||||
|
@ -29,6 +29,9 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if motion.is_amendment %}
|
||||||
|
(Amendment of <a href="{{ motion.parent|absolute_url }}">{{ motion.parent.identifier|default:motion.parent }}</a>)
|
||||||
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
<small class="pull-right">
|
<small class="pull-right">
|
||||||
<a href="{% url 'motion_list' %}" class="btn btn-mini">
|
<a href="{% url 'motion_list' %}" class="btn btn-mini">
|
||||||
@ -68,6 +71,8 @@
|
|||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{{ motion.tags.all|join:', ' }}
|
||||||
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span8">
|
<div class="span8">
|
||||||
{# TODO: show only for workflow with versioning #}
|
{# TODO: show only for workflow with versioning #}
|
||||||
@ -280,6 +285,23 @@
|
|||||||
</h5>
|
</h5>
|
||||||
{{ version.creation_time }}
|
{{ version.creation_time }}
|
||||||
|
|
||||||
|
{% if 'motion_amendments_enabled'|get_config %}
|
||||||
|
<h5>{% trans 'Amendments' %}:</h5>
|
||||||
|
{% with amendments=motion.amendments.all %}
|
||||||
|
{% if amendments %}
|
||||||
|
<ul>
|
||||||
|
{% for amendment in amendments %}
|
||||||
|
<li><a href="{{ amendment|absolute_url }}">{{ amendment }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<a class="btn btn-mini btn-primary" href="{% url 'motion_create_amendment' motion.pk %}">
|
||||||
|
<i class="icon-plus icon-white"></i>
|
||||||
|
{% trans 'New amendment' %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Support/Unsupport button -->
|
<!-- Support/Unsupport button -->
|
||||||
{% if perms.motion.can_support_motion and 'motion_min_supporters'|get_config > 0 %}
|
{% if perms.motion.can_support_motion and 'motion_min_supporters'|get_config > 0 %}
|
||||||
{% if allowed_actions.unsupport %}
|
{% if allowed_actions.unsupport %}
|
||||||
|
@ -7,25 +7,8 @@
|
|||||||
{% block header %}
|
{% block header %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<link type="text/css" rel="stylesheet" media="all" href="{% static 'css/motion.css' %}" />
|
<link type="text/css" rel="stylesheet" media="all" href="{% static 'css/motion.css' %}" />
|
||||||
<link href="{% static 'css/jquery.bsmselect.css' %}" type="text/css" rel="stylesheet" />
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script type="text/javascript" src="{% static 'js/jquery/jquery.bsmselect.js' %}"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
// use jquery-bsmselect for all <select multiple> form elements
|
|
||||||
$("select[multiple]").bsmSelect({
|
|
||||||
removeLabel: '<sup><b>X</b></sup>',
|
|
||||||
containerClass: 'bsmContainer',
|
|
||||||
listClass: 'bsmList-custom',
|
|
||||||
listItemClass: 'bsmListItem-custom',
|
|
||||||
listItemLabelClass: 'bsmListItemLabel-custom',
|
|
||||||
removeClass: 'bsmListItemRemove-custom'
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if motion %}
|
{% if motion %}
|
||||||
|
@ -33,11 +33,20 @@
|
|||||||
<small class="pull-right">
|
<small class="pull-right">
|
||||||
{% if perms.motion.can_create_motion %}
|
{% if perms.motion.can_create_motion %}
|
||||||
{% if not 'motion_stop_submitting'|get_config or perms.motion.can_manage_motion %}
|
{% if not 'motion_stop_submitting'|get_config or perms.motion.can_manage_motion %}
|
||||||
<a href="{% url 'motion_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New motion' %}"><i class="icon-plus icon-white"></i> {% trans 'New' %}</a>
|
<a href="{% url 'motion_create' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New motion' %}"><i class="icon-plus icon-white"></i> {% trans 'New' %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.core.can_manage_tags %}
|
||||||
|
<a href="{% url 'core_tag_list' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage tags' %}">
|
||||||
|
<i class="icon-th"></i>
|
||||||
|
<span class="optional-small"> {% trans 'Tags' %}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if perms.motion.can_manage_motion %}
|
{% if perms.motion.can_manage_motion %}
|
||||||
<a href="{% url 'motion_category_list' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage categories' %}"><i class="icon-th-large"></i><span class="optional-small"> {% trans 'Categories' %}</span></a>
|
<a href="{% url 'motion_category_list' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Manage categories' %}">
|
||||||
|
<i class="icon-th-large"></i>
|
||||||
|
<span class="optional-small"> {% trans 'Categories' %}</span>
|
||||||
|
</a>
|
||||||
<a href="{% url 'motion_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import motions' %}"><i class="icon-import"></i><span class="optional-small"> {% trans 'Import' %}</span></a>
|
<a href="{% url 'motion_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import motions' %}"><i class="icon-import"></i><span class="optional-small"> {% trans 'Import' %}</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'motion_list_pdf' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print all motions as PDF' %}" target="_blank"><i class="icon-print"></i><span class="optional-small"> PDF</span></a>
|
<a href="{% url 'motion_list_pdf' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print all motions as PDF' %}" target="_blank"><i class="icon-print"></i><span class="optional-small"> PDF</span></a>
|
||||||
@ -64,7 +73,14 @@
|
|||||||
{% for motion in motion_list %}
|
{% for motion in motion_list %}
|
||||||
<tr class="{% if motion.is_active_slide %}activeline{% endif %}">
|
<tr class="{% if motion.is_active_slide %}activeline{% endif %}">
|
||||||
<td class="nobr">{{ motion.identifier|default:'' }}</td>
|
<td class="nobr">{{ motion.identifier|default:'' }}</td>
|
||||||
<td><a href="{{ motion|absolute_url }}">{{ motion.title }}</a></td>
|
<td>
|
||||||
|
<a href="{{ motion|absolute_url }}">{{ motion.title }}</a>
|
||||||
|
{% if motion.is_amendment %}
|
||||||
|
<a class="label label-success" data-original-title="Amendment" rel="tooltip">
|
||||||
|
{{ 'motion_amendments_prefix'|get_config }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="optional">{% if motion.category %}{{ motion.category }}{% else %}–{% endif %}</td>
|
<td class="optional">{% if motion.category %}{{ motion.category }}{% else %}–{% endif %}</td>
|
||||||
<td class="optional-small"><span class="label label-info">{% trans motion.state.name %}</span></td>
|
<td class="optional-small"><span class="label label-info">{% trans motion.state.name %}</span></td>
|
||||||
<td class="optional">
|
<td class="optional">
|
||||||
|
@ -77,6 +77,9 @@
|
|||||||
{{ motion.active_version.title }}
|
{{ motion.active_version.title }}
|
||||||
<small>
|
<small>
|
||||||
{% trans "Motion" %} {{ motion.identifier|default:'' }}
|
{% trans "Motion" %} {{ motion.identifier|default:'' }}
|
||||||
|
{% if motion.is_amendment %}
|
||||||
|
(Amendment of {{ motion.parent.identifier|default:motion.parent }})
|
||||||
|
{% endif %}
|
||||||
{% if motion.get_active_version.version_number > 1 %} | {% trans 'Version' %} {{ motion.active_version.version_number }}{% endif %}
|
{% if motion.get_active_version.version_number > 1 %} | {% trans 'Version' %} {{ motion.active_version.version_number }}{% endif %}
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -1,106 +1,111 @@
|
|||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import patterns, url
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
# TODO: define the Views inhere
|
# TODO: define the Views inhere
|
||||||
urlpatterns = patterns(
|
urlpatterns = patterns(
|
||||||
'openslides.motion.views',
|
'openslides.motion.views',
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
'motion_list',
|
views.MotionListView.as_view(),
|
||||||
name='motion_list'),
|
name='motion_list'),
|
||||||
|
|
||||||
url(r'^new/$',
|
url(r'^new/$',
|
||||||
'motion_create',
|
views.MotionCreateView.as_view(),
|
||||||
# TODO: rename to motion_create
|
name='motion_create'),
|
||||||
name='motion_new'),
|
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/$',
|
url(r'^(?P<pk>\d+)/$',
|
||||||
'motion_detail',
|
views.MotionDetailView.as_view(),
|
||||||
name='motion_detail'),
|
name='motion_detail'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/edit/$',
|
url(r'^(?P<pk>\d+)/edit/$',
|
||||||
'motion_update',
|
views.MotionUpdateView.as_view(),
|
||||||
name='motion_update'),
|
name='motion_update'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/del/$',
|
url(r'^(?P<pk>\d+)/del/$',
|
||||||
'motion_delete',
|
views.MotionDeleteView.as_view(),
|
||||||
name='motion_delete'),
|
name='motion_delete'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/new_amendment/$',
|
||||||
|
views.MotionCreateAmendmentView.as_view(),
|
||||||
|
name='motion_create_amendment'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/$',
|
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/$',
|
||||||
'motion_detail',
|
views.MotionDetailView.as_view(),
|
||||||
name='motion_version_detail'),
|
name='motion_version_detail'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/permit/$',
|
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/permit/$',
|
||||||
'version_permit',
|
views.VersionPermitView.as_view(),
|
||||||
name='motion_version_permit'),
|
name='motion_version_permit'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/del/$',
|
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/del/$',
|
||||||
'version_delete',
|
views.VersionDeleteView.as_view(),
|
||||||
name='motion_version_delete'),
|
name='motion_version_delete'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/diff/$',
|
url(r'^(?P<pk>\d+)/diff/$',
|
||||||
'version_diff',
|
views.VersionDiffView.as_view(),
|
||||||
name='motion_version_diff'),
|
name='motion_version_diff'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/support/$',
|
url(r'^(?P<pk>\d+)/support/$',
|
||||||
'motion_support',
|
views.SupportView.as_view(support=True),
|
||||||
name='motion_support'),
|
name='motion_support'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/unsupport/$',
|
url(r'^(?P<pk>\d+)/unsupport/$',
|
||||||
'motion_unsupport',
|
views.SupportView.as_view(support=False),
|
||||||
name='motion_unsupport'),
|
name='motion_unsupport'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/create_poll/$',
|
url(r'^(?P<pk>\d+)/create_poll/$',
|
||||||
'poll_create',
|
views.PollCreateView.as_view(),
|
||||||
name='motionpoll_create'),
|
name='motionpoll_create'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/edit/$',
|
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/edit/$',
|
||||||
'poll_update',
|
views.PollUpdateView.as_view(),
|
||||||
name='motionpoll_update'),
|
name='motionpoll_update'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/del/$',
|
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/del/$',
|
||||||
'poll_delete',
|
views.PollDeleteView.as_view(),
|
||||||
name='motionpoll_delete'),
|
name='motionpoll_delete'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/pdf/$',
|
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/pdf/$',
|
||||||
'poll_pdf',
|
views.PollPDFView.as_view(),
|
||||||
name='motionpoll_pdf'),
|
name='motionpoll_pdf'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/set_state/(?P<state>\d+)/$',
|
url(r'^(?P<pk>\d+)/set_state/(?P<state>\d+)/$',
|
||||||
'set_state',
|
views.MotionSetStateView.as_view(),
|
||||||
name='motion_set_state'),
|
name='motion_set_state'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/reset_state/$',
|
url(r'^(?P<pk>\d+)/reset_state/$',
|
||||||
'reset_state',
|
views.MotionSetStateView.as_view(reset=True),
|
||||||
name='motion_reset_state'),
|
name='motion_reset_state'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/agenda/$',
|
url(r'^(?P<pk>\d+)/agenda/$',
|
||||||
'create_agenda_item',
|
views.CreateRelatedAgendaItemView.as_view(),
|
||||||
name='motion_create_agenda'),
|
name='motion_create_agenda'),
|
||||||
|
|
||||||
url(r'^pdf/$',
|
url(r'^pdf/$',
|
||||||
'motion_list_pdf',
|
views.MotionPDFView.as_view(print_all_motions=True),
|
||||||
name='motion_list_pdf'),
|
name='motion_list_pdf'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/pdf/$',
|
url(r'^(?P<pk>\d+)/pdf/$',
|
||||||
'motion_detail_pdf',
|
views.MotionPDFView.as_view(print_all_motions=False),
|
||||||
name='motion_detail_pdf'),
|
name='motion_detail_pdf'),
|
||||||
|
|
||||||
url(r'^category/$',
|
url(r'^category/$',
|
||||||
'category_list',
|
views.CategoryListView.as_view(),
|
||||||
name='motion_category_list'),
|
name='motion_category_list'),
|
||||||
|
|
||||||
url(r'^category/new/$',
|
url(r'^category/new/$',
|
||||||
'category_create',
|
views.CategoryCreateView.as_view(),
|
||||||
name='motion_category_create'),
|
name='motion_category_create'),
|
||||||
|
|
||||||
url(r'^category/(?P<pk>\d+)/edit/$',
|
url(r'^category/(?P<pk>\d+)/edit/$',
|
||||||
'category_update',
|
views.CategoryUpdateView.as_view(),
|
||||||
name='motion_category_update'),
|
name='motion_category_update'),
|
||||||
|
|
||||||
url(r'^category/(?P<pk>\d+)/del/$',
|
url(r'^category/(?P<pk>\d+)/del/$',
|
||||||
'category_delete',
|
views.CategoryDeleteView.as_view(),
|
||||||
name='motion_category_delete'),
|
name='motion_category_delete'),
|
||||||
|
|
||||||
url(r'^csv_import/$',
|
url(r'^csv_import/$',
|
||||||
'motion_csv_import',
|
views.MotionCSVImportView.as_view(),
|
||||||
name='motion_csv_import'),
|
name='motion_csv_import'),
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ class MotionListView(ListView):
|
|||||||
Returns not a QuerySet but a filtered list of motions. Excludes motions
|
Returns not a QuerySet but a filtered list of motions. Excludes motions
|
||||||
that the user is not allowed to see.
|
that the user is not allowed to see.
|
||||||
"""
|
"""
|
||||||
queryset = super(MotionListView, self).get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
motions = []
|
motions = []
|
||||||
for motion in queryset:
|
for motion in queryset:
|
||||||
if (not motion.state.required_permission_to_see or
|
if (not motion.state.required_permission_to_see or
|
||||||
@ -55,8 +55,6 @@ class MotionListView(ListView):
|
|||||||
motions.append(motion)
|
motions.append(motion)
|
||||||
return motions
|
return motions
|
||||||
|
|
||||||
motion_list = MotionListView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class MotionDetailView(DetailView):
|
class MotionDetailView(DetailView):
|
||||||
"""
|
"""
|
||||||
@ -92,9 +90,7 @@ class MotionDetailView(DetailView):
|
|||||||
'title': version.title,
|
'title': version.title,
|
||||||
'text': version.text,
|
'text': version.text,
|
||||||
'reason': version.reason})
|
'reason': version.reason})
|
||||||
return super(MotionDetailView, self).get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
motion_detail = MotionDetailView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class MotionEditMixin(object):
|
class MotionEditMixin(object):
|
||||||
@ -142,6 +138,10 @@ class MotionEditMixin(object):
|
|||||||
self.object.attachments.clear()
|
self.object.attachments.clear()
|
||||||
self.object.attachments.add(*form.cleaned_data['attachments'])
|
self.object.attachments.add(*form.cleaned_data['attachments'])
|
||||||
|
|
||||||
|
# Save the tags
|
||||||
|
self.object.tags.clear()
|
||||||
|
self.object.tags.add(*form.cleaned_data['tags'])
|
||||||
|
|
||||||
# Update the projector if the motion is on it. This can not be done in
|
# Update the projector if the motion is on it. This can not be done in
|
||||||
# the model, because bulk_create does not call the save method.
|
# the model, because bulk_create does not call the save method.
|
||||||
active_slide = get_active_slide()
|
active_slide = get_active_slide()
|
||||||
@ -203,17 +203,26 @@ class MotionCreateView(MotionEditMixin, CreateView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""
|
"""
|
||||||
Write a log message if the form is valid.
|
Write a log message and set the submitter if necessary.
|
||||||
"""
|
"""
|
||||||
response = super(MotionCreateView, self).form_valid(form)
|
# First, validate and process the form and create the motion
|
||||||
|
response = super().form_valid(form)
|
||||||
|
|
||||||
|
# Write the log message
|
||||||
self.object.write_log([ugettext_noop('Motion created')], self.request.user)
|
self.object.write_log([ugettext_noop('Motion created')], self.request.user)
|
||||||
|
|
||||||
|
# Set submitter to request.user if no submitter is set yet
|
||||||
if ('submitter' not in form.cleaned_data or
|
if ('submitter' not in form.cleaned_data or
|
||||||
not form.cleaned_data['submitter']):
|
not form.cleaned_data['submitter']):
|
||||||
self.object.add_submitter(self.request.user)
|
self.object.add_submitter(self.request.user)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super(MotionCreateView, self).get_initial()
|
"""
|
||||||
|
Sets the initial data for the MotionCreateForm.
|
||||||
|
"""
|
||||||
|
initial = super().get_initial()
|
||||||
|
initial['text'] = config['motion_preamble']
|
||||||
if self.request.user.has_perm('motion.can_manage_motion'):
|
if self.request.user.has_perm('motion.can_manage_motion'):
|
||||||
initial['workflow'] = config['motion_workflow']
|
initial['workflow'] = config['motion_workflow']
|
||||||
return initial
|
return initial
|
||||||
@ -227,7 +236,53 @@ class MotionCreateView(MotionEditMixin, CreateView):
|
|||||||
self.object.reset_state(workflow)
|
self.object.reset_state(workflow)
|
||||||
self.version = self.object.get_new_version()
|
self.version = self.object.get_new_version()
|
||||||
|
|
||||||
motion_create = MotionCreateView.as_view()
|
|
||||||
|
class MotionCreateAmendmentView(MotionCreateView):
|
||||||
|
"""
|
||||||
|
Create an amendment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
if not config['motion_amendments_enabled']:
|
||||||
|
raise Http404('Amendments are disabled in the config.')
|
||||||
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_parent_motion(self):
|
||||||
|
"""
|
||||||
|
Gets the parent motion from the url.
|
||||||
|
|
||||||
|
Caches the value.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parent = self._object_parent
|
||||||
|
except AttributeError:
|
||||||
|
# self.get_object() is the django method, which does not cache the
|
||||||
|
# object. For now this is not a problem, because get_object() is only
|
||||||
|
# called once.
|
||||||
|
parent = self._object_parent = self.get_object()
|
||||||
|
return parent
|
||||||
|
|
||||||
|
def manipulate_object(self, form):
|
||||||
|
"""
|
||||||
|
Sets the parent to the motion to which this amendment refers.
|
||||||
|
"""
|
||||||
|
self.object.parent = self.get_parent_motion()
|
||||||
|
super().manipulate_object(form)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""
|
||||||
|
Sets the initial values to the form.
|
||||||
|
|
||||||
|
This are the values for title, text and reason which are set to the
|
||||||
|
values from the parent motion.
|
||||||
|
"""
|
||||||
|
initial = super().get_initial()
|
||||||
|
parent = self.get_parent_motion()
|
||||||
|
initial['title'] = parent.title
|
||||||
|
initial['text'] = parent.text
|
||||||
|
initial['reason'] = parent.reason
|
||||||
|
initial['category'] = parent.category
|
||||||
|
return initial
|
||||||
|
|
||||||
|
|
||||||
class MotionUpdateView(MotionEditMixin, UpdateView):
|
class MotionUpdateView(MotionEditMixin, UpdateView):
|
||||||
@ -246,7 +301,7 @@ class MotionUpdateView(MotionEditMixin, UpdateView):
|
|||||||
"""
|
"""
|
||||||
Writes a log message and removes supports in some cases if the form is valid.
|
Writes a log message and removes supports in some cases if the form is valid.
|
||||||
"""
|
"""
|
||||||
response = super(MotionUpdateView, self).form_valid(form)
|
response = super().form_valid(form)
|
||||||
self.write_log()
|
self.write_log()
|
||||||
if (config['motion_remove_supporters'] and self.object.state.allow_support and
|
if (config['motion_remove_supporters'] and self.object.state.allow_support and
|
||||||
not self.request.user.has_perm('motion.can_manage_motion')):
|
not self.request.user.has_perm('motion.can_manage_motion')):
|
||||||
@ -271,7 +326,7 @@ class MotionUpdateView(MotionEditMixin, UpdateView):
|
|||||||
self.request.user)
|
self.request.user)
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super(MotionUpdateView, self).get_initial()
|
initial = super().get_initial()
|
||||||
if self.request.user.has_perm('motion.can_manage_motion'):
|
if self.request.user.has_perm('motion.can_manage_motion'):
|
||||||
initial['workflow'] = self.object.state.workflow
|
initial['workflow'] = self.object.state.workflow
|
||||||
return initial
|
return initial
|
||||||
@ -295,8 +350,6 @@ class MotionUpdateView(MotionEditMixin, UpdateView):
|
|||||||
self.version = self.object.get_last_version()
|
self.version = self.object.get_last_version()
|
||||||
self.used_new_version = False
|
self.used_new_version = False
|
||||||
|
|
||||||
motion_update = MotionUpdateView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class MotionDeleteView(DeleteView):
|
class MotionDeleteView(DeleteView):
|
||||||
"""
|
"""
|
||||||
@ -314,8 +367,6 @@ class MotionDeleteView(DeleteView):
|
|||||||
def get_final_message(self):
|
def get_final_message(self):
|
||||||
return _('%s was successfully deleted.') % _('Motion')
|
return _('%s was successfully deleted.') % _('Motion')
|
||||||
|
|
||||||
motion_delete = MotionDeleteView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class VersionDeleteView(DeleteView):
|
class VersionDeleteView(DeleteView):
|
||||||
"""
|
"""
|
||||||
@ -347,8 +398,6 @@ class VersionDeleteView(DeleteView):
|
|||||||
def get_url_name_args(self):
|
def get_url_name_args(self):
|
||||||
return (self.get_object().motion_id, )
|
return (self.get_object().motion_id, )
|
||||||
|
|
||||||
version_delete = VersionDeleteView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class VersionPermitView(SingleObjectMixin, QuestionView):
|
class VersionPermitView(SingleObjectMixin, QuestionView):
|
||||||
"""
|
"""
|
||||||
@ -368,7 +417,7 @@ class VersionPermitView(SingleObjectMixin, QuestionView):
|
|||||||
self.version = self.get_object().versions.get(version_number=int(version_number))
|
self.version = self.get_object().versions.get(version_number=int(version_number))
|
||||||
except MotionVersion.DoesNotExist:
|
except MotionVersion.DoesNotExist:
|
||||||
raise Http404('Version %s not found.' % version_number)
|
raise Http404('Version %s not found.' % version_number)
|
||||||
return super(VersionPermitView, self).get(*args, **kwargs)
|
return super().get(*args, **kwargs)
|
||||||
|
|
||||||
def get_url_name_args(self):
|
def get_url_name_args(self):
|
||||||
"""
|
"""
|
||||||
@ -394,8 +443,6 @@ class VersionPermitView(SingleObjectMixin, QuestionView):
|
|||||||
ugettext_noop('permitted')],
|
ugettext_noop('permitted')],
|
||||||
person=self.request.user)
|
person=self.request.user)
|
||||||
|
|
||||||
version_permit = VersionPermitView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class VersionDiffView(DetailView):
|
class VersionDiffView(DetailView):
|
||||||
"""
|
"""
|
||||||
@ -427,7 +474,7 @@ class VersionDiffView(DetailView):
|
|||||||
version_rev2 = None
|
version_rev2 = None
|
||||||
diff_text = None
|
diff_text = None
|
||||||
diff_reason = None
|
diff_reason = None
|
||||||
context = super(VersionDiffView, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context.update({
|
context.update({
|
||||||
'version_rev1': version_rev1,
|
'version_rev1': version_rev1,
|
||||||
'version_rev2': version_rev2,
|
'version_rev2': version_rev2,
|
||||||
@ -436,8 +483,6 @@ class VersionDiffView(DetailView):
|
|||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
version_diff = VersionDiffView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class SupportView(SingleObjectMixin, QuestionView):
|
class SupportView(SingleObjectMixin, QuestionView):
|
||||||
"""
|
"""
|
||||||
@ -500,9 +545,6 @@ class SupportView(SingleObjectMixin, QuestionView):
|
|||||||
else:
|
else:
|
||||||
return _("You have unsupported this motion successfully.")
|
return _("You have unsupported this motion successfully.")
|
||||||
|
|
||||||
motion_support = SupportView.as_view(support=True)
|
|
||||||
motion_unsupport = SupportView.as_view(support=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PollCreateView(SingleObjectMixin, RedirectView):
|
class PollCreateView(SingleObjectMixin, RedirectView):
|
||||||
"""
|
"""
|
||||||
@ -526,8 +568,6 @@ class PollCreateView(SingleObjectMixin, RedirectView):
|
|||||||
"""
|
"""
|
||||||
return reverse('motionpoll_update', args=[self.get_object().pk, self.poll.poll_number])
|
return reverse('motionpoll_update', args=[self.get_object().pk, self.poll.poll_number])
|
||||||
|
|
||||||
poll_create = PollCreateView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class PollMixin(object):
|
class PollMixin(object):
|
||||||
"""
|
"""
|
||||||
@ -579,7 +619,7 @@ class PollUpdateView(PollMixin, PollFormView):
|
|||||||
|
|
||||||
Append the motion object to the context.
|
Append the motion object to the context.
|
||||||
"""
|
"""
|
||||||
context = super(PollUpdateView, self).get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context.update({
|
context.update({
|
||||||
'motion': self.poll.motion,
|
'motion': self.poll.motion,
|
||||||
'poll': self.poll})
|
'poll': self.poll})
|
||||||
@ -589,12 +629,10 @@ class PollUpdateView(PollMixin, PollFormView):
|
|||||||
"""
|
"""
|
||||||
Write a log message, if the form is valid.
|
Write a log message, if the form is valid.
|
||||||
"""
|
"""
|
||||||
value = super(PollUpdateView, self).form_valid(form)
|
value = super().form_valid(form)
|
||||||
self.get_object().write_log([ugettext_noop('Poll updated')], self.request.user)
|
self.get_object().write_log([ugettext_noop('Poll updated')], self.request.user)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
poll_update = PollUpdateView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class PollDeleteView(PollMixin, DeleteView):
|
class PollDeleteView(PollMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
@ -607,7 +645,7 @@ class PollDeleteView(PollMixin, DeleteView):
|
|||||||
"""
|
"""
|
||||||
Write a log message, if the form is valid.
|
Write a log message, if the form is valid.
|
||||||
"""
|
"""
|
||||||
super(PollDeleteView, self).on_clicked_yes()
|
super().on_clicked_yes()
|
||||||
self.get_object().motion.write_log([ugettext_noop('Poll deleted')], self.request.user)
|
self.get_object().motion.write_log([ugettext_noop('Poll deleted')], self.request.user)
|
||||||
|
|
||||||
def get_redirect_url(self, **kwargs):
|
def get_redirect_url(self, **kwargs):
|
||||||
@ -616,8 +654,6 @@ class PollDeleteView(PollMixin, DeleteView):
|
|||||||
"""
|
"""
|
||||||
return reverse('motion_detail', args=[self.get_object().motion.pk])
|
return reverse('motion_detail', args=[self.get_object().motion.pk])
|
||||||
|
|
||||||
poll_delete = PollDeleteView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class PollPDFView(PollMixin, PDFView):
|
class PollPDFView(PollMixin, PDFView):
|
||||||
"""
|
"""
|
||||||
@ -647,8 +683,6 @@ class PollPDFView(PollMixin, PDFView):
|
|||||||
"""
|
"""
|
||||||
motion_poll_to_pdf(pdf, self.get_object())
|
motion_poll_to_pdf(pdf, self.get_object())
|
||||||
|
|
||||||
poll_pdf = PollPDFView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class MotionSetStateView(SingleObjectMixin, RedirectView):
|
class MotionSetStateView(SingleObjectMixin, RedirectView):
|
||||||
"""
|
"""
|
||||||
@ -686,9 +720,6 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
|
|||||||
_('The state of the motion was set to %s.')
|
_('The state of the motion was set to %s.')
|
||||||
% html_strong(_(self.get_object().state.name)))
|
% html_strong(_(self.get_object().state.name)))
|
||||||
|
|
||||||
set_state = MotionSetStateView.as_view()
|
|
||||||
reset_state = MotionSetStateView.as_view(reset=True)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateRelatedAgendaItemView(_CreateRelatedAgendaItemView):
|
class CreateRelatedAgendaItemView(_CreateRelatedAgendaItemView):
|
||||||
"""
|
"""
|
||||||
@ -700,11 +731,9 @@ class CreateRelatedAgendaItemView(_CreateRelatedAgendaItemView):
|
|||||||
"""
|
"""
|
||||||
Create the agenda item.
|
Create the agenda item.
|
||||||
"""
|
"""
|
||||||
super(CreateRelatedAgendaItemView, self).pre_redirect(request, *args, **kwargs)
|
super().pre_redirect(request, *args, **kwargs)
|
||||||
self.get_object().write_log([ugettext_noop('Agenda item created')], self.request.user)
|
self.get_object().write_log([ugettext_noop('Agenda item created')], self.request.user)
|
||||||
|
|
||||||
create_agenda_item = CreateRelatedAgendaItemView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class MotionPDFView(SingleObjectMixin, PDFView):
|
class MotionPDFView(SingleObjectMixin, PDFView):
|
||||||
"""
|
"""
|
||||||
@ -734,7 +763,7 @@ class MotionPDFView(SingleObjectMixin, PDFView):
|
|||||||
if self.print_all_motions:
|
if self.print_all_motions:
|
||||||
obj = None
|
obj = None
|
||||||
else:
|
else:
|
||||||
obj = super(MotionPDFView, self).get_object(*args, **kwargs)
|
obj = super().get_object(*args, **kwargs)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
@ -765,16 +794,11 @@ class MotionPDFView(SingleObjectMixin, PDFView):
|
|||||||
else:
|
else:
|
||||||
motion_to_pdf(pdf, self.get_object())
|
motion_to_pdf(pdf, self.get_object())
|
||||||
|
|
||||||
motion_list_pdf = MotionPDFView.as_view(print_all_motions=True)
|
|
||||||
motion_detail_pdf = MotionPDFView.as_view(print_all_motions=False)
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryListView(ListView):
|
class CategoryListView(ListView):
|
||||||
required_permission = 'motion.can_manage_motion'
|
required_permission = 'motion.can_manage_motion'
|
||||||
model = Category
|
model = Category
|
||||||
|
|
||||||
category_list = CategoryListView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryCreateView(CreateView):
|
class CategoryCreateView(CreateView):
|
||||||
required_permission = 'motion.can_manage_motion'
|
required_permission = 'motion.can_manage_motion'
|
||||||
@ -782,8 +806,6 @@ class CategoryCreateView(CreateView):
|
|||||||
success_url_name = 'motion_category_list'
|
success_url_name = 'motion_category_list'
|
||||||
url_name_args = []
|
url_name_args = []
|
||||||
|
|
||||||
category_create = CategoryCreateView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryUpdateView(UpdateView):
|
class CategoryUpdateView(UpdateView):
|
||||||
required_permission = 'motion.can_manage_motion'
|
required_permission = 'motion.can_manage_motion'
|
||||||
@ -791,8 +813,6 @@ class CategoryUpdateView(UpdateView):
|
|||||||
success_url_name = 'motion_category_list'
|
success_url_name = 'motion_category_list'
|
||||||
url_name_args = []
|
url_name_args = []
|
||||||
|
|
||||||
category_update = CategoryUpdateView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryDeleteView(DeleteView):
|
class CategoryDeleteView(DeleteView):
|
||||||
required_permission = 'motion.can_manage_motion'
|
required_permission = 'motion.can_manage_motion'
|
||||||
@ -801,8 +821,6 @@ class CategoryDeleteView(DeleteView):
|
|||||||
url_name_args = []
|
url_name_args = []
|
||||||
success_url_name = 'motion_category_list'
|
success_url_name = 'motion_category_list'
|
||||||
|
|
||||||
category_delete = CategoryDeleteView.as_view()
|
|
||||||
|
|
||||||
|
|
||||||
class MotionCSVImportView(CSVImportView):
|
class MotionCSVImportView(CSVImportView):
|
||||||
"""
|
"""
|
||||||
@ -817,7 +835,7 @@ class MotionCSVImportView(CSVImportView):
|
|||||||
"""
|
"""
|
||||||
Sets the request user as initial for the default submitter.
|
Sets the request user as initial for the default submitter.
|
||||||
"""
|
"""
|
||||||
return_value = super(MotionCSVImportView, self).get_initial(*args, **kwargs)
|
return_value = super().get_initial(*args, **kwargs)
|
||||||
return_value.update({'default_submitter': self.request.user.person_id})
|
return_value.update({'default_submitter': self.request.user.person_id})
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
@ -828,5 +846,3 @@ class MotionCSVImportView(CSVImportView):
|
|||||||
messages.error(self.request, error)
|
messages.error(self.request, error)
|
||||||
# Overleap method of CSVImportView
|
# Overleap method of CSVImportView
|
||||||
return super(CSVImportView, self).form_valid(form)
|
return super(CSVImportView, self).form_valid(form)
|
||||||
|
|
||||||
motion_csv_import = MotionCSVImportView.as_view()
|
|
||||||
|
@ -170,11 +170,14 @@ def create_builtin_groups_and_admin(sender, **kwargs):
|
|||||||
ct_config = ContentType.objects.get(app_label='config', model='configstore')
|
ct_config = ContentType.objects.get(app_label='config', model='configstore')
|
||||||
perm_48 = Permission.objects.get(content_type=ct_config, codename='can_manage')
|
perm_48 = Permission.objects.get(content_type=ct_config, codename='can_manage')
|
||||||
|
|
||||||
|
ct_tag = ContentType.objects.get(app_label='core', model='tag')
|
||||||
|
can_manage_tags = Permission.objects.get(content_type=ct_tag, codename='can_manage_tags')
|
||||||
|
|
||||||
group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4)
|
group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4)
|
||||||
# add delegate permissions (without can_support_motion)
|
# add delegate permissions (without can_support_motion)
|
||||||
group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35)
|
group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35)
|
||||||
# add staff permissions
|
# add staff permissions
|
||||||
group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48)
|
group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags)
|
||||||
# add can_see_user permission
|
# add can_see_user permission
|
||||||
group_staff.permissions.add(perm_17) # TODO: Remove this redundancy after cleanup of the permission system
|
group_staff.permissions.add(perm_17) # TODO: Remove this redundancy after cleanup of the permission system
|
||||||
|
|
||||||
|
@ -3,24 +3,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<link href="{% static 'css/jquery.bsmselect.css' %}" type="text/css" rel="stylesheet" />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script type="text/javascript" src="{% static 'js/jquery/jquery.bsmselect.js' %}"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
// use jquery-bsmselect for all <select multiple> form elements
|
|
||||||
$("select[multiple]").bsmSelect({
|
|
||||||
removeLabel: '<sup><b>X</b></sup>',
|
|
||||||
containerClass: 'bsmContainer',
|
|
||||||
listClass: 'bsmList-custom',
|
|
||||||
listItemClass: 'bsmListItem-custom',
|
|
||||||
listItemLabelClass: 'bsmListItemLabel-custom',
|
|
||||||
removeClass: 'bsmListItemRemove-custom'
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if edit_user %}
|
{% if edit_user %}
|
||||||
|
@ -292,8 +292,12 @@ class AjaxView(PermissionMixin, AjaxMixin, View):
|
|||||||
View for ajax requests.
|
View for ajax requests.
|
||||||
"""
|
"""
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
# TODO: Raise an error, if the request is not an ajax-request
|
||||||
return self.ajax_get(request, *args, **kwargs)
|
return self.ajax_get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
return self.get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView):
|
class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView):
|
||||||
"""
|
"""
|
||||||
|
@ -142,3 +142,24 @@ class CustomSlidesTest(TestCase):
|
|||||||
response = self.admin_client.post(url, {'yes': 'true'})
|
response = self.admin_client.post(url, {'yes': 'true'})
|
||||||
self.assertRedirects(response, '/dashboard/')
|
self.assertRedirects(response, '/dashboard/')
|
||||||
self.assertFalse(CustomSlide.objects.exists())
|
self.assertFalse(CustomSlide.objects.exists())
|
||||||
|
|
||||||
|
|
||||||
|
class TagListViewTest(TestCase):
|
||||||
|
def test_get_tag_queryset(self):
|
||||||
|
view = views.TagListView()
|
||||||
|
|
||||||
|
with patch('openslides.core.views.Tag') as mock_tag:
|
||||||
|
view.get_tag_queryset('some_name_with_123', 15)
|
||||||
|
|
||||||
|
self.assertEqual(view.pk, 123)
|
||||||
|
mock_tag.objects.filter.assert_called_with(pk=123)
|
||||||
|
|
||||||
|
def test_get_tag_queryset_wrong_name(self):
|
||||||
|
view = views.TagListView()
|
||||||
|
|
||||||
|
with patch('openslides.core.views.Tag'):
|
||||||
|
with self.assertRaises(views.TagException) as context:
|
||||||
|
view.get_tag_queryset('some_name_with_', 15)
|
||||||
|
|
||||||
|
self.assertFalse(hasattr(view, 'pk'))
|
||||||
|
self.assertEqual(str(context.exception), 'Invalid name in request')
|
||||||
|
@ -145,6 +145,64 @@ class ModelTest(TestCase):
|
|||||||
# motion.__unicode__() raised an AttributeError
|
# motion.__unicode__() raised an AttributeError
|
||||||
self.assertEqual(str(motion), 'test_identifier_VohT1hu9uhiSh6ooVBFS | test_title_Koowoh1ISheemeey1air')
|
self.assertEqual(str(motion), 'test_identifier_VohT1hu9uhiSh6ooVBFS | test_title_Koowoh1ISheemeey1air')
|
||||||
|
|
||||||
|
def test_is_amendment(self):
|
||||||
|
config['motion_amendments_enabled'] = True
|
||||||
|
amendment = Motion.objects.create(title='amendment', parent=self.motion)
|
||||||
|
|
||||||
|
self.assertTrue(amendment.is_amendment())
|
||||||
|
self.assertFalse(self.motion.is_amendment())
|
||||||
|
|
||||||
|
def test_set_identifier_allready_set(self):
|
||||||
|
"""
|
||||||
|
If the motion already has a identifier, the method does nothing.
|
||||||
|
"""
|
||||||
|
motion = Motion(identifier='My test identifier')
|
||||||
|
|
||||||
|
motion.set_identifier()
|
||||||
|
|
||||||
|
self.assertEqual(motion.identifier, 'My test identifier')
|
||||||
|
|
||||||
|
def test_set_identifier_manually(self):
|
||||||
|
"""
|
||||||
|
If the config is set to manually, the method does nothing.
|
||||||
|
"""
|
||||||
|
config['motion_identifier'] = 'manually'
|
||||||
|
motion = Motion()
|
||||||
|
|
||||||
|
motion.set_identifier()
|
||||||
|
|
||||||
|
# If the identifier should be set manually, the method does nothing
|
||||||
|
self.assertIsNone(motion.identifier)
|
||||||
|
|
||||||
|
def test_set_identifier_amendment(self):
|
||||||
|
"""
|
||||||
|
If the motion is an amendment, the identifier is the identifier from the
|
||||||
|
parent + a suffix.
|
||||||
|
"""
|
||||||
|
config['motion_amendments_enabled'] = True
|
||||||
|
self.motion.identifier = 'Parent identifier'
|
||||||
|
self.motion.save()
|
||||||
|
motion = Motion(parent=self.motion)
|
||||||
|
|
||||||
|
motion.set_identifier()
|
||||||
|
|
||||||
|
self.assertEqual(motion.identifier, 'Parent identifier A 1')
|
||||||
|
|
||||||
|
def test_set_identifier_second_amendment(self):
|
||||||
|
"""
|
||||||
|
If a motion has already an amendment, the second motion gets another
|
||||||
|
identifier.
|
||||||
|
"""
|
||||||
|
config['motion_amendments_enabled'] = True
|
||||||
|
self.motion.identifier = 'Parent identifier'
|
||||||
|
self.motion.save()
|
||||||
|
Motion.objects.create(title='Amendment1', parent=self.motion)
|
||||||
|
motion = Motion(parent=self.motion)
|
||||||
|
|
||||||
|
motion.set_identifier()
|
||||||
|
|
||||||
|
self.assertEqual(motion.identifier, 'Parent identifier A 2')
|
||||||
|
|
||||||
|
|
||||||
class ConfigTest(TestCase):
|
class ConfigTest(TestCase):
|
||||||
def test_stop_submitting(self):
|
def test_stop_submitting(self):
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.test import RequestFactory
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
from openslides.mediafile.models import Mediafile
|
from openslides.mediafile.models import Mediafile
|
||||||
|
from openslides.motion import views
|
||||||
from openslides.motion.models import Category, Motion, MotionLog, State
|
from openslides.motion.models import Category, Motion, MotionLog, State
|
||||||
from openslides.users.models import Group, User
|
from openslides.users.models import Group, User
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
@ -241,6 +244,67 @@ class TestMotionCreateView(MotionViewTestCase):
|
|||||||
self.assertEqual(MotionLog.objects.get(pk=1).message_list, ['Motion created'])
|
self.assertEqual(MotionLog.objects.get(pk=1).message_list, ['Motion created'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestMotionCreateAmendmentView(MotionViewTestCase):
|
||||||
|
url = '/motion/1/new_amendment/'
|
||||||
|
|
||||||
|
def test_get_amendment_active(self):
|
||||||
|
config['motion_amendments_enabled'] = True
|
||||||
|
self.check_url(self.url, self.admin_client, 200)
|
||||||
|
|
||||||
|
def test_get_amendment_inactive(self):
|
||||||
|
config['motion_amendments_enabled'] = False
|
||||||
|
self.check_url(self.url, self.admin_client, 404)
|
||||||
|
|
||||||
|
def test_get_parent_motion(self):
|
||||||
|
motion = Motion.objects.create(title='Test Motion')
|
||||||
|
view = views.MotionCreateAmendmentView()
|
||||||
|
view.request = RequestFactory().get(self.url)
|
||||||
|
view.kwargs = {'pk': motion.pk}
|
||||||
|
|
||||||
|
self.assertEqual(view.get_parent_motion(), motion)
|
||||||
|
|
||||||
|
def test_manipulate_object(self):
|
||||||
|
motion = Motion.objects.create(title='Test Motion')
|
||||||
|
view = views.MotionCreateAmendmentView()
|
||||||
|
view.request = RequestFactory().get(self.url)
|
||||||
|
view.kwargs = {'pk': motion.pk}
|
||||||
|
view.object = MagicMock()
|
||||||
|
|
||||||
|
view.manipulate_object(MagicMock())
|
||||||
|
|
||||||
|
self.assertEqual(view.object.parent, motion)
|
||||||
|
|
||||||
|
def test_get_initial(self):
|
||||||
|
motion = Motion.objects.create(
|
||||||
|
title='Test Motion', text='Parent Motion text', reason='test reason')
|
||||||
|
view = views.MotionCreateAmendmentView()
|
||||||
|
view.request = MagicMock()
|
||||||
|
view.kwargs = {'pk': motion.pk}
|
||||||
|
|
||||||
|
self.assertEqual(view.get_initial(), {
|
||||||
|
'reason': u'test reason',
|
||||||
|
'text': u'Parent Motion text',
|
||||||
|
'title': u'Test Motion',
|
||||||
|
'category': None,
|
||||||
|
'workflow': '1'})
|
||||||
|
|
||||||
|
def test_get_initial_with_category(self):
|
||||||
|
category = Category.objects.create(name='test category')
|
||||||
|
motion = Motion.objects.create(
|
||||||
|
title='Test Motion', text='Parent Motion text', reason='test reason',
|
||||||
|
category=category)
|
||||||
|
view = views.MotionCreateAmendmentView()
|
||||||
|
view.request = MagicMock()
|
||||||
|
view.kwargs = {'pk': motion.pk}
|
||||||
|
|
||||||
|
self.assertEqual(view.get_initial(), {
|
||||||
|
'reason': u'test reason',
|
||||||
|
'text': u'Parent Motion text',
|
||||||
|
'title': u'Test Motion',
|
||||||
|
'category': category,
|
||||||
|
'workflow': '1'})
|
||||||
|
|
||||||
|
|
||||||
class TestMotionUpdateView(MotionViewTestCase):
|
class TestMotionUpdateView(MotionViewTestCase):
|
||||||
url = '/motion/1/edit/'
|
url = '/motion/1/edit/'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user