Merge pull request #1381 from ostcar/motion-tags

Tags for motions
This commit is contained in:
Oskar Hahn 2015-01-05 13:55:26 +01:00
commit d0d674601d
25 changed files with 400 additions and 47 deletions

View File

@ -8,6 +8,9 @@ Version 1.7 (unreleased)
======================== ========================
[https://github.com/OpenSlides/OpenSlides/milestones/1.7] [https://github.com/OpenSlides/OpenSlides/milestones/1.7]
Core:
- New feature to tag motions, agenda and assignments.
motion: motion:
- New Feature to create amendments, which are related to a parent motion. - New Feature to create amendments, which are related to a parent motion.

View File

@ -32,7 +32,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')}

View File

@ -13,6 +13,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)
@ -109,6 +110,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")),

View File

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

View File

@ -8,6 +8,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,
@ -58,6 +59,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 = (

View File

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

View File

@ -154,6 +154,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

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from openslides.utils.exceptions import OpenSlidesError
class TagException(OpenSlidesError):
pass

View File

@ -38,3 +38,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 __unicode__(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/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 %}

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

@ -41,4 +41,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'),
) )

View File

@ -4,6 +4,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
@ -19,7 +20,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):
@ -213,3 +215,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(TagListView, self).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(TagListView, self).get_ajax_context(
pk=getattr(self, 'pk', None),
action=getattr(self, 'action', None),
error=getattr(self, 'error', None),
**context)

View File

@ -10,7 +10,7 @@ from openslides.utils.person import MultiplePersonFormField, PersonFormField
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):
@ -48,6 +48,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 = ()
@ -55,7 +60,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', {})
@ -65,6 +70,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()
self.initial['tags'] = self.motion.tags.all()
super(BaseMotionForm, self).__init__(*args, **kwargs) super(BaseMotionForm, self).__init__(*args, **kwargs)

View File

@ -8,6 +8,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
@ -78,6 +79,11 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
Null if the motion is not an amendment. Null if the motion is not an amendment.
""" """
tags = models.ManyToManyField(Tag)
"""
Tags to categorise motions.
"""
class Meta: class Meta:
permissions = ( permissions = (
('can_see_motion', ugettext_noop('Can see motions')), ('can_see_motion', ugettext_noop('Can see motions')),

View File

@ -71,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 #}

View File

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

View File

@ -36,8 +36,17 @@
<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> <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>

View File

@ -140,6 +140,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()

View File

@ -173,11 +173,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_participant permission # add can_see_participant 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

View File

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

View File

@ -295,8 +295,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):
""" """

View File

@ -143,3 +143,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')