commit
d0d674601d
@ -8,6 +8,9 @@ Version 1.7 (unreleased)
|
||||
========================
|
||||
[https://github.com/OpenSlides/OpenSlides/milestones/1.7]
|
||||
|
||||
Core:
|
||||
- New feature to tag motions, agenda and assignments.
|
||||
|
||||
motion:
|
||||
- New Feature to create amendments, which are related to a parent motion.
|
||||
|
||||
|
@ -32,7 +32,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')}
|
||||
|
||||
|
||||
|
@ -13,6 +13,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)
|
||||
@ -109,6 +110,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")),
|
||||
|
@ -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 %}
|
||||
|
@ -8,6 +8,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,
|
||||
@ -58,6 +59,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 = (
|
||||
|
@ -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 %}
|
||||
|
@ -154,6 +154,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
|
||||
|
7
openslides/core/exceptions.py
Normal file
7
openslides/core/exceptions.py
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
|
||||
|
||||
class TagException(OpenSlidesError):
|
||||
pass
|
@ -38,3 +38,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 __unicode__(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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -114,7 +129,7 @@ $(document).ready(function(){
|
||||
}
|
||||
// Sticky navigation
|
||||
$(window).scroll(function(){
|
||||
var el = $('.leftmenu > ul');
|
||||
var el = $('.leftmenu > ul');
|
||||
if($(window).width() > 479) {
|
||||
if ( ($(this).scrollTop() > 80) && ($(this).scrollLeft() < 10)) {
|
||||
el.css({'position':'fixed','top':'10px','width':'14.15%'});
|
||||
@ -188,12 +203,12 @@ $(document).ready(function(){
|
||||
});
|
||||
$('#content').removeClass('span10').addClass('span11');
|
||||
}
|
||||
|
||||
|
||||
function fullmenu(){
|
||||
$('.leftmenu').removeClass('span1').removeClass('lefticon').addClass('span2');
|
||||
$('.leftmenu > ul > li > a').each(function(){
|
||||
$(this).attr({'rel':'','title':''});
|
||||
});
|
||||
$('#content').removeClass('span11').addClass('span10');
|
||||
$('#content').removeClass('span11').addClass('span10');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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 %}
|
||||
|
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 %}
|
@ -41,4 +41,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'),
|
||||
)
|
||||
|
@ -4,6 +4,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
|
||||
@ -19,7 +20,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):
|
||||
@ -213,3 +215,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(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)
|
||||
|
@ -10,7 +10,7 @@ from openslides.utils.person import MultiplePersonFormField, PersonFormField
|
||||
|
||||
from ckeditor.widgets import CKEditorWidget
|
||||
|
||||
from .models import Category, Motion, Workflow
|
||||
from .models import Category, Motion, Workflow, Tag
|
||||
|
||||
|
||||
class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
||||
@ -48,6 +48,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 = ()
|
||||
@ -55,7 +60,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', {})
|
||||
@ -65,6 +70,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()
|
||||
self.initial['tags'] = self.motion.tags.all()
|
||||
super(BaseMotionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
@ -8,6 +8,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
|
||||
@ -78,6 +79,11 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
|
||||
Null if the motion is not an amendment.
|
||||
"""
|
||||
|
||||
tags = models.ManyToManyField(Tag)
|
||||
"""
|
||||
Tags to categorise motions.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('can_see_motion', ugettext_noop('Can see motions')),
|
||||
|
@ -71,6 +71,8 @@
|
||||
</small>
|
||||
</h1>
|
||||
|
||||
{{ motion.tags.all|join:', ' }}
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
{# TODO: show only for workflow with versioning #}
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
{% 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>
|
||||
|
@ -140,6 +140,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()
|
||||
|
@ -173,11 +173,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_participant permission
|
||||
group_staff.permissions.add(perm_17) # TODO: Remove this redundancy after cleanup of the permission system
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -295,8 +295,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):
|
||||
"""
|
||||
|
@ -143,3 +143,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')
|
||||
|
Loading…
Reference in New Issue
Block a user