Merge branch 'stable/1.6.x'

Conflicts:
	CHANGELOG
	openslides/users/signals.py
This commit is contained in:
Oskar Hahn 2015-01-05 17:14:29 +01:00
commit 34b6ca80f3
30 changed files with 719 additions and 155 deletions

View File

@ -16,11 +16,14 @@ Other:
template signals and slides.
Version 1.7.0 (unreleased)
==========================
Version 1.7 (unreleased)
========================
[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.
Other:
- Cleaned up utils.views to increase performance when fetching single objects

View File

@ -30,7 +30,7 @@ class ItemForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
class Meta:
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')}

View File

@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
from mptt.models import MPTTModel, TreeForeignKey
from openslides.config.api import config
from openslides.core.models import Tag
from openslides.projector.api import (get_active_slide, reset_countdown,
start_countdown, stop_countdown,
update_projector, update_projector_overlay)
@ -107,6 +108,11 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
True, if the list of speakers is closed.
"""
tags = models.ManyToManyField(Tag, blank=True)
"""
Tags to categorise agenda items.
"""
class Meta:
permissions = (
('can_see_agenda', ugettext_noop("Can see agenda")),

View File

@ -34,8 +34,22 @@
<h1>{% trans "Agenda" %}
<small class="pull-right">
{% 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_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}"><i class="icon-import"></i> {% trans "Import" %}</a>
<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>
{% 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 %}
<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 %}

View File

@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.agenda.models import Item, Speaker
from openslides.core.models import Tag
from openslides.config.api import config
from openslides.poll.models import (BaseOption, BasePoll, BaseVote,
CollectDefaultVotesMixin,
@ -56,6 +57,7 @@ class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model):
max_length=79, null=True, blank=True,
verbose_name=ugettext_lazy("Default comment on the ballot paper"))
status = models.CharField(max_length=3, choices=STATUS, default='sea')
tags = models.ManyToManyField(Tag, blank=True)
class Meta:
permissions = (

View File

@ -21,6 +21,12 @@
{% 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>
{% 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 %}
<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 %}

View File

@ -152,6 +152,7 @@ class ConfigGroup(object):
A simple object class representing a group of variables (tuple) with
a special title.
"""
def __init__(self, title, variables):
self.title = title
self.variables = variables

View File

@ -0,0 +1,5 @@
from openslides.utils.exceptions import OpenSlidesError
class TagException(OpenSlidesError):
pass

View File

@ -44,3 +44,20 @@ class CustomSlide(SlideMixin, AbsoluteUrlMixin, models.Model):
else:
url = super(CustomSlide, self).get_absolute_url(link)
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

View 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');
};
},
});
}
});
});

View File

@ -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);
}
}
});
});

View File

@ -15,6 +15,7 @@
<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 '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 %}
<link href="{% static stylefile %}" type="text/css" rel="stylesheet" />
{% endfor %}
@ -132,6 +133,18 @@
<script src="{% static 'js/utils.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 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 %}
<script src="{% static javascript %}" type="text/javascript"></script>
{% endfor %}

View 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 %}

View File

@ -39,4 +39,8 @@ urlpatterns = patterns(
url(r'^customslide/(?P<pk>\d+)/del/$',
views.CustomSlideDeleteView.as_view(),
name='customslide_delete'),
url(r'tags/$',
views.TagListView.as_view(),
name='core_tag_list'),
)

View File

@ -2,6 +2,7 @@ from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.db import IntegrityError
from django.shortcuts import redirect, render_to_response
from django.template import RequestContext
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 .forms import SelectWidgetsForm
from .models import CustomSlide
from .models import CustomSlide, Tag
from .exceptions import TagException
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'
def get_context_data(self, **kwargs):
context = super(DashboardView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
widgets = []
for widget in Widget.get_all(self.request):
if widget.is_active():
@ -51,7 +53,7 @@ class SelectWidgetsView(utils_views.TemplateView):
template_name = 'core/select_widgets.html'
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)
for widget in widgets:
initial = {'widget': widget.is_active()}
@ -93,7 +95,7 @@ class VersionView(utils_views.TemplateView):
"""
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.
if RELEASE:
description = ''
@ -119,7 +121,7 @@ class SearchView(_SearchView):
def __call__(self, request):
if not request.user.is_authenticated() and not config['system_enable_anonymous']:
raise PermissionDenied
return super(SearchView, self).__call__(request)
return super().__call__(request)
def extra_context(self):
"""
@ -211,3 +213,81 @@ class CustomSlideDeleteView(CustomSlideViewMixin, utils_views.DeleteView):
Delete a custom slide.
"""
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)

View File

@ -1,7 +1,6 @@
from django import forms
from django.utils.translation import ugettext_lazy
from openslides.config.api import config
from openslides.mediafile.models import Mediafile
from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin,
CSVImportForm, LocalizedModelChoiceField)
@ -9,7 +8,7 @@ from openslides.users.models import User
from ckeditor.widgets import CKEditorWidget
from .models import Category, Motion, Workflow
from .models import Category, Motion, Workflow, Tag
class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
@ -47,6 +46,11 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
Attachments of the motion.
"""
tags = forms.ModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False,
label=ugettext_lazy('Tags'))
class Meta:
model = Motion
fields = ()
@ -54,7 +58,7 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
"""
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.initial = kwargs.setdefault('initial', {})
@ -64,8 +68,7 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
self.initial['text'] = last_version.text
self.initial['reason'] = last_version.reason
self.initial['attachments'] = self.motion.attachments.all()
else:
self.initial['text'] = config['motion_preamble']
self.initial['tags'] = self.motion.tags.all()
super(BaseMotionForm, self).__init__(*args, **kwargs)

View File

@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.config.api import config
from openslides.core.models import Tag
from openslides.mediafile.models import Mediafile
from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefaultVotesMixin)
from openslides.projector.models import RelatedModelMixin, SlideMixin
@ -69,8 +70,17 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
Many to many relation to mediafile objects.
"""
# TODO: proposal
# master = models.ForeignKey('self', null=True, blank=True)
parent = models.ForeignKey('self', null=True, blank=True, related_name='amendments')
"""
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:
permissions = (
@ -204,19 +214,31 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
def set_identifier(self):
"""
Sets the motion identifier automaticly according to the config
value if it is not set yet.
Sets the motion identifier automaticly according to the config value if
it is not set yet.
"""
# The identifier is already set or should be set manually
if config['motion_identifier'] == 'manually' or self.identifier:
# Do not set an identifier.
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':
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()
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 = ''
else:
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)
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):
"""
@ -810,10 +841,12 @@ class State(models.Model):
"""If true, new versions are not automaticly set active."""
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
this one, else it does."""
this one, else it does.
"""
def __str__(self):
"""Returns the name of the state."""

View File

@ -1,6 +1,6 @@
from django import forms
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.poll.models import PERCENT_BASE_CHOICES
@ -62,6 +62,25 @@ def setup_motion_config(sender, **kwargs):
motion_stop_submitting,
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
motion_min_supporters = ConfigVariable(
name='motion_min_supporters',
@ -143,7 +162,8 @@ def setup_motion_config(sender, **kwargs):
title=ugettext_noop('Motion'),
url='motion',
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):

View File

@ -29,6 +29,9 @@
</span>
{% endif %}
{% endif %}
{% if motion.is_amendment %}
(Amendment of <a href="{{ motion.parent|absolute_url }}">{{ motion.parent.identifier|default:motion.parent }}</a>)
{% endif %}
</small>
<small class="pull-right">
<a href="{% url 'motion_list' %}" class="btn btn-mini">
@ -68,6 +71,8 @@
</small>
</h1>
{{ motion.tags.all|join:', ' }}
<div class="row-fluid">
<div class="span8">
{# TODO: show only for workflow with versioning #}
@ -280,6 +285,23 @@
</h5>
{{ 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 -->
{% if perms.motion.can_support_motion and 'motion_min_supporters'|get_config > 0 %}
{% if allowed_actions.unsupport %}

View File

@ -7,25 +7,8 @@
{% block header %}
{{ block.super }}
<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 %}
{% 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 %}
{% if motion %}

View File

@ -33,11 +33,20 @@
<small class="pull-right">
{% if perms.motion.can_create_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 %}
{% 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 %}
<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>
{% 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>
@ -64,7 +73,14 @@
{% for motion in motion_list %}
<tr class="{% if motion.is_active_slide %}activeline{% endif %}">
<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-small"><span class="label label-info">{% trans motion.state.name %}</span></td>
<td class="optional">

View File

@ -77,6 +77,9 @@
{{ motion.active_version.title }}
<small>
{% 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 %}
</small>
</h1>

View File

@ -1,106 +1,111 @@
from django.conf.urls import patterns, url
from . import views
# TODO: define the Views inhere
urlpatterns = patterns(
'openslides.motion.views',
url(r'^$',
'motion_list',
views.MotionListView.as_view(),
name='motion_list'),
url(r'^new/$',
'motion_create',
# TODO: rename to motion_create
name='motion_new'),
views.MotionCreateView.as_view(),
name='motion_create'),
url(r'^(?P<pk>\d+)/$',
'motion_detail',
views.MotionDetailView.as_view(),
name='motion_detail'),
url(r'^(?P<pk>\d+)/edit/$',
'motion_update',
views.MotionUpdateView.as_view(),
name='motion_update'),
url(r'^(?P<pk>\d+)/del/$',
'motion_delete',
views.MotionDeleteView.as_view(),
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+)/$',
'motion_detail',
views.MotionDetailView.as_view(),
name='motion_version_detail'),
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/permit/$',
'version_permit',
views.VersionPermitView.as_view(),
name='motion_version_permit'),
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/del/$',
'version_delete',
views.VersionDeleteView.as_view(),
name='motion_version_delete'),
url(r'^(?P<pk>\d+)/diff/$',
'version_diff',
views.VersionDiffView.as_view(),
name='motion_version_diff'),
url(r'^(?P<pk>\d+)/support/$',
'motion_support',
views.SupportView.as_view(support=True),
name='motion_support'),
url(r'^(?P<pk>\d+)/unsupport/$',
'motion_unsupport',
views.SupportView.as_view(support=False),
name='motion_unsupport'),
url(r'^(?P<pk>\d+)/create_poll/$',
'poll_create',
views.PollCreateView.as_view(),
name='motionpoll_create'),
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/edit/$',
'poll_update',
views.PollUpdateView.as_view(),
name='motionpoll_update'),
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/del/$',
'poll_delete',
views.PollDeleteView.as_view(),
name='motionpoll_delete'),
url(r'^(?P<pk>\d+)/poll/(?P<poll_number>\d+)/pdf/$',
'poll_pdf',
views.PollPDFView.as_view(),
name='motionpoll_pdf'),
url(r'^(?P<pk>\d+)/set_state/(?P<state>\d+)/$',
'set_state',
views.MotionSetStateView.as_view(),
name='motion_set_state'),
url(r'^(?P<pk>\d+)/reset_state/$',
'reset_state',
views.MotionSetStateView.as_view(reset=True),
name='motion_reset_state'),
url(r'^(?P<pk>\d+)/agenda/$',
'create_agenda_item',
views.CreateRelatedAgendaItemView.as_view(),
name='motion_create_agenda'),
url(r'^pdf/$',
'motion_list_pdf',
views.MotionPDFView.as_view(print_all_motions=True),
name='motion_list_pdf'),
url(r'^(?P<pk>\d+)/pdf/$',
'motion_detail_pdf',
views.MotionPDFView.as_view(print_all_motions=False),
name='motion_detail_pdf'),
url(r'^category/$',
'category_list',
views.CategoryListView.as_view(),
name='motion_category_list'),
url(r'^category/new/$',
'category_create',
views.CategoryCreateView.as_view(),
name='motion_category_create'),
url(r'^category/(?P<pk>\d+)/edit/$',
'category_update',
views.CategoryUpdateView.as_view(),
name='motion_category_update'),
url(r'^category/(?P<pk>\d+)/del/$',
'category_delete',
views.CategoryDeleteView.as_view(),
name='motion_category_delete'),
url(r'^csv_import/$',
'motion_csv_import',
views.MotionCSVImportView.as_view(),
name='motion_csv_import'),
)

View File

@ -47,7 +47,7 @@ class MotionListView(ListView):
Returns not a QuerySet but a filtered list of motions. Excludes motions
that the user is not allowed to see.
"""
queryset = super(MotionListView, self).get_queryset(*args, **kwargs)
queryset = super().get_queryset(*args, **kwargs)
motions = []
for motion in queryset:
if (not motion.state.required_permission_to_see or
@ -55,8 +55,6 @@ class MotionListView(ListView):
motions.append(motion)
return motions
motion_list = MotionListView.as_view()
class MotionDetailView(DetailView):
"""
@ -92,9 +90,7 @@ class MotionDetailView(DetailView):
'title': version.title,
'text': version.text,
'reason': version.reason})
return super(MotionDetailView, self).get_context_data(**kwargs)
motion_detail = MotionDetailView.as_view()
return super().get_context_data(**kwargs)
class MotionEditMixin(object):
@ -142,6 +138,10 @@ class MotionEditMixin(object):
self.object.attachments.clear()
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
# the model, because bulk_create does not call the save method.
active_slide = get_active_slide()
@ -203,17 +203,26 @@ class MotionCreateView(MotionEditMixin, CreateView):
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)
# Set submitter to request.user if no submitter is set yet
if ('submitter' not in form.cleaned_data or
not form.cleaned_data['submitter']):
self.object.add_submitter(self.request.user)
return response
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'):
initial['workflow'] = config['motion_workflow']
return initial
@ -227,7 +236,53 @@ class MotionCreateView(MotionEditMixin, CreateView):
self.object.reset_state(workflow)
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):
@ -246,7 +301,7 @@ class MotionUpdateView(MotionEditMixin, UpdateView):
"""
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()
if (config['motion_remove_supporters'] and self.object.state.allow_support and
not self.request.user.has_perm('motion.can_manage_motion')):
@ -271,7 +326,7 @@ class MotionUpdateView(MotionEditMixin, UpdateView):
self.request.user)
def get_initial(self):
initial = super(MotionUpdateView, self).get_initial()
initial = super().get_initial()
if self.request.user.has_perm('motion.can_manage_motion'):
initial['workflow'] = self.object.state.workflow
return initial
@ -295,8 +350,6 @@ class MotionUpdateView(MotionEditMixin, UpdateView):
self.version = self.object.get_last_version()
self.used_new_version = False
motion_update = MotionUpdateView.as_view()
class MotionDeleteView(DeleteView):
"""
@ -314,8 +367,6 @@ class MotionDeleteView(DeleteView):
def get_final_message(self):
return _('%s was successfully deleted.') % _('Motion')
motion_delete = MotionDeleteView.as_view()
class VersionDeleteView(DeleteView):
"""
@ -347,8 +398,6 @@ class VersionDeleteView(DeleteView):
def get_url_name_args(self):
return (self.get_object().motion_id, )
version_delete = VersionDeleteView.as_view()
class VersionPermitView(SingleObjectMixin, QuestionView):
"""
@ -368,7 +417,7 @@ class VersionPermitView(SingleObjectMixin, QuestionView):
self.version = self.get_object().versions.get(version_number=int(version_number))
except MotionVersion.DoesNotExist:
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):
"""
@ -394,8 +443,6 @@ class VersionPermitView(SingleObjectMixin, QuestionView):
ugettext_noop('permitted')],
person=self.request.user)
version_permit = VersionPermitView.as_view()
class VersionDiffView(DetailView):
"""
@ -427,7 +474,7 @@ class VersionDiffView(DetailView):
version_rev2 = None
diff_text = None
diff_reason = None
context = super(VersionDiffView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context.update({
'version_rev1': version_rev1,
'version_rev2': version_rev2,
@ -436,8 +483,6 @@ class VersionDiffView(DetailView):
})
return context
version_diff = VersionDiffView.as_view()
class SupportView(SingleObjectMixin, QuestionView):
"""
@ -500,9 +545,6 @@ class SupportView(SingleObjectMixin, QuestionView):
else:
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):
"""
@ -526,8 +568,6 @@ class PollCreateView(SingleObjectMixin, RedirectView):
"""
return reverse('motionpoll_update', args=[self.get_object().pk, self.poll.poll_number])
poll_create = PollCreateView.as_view()
class PollMixin(object):
"""
@ -579,7 +619,7 @@ class PollUpdateView(PollMixin, PollFormView):
Append the motion object to the context.
"""
context = super(PollUpdateView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context.update({
'motion': self.poll.motion,
'poll': self.poll})
@ -589,12 +629,10 @@ class PollUpdateView(PollMixin, PollFormView):
"""
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)
return value
poll_update = PollUpdateView.as_view()
class PollDeleteView(PollMixin, DeleteView):
"""
@ -607,7 +645,7 @@ class PollDeleteView(PollMixin, DeleteView):
"""
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)
def get_redirect_url(self, **kwargs):
@ -616,8 +654,6 @@ class PollDeleteView(PollMixin, DeleteView):
"""
return reverse('motion_detail', args=[self.get_object().motion.pk])
poll_delete = PollDeleteView.as_view()
class PollPDFView(PollMixin, PDFView):
"""
@ -647,8 +683,6 @@ class PollPDFView(PollMixin, PDFView):
"""
motion_poll_to_pdf(pdf, self.get_object())
poll_pdf = PollPDFView.as_view()
class MotionSetStateView(SingleObjectMixin, RedirectView):
"""
@ -686,9 +720,6 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
_('The state of the motion was set to %s.')
% html_strong(_(self.get_object().state.name)))
set_state = MotionSetStateView.as_view()
reset_state = MotionSetStateView.as_view(reset=True)
class CreateRelatedAgendaItemView(_CreateRelatedAgendaItemView):
"""
@ -700,11 +731,9 @@ class CreateRelatedAgendaItemView(_CreateRelatedAgendaItemView):
"""
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)
create_agenda_item = CreateRelatedAgendaItemView.as_view()
class MotionPDFView(SingleObjectMixin, PDFView):
"""
@ -734,7 +763,7 @@ class MotionPDFView(SingleObjectMixin, PDFView):
if self.print_all_motions:
obj = None
else:
obj = super(MotionPDFView, self).get_object(*args, **kwargs)
obj = super().get_object(*args, **kwargs)
return obj
def get_filename(self):
@ -765,16 +794,11 @@ class MotionPDFView(SingleObjectMixin, PDFView):
else:
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):
required_permission = 'motion.can_manage_motion'
model = Category
category_list = CategoryListView.as_view()
class CategoryCreateView(CreateView):
required_permission = 'motion.can_manage_motion'
@ -782,8 +806,6 @@ class CategoryCreateView(CreateView):
success_url_name = 'motion_category_list'
url_name_args = []
category_create = CategoryCreateView.as_view()
class CategoryUpdateView(UpdateView):
required_permission = 'motion.can_manage_motion'
@ -791,8 +813,6 @@ class CategoryUpdateView(UpdateView):
success_url_name = 'motion_category_list'
url_name_args = []
category_update = CategoryUpdateView.as_view()
class CategoryDeleteView(DeleteView):
required_permission = 'motion.can_manage_motion'
@ -801,8 +821,6 @@ class CategoryDeleteView(DeleteView):
url_name_args = []
success_url_name = 'motion_category_list'
category_delete = CategoryDeleteView.as_view()
class MotionCSVImportView(CSVImportView):
"""
@ -817,7 +835,7 @@ class MotionCSVImportView(CSVImportView):
"""
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 return_value
@ -828,5 +846,3 @@ class MotionCSVImportView(CSVImportView):
messages.error(self.request, error)
# Overleap method of CSVImportView
return super(CSVImportView, self).form_valid(form)
motion_csv_import = MotionCSVImportView.as_view()

View File

@ -170,11 +170,14 @@ def create_builtin_groups_and_admin(sender, **kwargs):
ct_config = ContentType.objects.get(app_label='config', model='configstore')
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)
# add delegate permissions (without can_support_motion)
group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35)
# 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
group_staff.permissions.add(perm_17) # TODO: Remove this redundancy after cleanup of the permission system

View File

@ -3,24 +3,6 @@
{% load i18n %}
{% 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 %}
{% if edit_user %}

View File

@ -292,8 +292,12 @@ class AjaxView(PermissionMixin, AjaxMixin, View):
View for ajax requests.
"""
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)
def post(self, *args, **kwargs):
return self.get(*args, **kwargs)
class RedirectView(PermissionMixin, AjaxMixin, UrlMixin, django_views.RedirectView):
"""

View File

@ -142,3 +142,24 @@ class CustomSlidesTest(TestCase):
response = self.admin_client.post(url, {'yes': 'true'})
self.assertRedirects(response, '/dashboard/')
self.assertFalse(CustomSlide.objects.exists())
class TagListViewTest(TestCase):
def test_get_tag_queryset(self):
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')

View File

@ -145,6 +145,64 @@ class ModelTest(TestCase):
# motion.__unicode__() raised an AttributeError
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):
def test_stop_submitting(self):

View File

@ -1,11 +1,14 @@
import os
import tempfile
from unittest.mock import MagicMock
from django.conf import settings
from django.test import RequestFactory
from django.test.client import Client
from openslides.config.api import config
from openslides.mediafile.models import Mediafile
from openslides.motion import views
from openslides.motion.models import Category, Motion, MotionLog, State
from openslides.users.models import Group, User
from openslides.utils.test import TestCase
@ -241,6 +244,67 @@ class TestMotionCreateView(MotionViewTestCase):
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):
url = '/motion/1/edit/'