Merge remote-tracking branch 'upstream/master' into OpenSlides-3

This commit is contained in:
Oskar Hahn 2018-08-20 20:54:54 +02:00
commit d11d2844b8
33 changed files with 1508 additions and 1073 deletions

View File

@ -7,6 +7,9 @@ https://openslides.org/
Version 2.3 (unreleased) Version 2.3 (unreleased)
======================== ========================
Agenda:
- New item type 'hidden'. New visibilty filter in agenda [#3790].
Motions: Motions:
- New feature to scroll the projector to a specific line [#3748]. - New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647]. - New possibility to sort submitters [#3647].
@ -17,11 +20,13 @@ Motions:
- New table of contents with page numbers and categories in PDF [#3766]. - New table of contents with page numbers and categories in PDF [#3766].
- New teporal field "modified final version" where the final version can - New teporal field "modified final version" where the final version can
be edited [#3781]. be edited [#3781].
- New config to show amendments also in motions table [#3792]
Core: Core:
- Python 3.4 is not supported anymore [#3777]. - Python 3.4 is not supported anymore [#3777].
- Support Python 3.7. - Support Python 3.7 [#3786].
- Updated pdfMake to 0.1.37 [#3766]. - Updated pdfMake to 0.1.37 [#3766].
- Updated Django to 2.1 [#3777, #3786].
Version 2.2 (2018-06-06) Version 2.2 (2018-06-06)

View File

@ -23,7 +23,8 @@ class ItemAccessPermissions(BaseAccessPermissions):
return ItemSerializer return ItemSerializer
# TODO: In the following method we use full_data['is_hidden'] but this can be out of date. # TODO: In the following method we use full_data['is_hidden'] and
# full_data['is_internal'] but this can be out of date.
def get_restricted_data( def get_restricted_data(
self, self,
@ -33,8 +34,10 @@ class ItemAccessPermissions(BaseAccessPermissions):
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. for the user.
Hidden items can only be seen by managers with can_manage permission.
We remove comments for non admins/managers and a lot of fields of We remove comments for non admins/managers and a lot of fields of
hidden items for users without permission to see hidden items. internal items for users without permission to see internal items.
""" """
def filtered_data(full_data, blocked_keys): def filtered_data(full_data, blocked_keys):
""" """
@ -45,38 +48,45 @@ class ItemAccessPermissions(BaseAccessPermissions):
# Parse data. # Parse data.
if has_perm(user, 'agenda.can_see'): if has_perm(user, 'agenda.can_see'):
if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_hidden_items'): if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_internal_items'):
# Managers with special permission can see everything. # Managers with special permission can see everything.
data = full_data data = full_data
elif has_perm(user, 'agenda.can_see_hidden_items'): elif has_perm(user, 'agenda.can_see_internal_items'):
# Non managers with special permission can see everything but comments. # Non managers with special permission can see everything but
# comments and hidden items.
data = [full for full in full_data if not full['is_hidden']] # filter hidden items
blocked_keys = ('comment',) blocked_keys = ('comment',)
data = [filtered_data(full, blocked_keys) for full in full_data] data = [filtered_data(full, blocked_keys) for full in data] # remove blocked_keys
else: else:
# Users without special permissin for hidden items. # Users without special permission for internal items.
# In hidden case managers and non managers see only some fields # In internal and hidden case managers and non managers see only some fields
# so that list of speakers is provided regardless. # so that list of speakers is provided regardless. Hidden items can only be seen by managers.
blocked_keys_hidden_case = set(full_data[0].keys()) - set(( blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set((
'id', 'id',
'title', 'title',
'speakers', 'speakers',
'speaker_list_closed', 'speaker_list_closed',
'content_object')) 'content_object'))
# In non hidden case managers see everything and non managers see # In non internal case managers see everything and non managers see
# everything but comments. # everything but comments.
if has_perm(user, 'agenda.can_manage'): if has_perm(user, 'agenda.can_manage'):
blocked_keys_non_hidden_case = [] # type: Iterable[str] blocked_keys_non_internal_hidden_case = [] # type: Iterable[str]
can_see_hidden = True
else: else:
blocked_keys_non_hidden_case = ('comment',) blocked_keys_non_internal_hidden_case = ('comment',)
can_see_hidden = False
data = [] data = []
for full in full_data: for full in full_data:
if full['is_hidden']: if full['is_hidden'] and can_see_hidden:
data.append(filtered_data(full, blocked_keys_hidden_case)) # Same filtering for internal and hidden items
else: data.append(filtered_data(full, blocked_keys_internal_hidden_case))
data.append(filtered_data(full, blocked_keys_non_hidden_case)) if full['is_internal']:
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
else: # agenda item
data.append(filtered_data(full, blocked_keys_non_internal_hidden_case))
else: else:
data = [] data = []

View File

@ -59,6 +59,19 @@ def get_config_variables():
group='Agenda', group='Agenda',
subgroup='General') subgroup='General')
yield ConfigVariable(
name='agenda_new_items_default_visibility',
default_value='2',
input_type='choice',
choices=(
{'value': '1', 'display_name': 'Public item'},
{'value': '2', 'display_name': 'Internal item'},
{'value': '3', 'display_name': 'Hidden item'}),
label='Default visibility for new agenda items',
weight=227,
group='Agenda',
subgroup='General')
# List of speakers # List of speakers
yield ConfigVariable( yield ConfigVariable(

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-08-15 09:09
from __future__ import unicode_literals
from django.contrib.auth.models import Permission
from django.db import migrations, models
from openslides.utils.migrations import \
add_permission_to_groups_based_on_existing_permission
def delete_old_can_see_hidden_permission(apps, schema_editor):
perm = Permission.objects.filter(codename='can_see_hidden_items')
if len(perm):
perm = perm.delete()
class Migration(migrations.Migration):
dependencies = [
('agenda', '0004_speaker_marked'),
]
operations = [
migrations.AlterModelOptions(
name='item',
options={
'default_permissions': (),
'permissions': (
('can_see', 'Can see agenda'),
('can_manage', 'Can manage agenda'),
('can_manage_list_of_speakers', 'Can manage list of speakers'),
('can_see_internal_items', 'Can see internal items and time scheduling of agenda')
)
},
),
migrations.AlterField(
model_name='item',
name='type',
field=models.IntegerField(
choices=[
(1, 'Agenda item'),
(2, 'Internal item'),
(3, 'Hidden item')
],
default=3
),
),
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
'can_see_hidden_items', 'item', 'agenda', 'can_see_internal_items', 'Can see internal items and time scheduling of agenda'
)),
migrations.RunPython(delete_old_can_see_hidden_permission),
]

View File

@ -33,58 +33,43 @@ class ItemManager(models.Manager):
""" """
return self.get_queryset().prefetch_related('speakers', 'content_object') return self.get_queryset().prefetch_related('speakers', 'content_object')
def get_only_agenda_items(self): def get_only_non_public_items(self):
""" """
Generator, which yields only agenda items. Skips hidden items. Generator, which yields only internal and hidden items, that means only items
which type is INTERNAL_ITEM or HIDDEN_ITEM or which are children of hidden items.
""" """
# Do not execute item.is_hidden() because this would create a lot of db queries # Do not execute non-hidden items because this would create a lot of db queries
root_items, item_children = self.get_root_and_children(only_agenda_items=True) root_items, item_children = self.get_root_and_children(only_item_type=None)
def yield_items(items): def yield_items(items, parent_is_not_public=False):
""" """
Generator that yields a list of items and their children. Generator that yields a list of items and their children.
""" """
for item in items: for item in items:
yield item if parent_is_not_public or item.type in (item.INTERNAL_ITEM, item.HIDDEN_ITEM):
yield from yield_items(item_children[item.pk]) item_is_not_public = True
yield from yield_items(root_items)
def get_only_hidden_items(self):
"""
Generator, which yields only hidden items, that means only items
which type is HIDDEN_ITEM or which are children of hidden items.
"""
# Do not execute item.is_hidden() because this would create a lot of db queries
root_items, item_children = self.get_root_and_children(only_agenda_items=False)
def yield_items(items, parent_is_hidden=False):
"""
Generator that yields a list of items and their children.
"""
for item in items:
if parent_is_hidden or item.type == item.HIDDEN_ITEM:
item_is_hidden = True
yield item yield item
else: else:
item_is_hidden = False item_is_not_public = False
yield from yield_items(item_children[item.pk], parent_is_hidden=item_is_hidden) yield from yield_items(
item_children[item.pk],
parent_is_not_public=item_is_not_public)
yield from yield_items(root_items) yield from yield_items(root_items)
def get_root_and_children(self, only_agenda_items=False): def get_root_and_children(self, only_item_type=None):
""" """
Returns a list with all root items and a dictonary where the key is an Returns a list with all root items and a dictonary where the key is an
item pk and the value is a list with all children of the item. item pk and the value is a list with all children of the item.
If only_agenda_items is True, the tree hides items with type If only_item_type is given, the tree hides items with other types and
HIDDEN_ITEM and all of their children. all of their children.
""" """
queryset = self.order_by('weight') queryset = self.order_by('weight')
item_children = defaultdict(list) # type: Dict[int, List[Item]] item_children = defaultdict(list) # type: Dict[int, List[Item]]
root_items = [] root_items = []
for item in queryset: for item in queryset:
if only_agenda_items and item.type == item.HIDDEN_ITEM: if only_item_type is not None and item.type != only_item_type:
continue continue
if item.parent_id is not None: if item.parent_id is not None:
item_children[item.parent_id].append(item) item_children[item.parent_id].append(item)
@ -92,19 +77,19 @@ class ItemManager(models.Manager):
root_items.append(item) root_items.append(item)
return root_items, item_children return root_items, item_children
def get_tree(self, only_agenda_items=False, include_content=False): def get_tree(self, only_item_type=None, include_content=False):
""" """
Generator that yields dictonaries. Each dictonary has two keys, id Generator that yields dictonaries. Each dictonary has two keys, id
and children, where id is the id of one agenda item and children is a and children, where id is the id of one agenda item and children is a
generator that yields dictonaries like the one discribed. generator that yields dictonaries like the one discribed.
If only_agenda_items is True, the tree hides items with type If only_item_type is given, the tree hides items with other types and
HIDDEN_ITEM and all of their children. all of their children.
If include_content is True, the yielded dictonaries have no key 'id' If include_content is True, the yielded dictonaries have no key 'id'
but a key 'item' with the entire object. but a key 'item' with the entire object.
""" """
root_items, item_children = self.get_root_and_children(only_agenda_items=only_agenda_items) root_items, item_children = self.get_root_and_children(only_item_type=only_item_type)
def get_children(items): def get_children(items):
""" """
@ -184,10 +169,10 @@ class ItemManager(models.Manager):
walk_tree(tree_element['children'], item_number) walk_tree(tree_element['children'], item_number)
# Start numbering visable agenda items. # Start numbering visable agenda items.
walk_tree(self.get_tree(only_agenda_items=True, include_content=True)) walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True))
# Reset number of hidden items. # Reset number of hidden items.
for item in self.get_only_hidden_items(): for item in self.get_only_non_public_items():
item.item_number = '' item.item_number = ''
item.save() item.save()
@ -200,10 +185,12 @@ class Item(RESTModelMixin, models.Model):
objects = ItemManager() objects = ItemManager()
AGENDA_ITEM = 1 AGENDA_ITEM = 1
HIDDEN_ITEM = 2 INTERNAL_ITEM = 2
HIDDEN_ITEM = 3
ITEM_TYPE = ( ITEM_TYPE = (
(AGENDA_ITEM, ugettext_lazy('Agenda item')), (AGENDA_ITEM, ugettext_lazy('Agenda item')),
(INTERNAL_ITEM, ugettext_lazy('Internal item')),
(HIDDEN_ITEM, ugettext_lazy('Hidden item'))) (HIDDEN_ITEM, ugettext_lazy('Hidden item')))
item_number = models.CharField(blank=True, max_length=255) item_number = models.CharField(blank=True, max_length=255)
@ -281,7 +268,7 @@ class Item(RESTModelMixin, models.Model):
('can_see', 'Can see agenda'), ('can_see', 'Can see agenda'),
('can_manage', 'Can manage agenda'), ('can_manage', 'Can manage agenda'),
('can_manage_list_of_speakers', 'Can manage list of speakers'), ('can_manage_list_of_speakers', 'Can manage list of speakers'),
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')) ('can_see_internal_items', 'Can see internal items and time scheduling of agenda'))
unique_together = ('content_type', 'object_id') unique_together = ('content_type', 'object_id')
def __str__(self): def __str__(self):
@ -320,6 +307,16 @@ class Item(RESTModelMixin, models.Model):
raise NotImplementedError('You have to provide a get_agenda_list_view_title ' raise NotImplementedError('You have to provide a get_agenda_list_view_title '
'method on your related model.') 'method on your related model.')
def is_internal(self):
"""
Returns True if the type of this object itself is a internal item or any
of its ancestors has such a type.
Attention! This executes one query for each ancestor of the item.
"""
return (self.type == self.INTERNAL_ITEM or
(self.parent is not None and self.parent.is_internal()))
def is_hidden(self): def is_hidden(self):
""" """
Returns True if the type of this object itself is a hidden item or any Returns True if the type of this object itself is a hidden item or any

View File

@ -49,6 +49,7 @@ class ItemSerializer(ModelSerializer):
'comment', 'comment',
'closed', 'closed',
'type', 'type',
'is_internal',
'is_hidden', 'is_hidden',
'duration', 'duration',
'speakers', 'speakers',

View File

@ -55,13 +55,13 @@ def listen_to_related_object_post_delete(sender, instance, **kwargs):
def get_permission_change_data(sender, permissions, **kwargs): def get_permission_change_data(sender, permissions, **kwargs):
""" """
Yields all necessary collections if 'agenda.can_see' or Yields all necessary collections if 'agenda.can_see' or
'agenda.can_see_hidden_items' permissions changes. 'agenda.can_see_internal_items' permissions changes.
""" """
agenda_app = apps.get_app_config(app_label='agenda') agenda_app = apps.get_app_config(app_label='agenda')
for permission in permissions: for permission in permissions:
# There could be only one 'agenda.can_see' and then we want to return data. # There could be only one 'agenda.can_see' and then we want to return data.
if (permission.content_type.app_label == agenda_app.label if (permission.content_type.app_label == agenda_app.label
and permission.codename in ('can_see', 'can_see_hidden_items')): and permission.codename in ('can_see', 'can_see_internal_items')):
yield from agenda_app.get_startup_elements() yield from agenda_app.get_startup_elements()
break break

View File

@ -39,6 +39,11 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Agenda'), verboseName: gettext('Agenda'),
computed: {
is_public: function () {
return !this.is_internal && !this.is_hidden;
},
},
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; return name;

View File

@ -17,10 +17,9 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
// generate the item list with all subitems // generate the item list with all subitems
var createItemList = function() { var createItemList = function() {
var agenda_items = []; var agenda_items = [];
angular.forEach(items, function (item) { _.forEach(items, function (item) {
if (item.is_hidden === false) { if (item.is_public) {
var itemIndent = item.parentCount * 15;
var itemIndent = item.parentCount * 20;
var itemStyle; var itemStyle;
if (item.parentCount === 0) { if (item.parentCount === 0) {
@ -29,13 +28,6 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
itemStyle = 'listChild'; itemStyle = 'listChild';
} }
var itemNumberWidth;
if (item.item_number === "") {
itemNumberWidth = 0;
} else {
itemNumberWidth = 60;
}
var agendaJsonString = { var agendaJsonString = {
style: itemStyle, style: itemStyle,
columns: [ columns: [
@ -44,7 +36,7 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
text: '' text: ''
}, },
{ {
width: itemNumberWidth, width: 60,
text: item.item_number text: item.item_number
}, },
{ {

View File

@ -84,12 +84,14 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
Config.lastModified('agenda_hide_internal_items_on_projector'); Config.lastModified('agenda_hide_internal_items_on_projector');
}, function () { }, function () {
if ($scope.element.id) { if ($scope.element.id) {
// remove hidden items
items = _.filter(Agenda.getAll(), function (item) {
return !item.is_hidden;
});
if (Config.get('agenda_hide_internal_items_on_projector').value) { if (Config.get('agenda_hide_internal_items_on_projector').value) {
items = _.filter(Agenda.getAll(), function (item) { items = _.filter(items, function (item) {
return item.type === 1; return item.is_public;
}); });
} else {
items = Agenda.getAll();
} }
var tree = AgendaTree.getTree(items); var tree = AgendaTree.getTree(items);
@ -115,7 +117,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
}); });
} else if ($scope.element.tree) { } else if ($scope.element.tree) {
items = _.filter(Agenda.getAll(), function (item) { items = _.filter(Agenda.getAll(), function (item) {
return item.type === 1; return item.is_public;
}); });
$scope.tree = AgendaTree.getTree(items); $scope.tree = AgendaTree.getTree(items);
} else { } else {
@ -124,7 +126,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
orderBy: 'weight' orderBy: 'weight'
}); });
items = _.filter(items, function (item) { items = _.filter(items, function (item) {
return item.type === 1; return item.is_public;
}); });
$scope.tree = AgendaTree.getTree(items); $scope.tree = AgendaTree.getTree(items);
} }

View File

@ -85,6 +85,30 @@ angular.module('OpenSlidesApp.agenda.site', [
} }
]) ])
.factory('ShowAsAgendaItemField', [
'operator',
'gettext',
'gettextCatalog',
function (operator, gettext, gettextCatalog) {
return function (managePermission) {
return {
key: 'agenda_type',
type: 'select-single',
templateOptions: {
label: gettextCatalog.getString('Agenda visibility'),
options: [
{type: 1, displayName: gettext('Public item')},
{type: 2, displayName: gettext('Internal item')},
{type: 3, displayName: gettext('Hidden item')}
],
ngOptions: 'type.type as (type.displayName | translate) for type in to.options',
},
hide: !(operator.hasPerms(managePermission) && operator.hasPerms('agenda.can_manage'))
};
};
}
])
.controller('ItemListCtrl', [ .controller('ItemListCtrl', [
'$scope', '$scope',
'$filter', '$filter',
@ -109,6 +133,11 @@ angular.module('OpenSlidesApp.agenda.site', [
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm,
AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter, AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter,
osTablePagination, AgendaCsvExport, AgendaPdfExport, AgendaDocxExport, ErrorMessage) { osTablePagination, AgendaCsvExport, AgendaPdfExport, AgendaDocxExport, ErrorMessage) {
$scope.AGENDA_ITEM = 1;
$scope.INTERNAL_ITEM = 2;
$scope.HIDDEN_ITEM = 3;
// Bind agenda tree to the scope // Bind agenda tree to the scope
$scope.$watch(function () { $scope.$watch(function () {
return Agenda.lastModified(); return Agenda.lastModified();
@ -143,16 +172,31 @@ angular.module('OpenSlidesApp.agenda.site', [
$scope.filter.booleanFilters = { $scope.filter.booleanFilters = {
closed: { closed: {
value: undefined, value: undefined,
defaultValue: undefined,
displayName: gettext('Closed items'), displayName: gettext('Closed items'),
choiceYes: gettext('Closed items'), choiceYes: gettext('Closed items'),
choiceNo: gettext('Open items'), choiceNo: gettext('Open items'),
}, },
is_hidden: { // The next filters are just on-off, so no undefined there
value: undefined, is_public: {
displayName: gettext('Internal items'), value: true,
defaultValue: true,
choiceYes: gettext('Public items'),
choiceNo: gettext('No public items'),
},
is_internal: {
value: true,
defaultValue: true,
choiceYes: gettext('Internal items'), choiceYes: gettext('Internal items'),
choiceNo: gettext('No internal items'), choiceNo: gettext('No internal items'),
permission: 'agenda.can_see_hidden_items', permission: 'agenda.can_see_internal_items',
},
is_hidden: {
value: false,
defaultValue: false,
choiceYes: gettext('Hidden items'),
choiceNo: gettext('No hidden items'),
permission: 'agenda.can_manage',
}, },
}; };
} }
@ -160,10 +204,23 @@ angular.module('OpenSlidesApp.agenda.site', [
$scope.filter.propertyFunctionList = [ $scope.filter.propertyFunctionList = [
function (item) {return item.getListViewTitle();}, function (item) {return item.getListViewTitle();},
]; ];
$scope.filter.propertyDict = { $scope.areFiltersSet = function () {
'speakers' : function (speaker) { return ($scope.areVisibilityFiltersSet() ||
return ''; $scope.filter.booleanFilters.closed.value !== $scope.filter.booleanFilters.closed.defaultValue);
}, };
$scope.areVisibilityFiltersSet = function () {
return ($scope.filter.booleanFilters.is_public.value !== $scope.filter.booleanFilters.is_public.defaultValue ||
$scope.filter.booleanFilters.is_internal.value !== $scope.filter.booleanFilters.is_internal.defaultValue ||
$scope.filter.booleanFilters.is_hidden.value !== $scope.filter.booleanFilters.is_hidden.defaultValue);
};
$scope.resetFilters = function (isSelectMode) {
if (!isSelectMode) {
_.forEach($scope.filter.booleanFilters, function (filter) {
filter.value = filter.defaultValue;
});
$scope.filter.save();
}
}; };
// Expand all items during searching. // Expand all items during searching.
@ -333,9 +390,31 @@ angular.module('OpenSlidesApp.agenda.site', [
}); });
} }
}; };
// set type for selected items
$scope.setTypeMultiple = function (type) {
_.forEach($scope.items, function (item) {
if (item.selected) {
item.type = type;
$scope.save(item);
}
});
$scope.isSelectMode = false;
$scope.uncheckAll();
};
// set closed for selected items
$scope.setStateMultiple = function (closed) {
_.forEach($scope.items, function (item) {
if (item.selected) {
item.closed = closed;
$scope.save(item);
}
});
$scope.isSelectMode = false;
$scope.uncheckAll();
};
// delete selected items // delete selected items
$scope.deleteMultiple = function () { $scope.deleteMultiple = function () {
angular.forEach($scope.items, function (item) { _.forEach($scope.items, function (item) {
if (item.selected) { if (item.selected) {
DS.destroy(item.content_object.collection, item.content_object.id); DS.destroy(item.content_object.collection, item.content_object.id);
} }
@ -421,6 +500,20 @@ angular.module('OpenSlidesApp.agenda.site', [
} }
]) ])
// Filter for the item type that filters the selected items by type. filters
// are the boolean filters from the ui.
.filter('itemTypeFilter', [
function () {
return function (items, filters) {
return _.filter(items, function (item) {
return (item.is_public && filters.is_public.value) ||
(item.is_internal && filters.is_internal.value) ||
(item.is_hidden && filters.is_hidden.value);
});
};
}
])
// filter to hide collapsed items. Items has to be a flat tree. // filter to hide collapsed items. Items has to be a flat tree.
.filter('collapsedItemFilter', [ .filter('collapsedItemFilter', [
function () { function () {
@ -789,6 +882,8 @@ angular.module('OpenSlidesApp.agenda.site', [
gettext('Couple countdown with the list of speakers'); gettext('Couple countdown with the list of speakers');
gettext('[Begin speech] starts the countdown, [End speech] stops the ' + gettext('[Begin speech] starts the countdown, [End speech] stops the ' +
'countdown.'); 'countdown.');
gettext('Agenda visibility');
gettext('Default visibility for new agenda items');
} }
]); ]);

View File

@ -10,7 +10,7 @@
<!-- import --> <!-- import -->
<span os-perms="agenda.can_manage"> <span os-perms="agenda.can_manage">
<a ui-sref="topics.topic.import" <a ui-sref="topics.topic.import"
os-perms="agenda.can_see_hidden_items" os-perms="agenda.can_see_internal_items"
class="btn btn-default btn-sm"> class="btn btn-default btn-sm">
<i class="fa fa-download fa-lg"></i> <i class="fa fa-download fa-lg"></i>
<translate>Import</translate> <translate>Import</translate>
@ -137,9 +137,45 @@
</div> </div>
</div> </div>
<div uib-collapse="!isSelectMode" class="row spacer"> <div uib-collapse="!isSelectMode" class="row spacer">
<div class="col-sm-12 text-left"> <div class="col-sm-12 text-left form-inline" os-perms="agenda.can_manage">
<!-- actions -->
<select ng-model="selectedAction" class="form-control input-sm">
<option value="" translate>--- Select action ---</option>
<option value="delete" translate>Delete</option>
<option value="setType" translate>Set visibility</option>
<option value="setState" translate>Set state</option>
</select>
<!-- Type (visibility) -->
<select ng-show="selectedAction == 'setType'" ng-model="selectedType" class="form-control input-sm">
<option value="" translate>--- Select visibility ---</option>
<option value="{{ AGENDA_ITEM }}" translate>
Public
</option>
<option value="{{ INTERNAL_ITEM }}" translate>
Internal
</option>
<option value="{{ HIDDEN_ITEM }}" translate>
Hidden
</option>
</select>
<!-- set type button -->
<a ng-show="selectedAction == 'setType' && selectedType"
ng-click="setTypeMultiple(selectedType)" class="btn btn-default btn-sm">
<translate>Set visibility</translate>
</a>
<!-- set state buttons -->
<a ng-show="selectedAction == 'setState'"
ng-click="setStateMultiple(true)" class="btn btn-default btn-sm">
<i class="fa fa-check-square-o"></i>
<translate>Done</translate>
</a>
<a ng-show="selectedAction == 'setState'"
ng-click="setStateMultiple(false)" class="btn btn-default btn-sm">
<i class="fa fa-square-o"></i>
<translate>Open</translate>
</a>
<!-- delete button --> <!-- delete button -->
<a ng-show="isSelectMode" os-perms="agenda.can_manage" <a ng-if="selectedAction === 'delete'"
ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected agenda items?' | translate }}" ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected agenda items?' | translate }}"
ng-bootbox-confirm-action="deleteMultiple()" ng-bootbox-confirm-action="deleteMultiple()"
class="btn btn-default btn-sm btn-danger"> class="btn btn-default btn-sm btn-danger">
@ -151,10 +187,10 @@
<div class="spacer-top-lg italic row"> <div class="spacer-top-lg italic row">
<div class="col-md-6"> <div class="col-md-6">
<span os-perms="agenda.can_see_hidden_items">{{ itemsFiltered.length }} /</span> <span os-perms="agenda.can_see_internal_items">{{ itemsFiltered.length }} /</span>
{{ items.length }} {{ "items" | translate }}<span ng-if="(items|filter:{selected:true}).length > 0">, {{ (items|filter:{is_hidden:false}).length }} {{ "items" | translate }}<span ng-if="(items|filter:{selected:true}).length > 0">,
{{(items|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(items|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
<span os-perms="agenda.can_see_hidden_items" class="optional"> <span os-perms="agenda.can_see_internal_items" class="optional">
<span ng-if="sumDurations() > 0">&middot; <span ng-if="sumDurations() > 0">&middot;
<translate>Duration</translate>: <translate>Duration</translate>:
{{ sumDurations() | osMinutesToTime }}h {{ sumDurations() | osMinutesToTime }}h
@ -196,50 +232,67 @@
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right"> <span class="form-inline text-right pull-right">
<!-- clear all filters --> <!-- clear all filters -->
<span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)" <span class="sort-spacer pointer" ng-click="resetFilters(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode" ng-if="areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<i class="fa fa-window-close"></i> <i class="fa fa-window-close"></i>
<translate>Filter</translate> <translate>Filter</translate>
</span> </span>
<!-- boolean Filters (combined!) --> <!-- boolean Filters -->
<span uib-dropdown> <!-- State -->
<span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle <span uib-dropdown>
ng-class="{'bold': (filter.booleanFilters.closed.value !== undefined) || <span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
(filter.booleanFilters.is_hidden.value !== undefined), ng-class="{'bold': filter.booleanFilters.closed.value !== filter.booleanFilters.closed.defaultValue,
'disabled': isSelectMode}" 'disabled': isSelectMode}"
ng-disabled="isSelectMode"> ng-disabled="isSelectMode">
<translate>Items</translate> <translate>State</translate>
<span class="caret"></span> <span class="caret"></span>
</span> </span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems"> <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
<li> <li>
<a href ng-click="filter.booleanFilters.closed.value = (filter.booleanFilters.closed.value ? undefined : true); filter.save();"> <a href ng-click="filter.booleanFilters.closed.value = (filter.booleanFilters.closed.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.closed.value === true}"></i> <i class="fa" ng-class="{'fa-check': filter.booleanFilters.closed.value === true}"></i>
{{ filter.booleanFilters.closed.choiceYes | translate }} {{ filter.booleanFilters.closed.choiceYes | translate }}
</a> </a>
</li> </li>
<li> <li>
<a href ng-click="filter.booleanFilters.closed.value = (filter.booleanFilters.closed.value === false) ? undefined : false; filter.save();"> <a href ng-click="filter.booleanFilters.closed.value = (filter.booleanFilters.closed.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.closed.value === false}"></i> <i class="fa" ng-class="{'fa-check': filter.booleanFilters.closed.value === false}"></i>
{{ filter.booleanFilters.closed.choiceNo | translate }} {{ filter.booleanFilters.closed.choiceNo | translate }}
</a> </a>
</li> </li>
<li class="divider"></li> </ul>
<li> </span>
<a href ng-click="filter.booleanFilters.is_hidden.value = (filter.booleanFilters.is_hidden.value ? undefined : true); filter.save();"> <!-- Visibility -->
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_hidden.value === true}"></i> <span uib-dropdown>
{{ filter.booleanFilters.is_hidden.choiceYes | translate }} <span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
</a> ng-class="{'bold': areVisibilityFiltersSet(),
</li> 'disabled': isSelectMode}"
<li> ng-disabled="isSelectMode">
<a href ng-click="filter.booleanFilters.is_hidden.value = (filter.booleanFilters.is_hidden.value === false) ? undefined : false; filter.save();"> <translate>Visibility</translate>
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_hidden.value === false}"></i> <span class="caret"></span>
{{ filter.booleanFilters.is_hidden.choiceNo | translate }} </span>
</a> <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
</li> <li>
</ul> <a href ng-click="filter.booleanFilters.is_public.value = !filter.booleanFilters.is_public.value; filter.save();">
</span> <i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_public.value}"></i>
<translate>Public items</translate>
</a>
</li>
<li os-perms="agenda.can_see_internal_items">
<a href ng-click="filter.booleanFilters.is_internal.value = !filter.booleanFilters.is_internal.value; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_internal.value}"></i>
<translate>Internal items</translate>
</a>
</li>
<li os-perms="agenda.can_see_internal_items agenda.can_manage">
<a href ng-click="filter.booleanFilters.is_hidden.value = !filter.booleanFilters.is_hidden.value; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_hidden.value}"></i>
<translate>Hidden items</translate>
</a>
</li>
</ul>
</span>
<!-- search field --> <!-- search field -->
<span class="form-group"> <span class="form-group">
<span class="input-group"> <span class="input-group">
@ -254,9 +307,9 @@
<span> <span>
<!-- for all boolean Filters --> <!-- for all boolean Filters -->
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters" <span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined" ng-hide="booleanFilter.value === booleanFilter.defaultValue"
class="pointer spacer-left-lg" class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined; filter.save();" ng-click="booleanFilter.value = booleanFilter.defaultValue; filter.save();"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<span class="nobr"> <span class="nobr">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
@ -273,9 +326,9 @@
ng-class="{'projected': item.isProjected().length, ng-class="{'projected': item.isProjected().length,
'related-projected': item.isRelatedProjected().length}" 'related-projected': item.isRelatedProjected().length}"
ng-repeat="item in itemsFiltered = (itemsSearched = (items ng-repeat="item in itemsFiltered = (itemsSearched = (items
| osFilter: filter.filterString : filter.getObjectQueryString) | osFilter : filter.filterString : filter.getObjectQueryString)
| filter: {closed: filter.booleanFilters.closed.value} | filter : {closed: filter.booleanFilters.closed.value}
| filter: {is_hidden: filter.booleanFilters.is_hidden.value}) | itemTypeFilter : filter.booleanFilters)
| collapsedItemFilter | collapsedItemFilter
| limitTo : pagination.itemsPerPage : pagination.limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
@ -361,6 +414,63 @@
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}"> <div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}">
<div style="width: 60%;" class="optional"> <div style="width: 60%;" class="optional">
<small> <small>
<!-- type dropdown for managers -->
<div os-perms="agenda.can_manage">
<div ng-mouseover="typeHover=true" ng-mouseleave="typeHover=false">
<span uib-dropdown>
<span id="dropdownType{{ item.id }}" class="pointer"
uib-dropdown-toggle uib-tooltip="{{ 'Change visibility' | translate }}"
tooltip-class="nobr">
<span ng-if="item.is_public && item.hover">
<i class="fa fa-eye"></i>
<translate>Public</translate>
</span>
<span ng-if="item.is_internal">
<i class="fa fa-eye"></i>
<translate>Internal</translate>
</span>
<span ng-if="item.is_hidden">
<i class="fa fa-eye"></i>
<translate>Hidden</translate>
</span>
<i class="fa fa-cog fa-lg spacer-left" ng-show="typeHover"></i>
</span>
<ul class="dropdown-menu" aria-labelledby="dropdownType{{ item.id }}">
<li>
<a href ng-click="item.type = AGENDA_ITEM; save(item);" translate>
Public item
</a>
</li>
<li>
<a href ng-click="item.type = INTERNAL_ITEM; save(item);" translate>
Internal item
</a>
</li>
<li>
<a href ng-click="item.type = HIDDEN_ITEM; save(item);" translate>
Hidden item
</a>
</li>
</ul>
</span>
</div>
</div>
<!-- type for non-managers -->
<div os-perms="!agenda.can_manage">
<span ng-if="item.is_public && item.hover">
<i class="fa fa-eye"></i>
<translate>Public</translate>
</span>
<span ng-if="item.is_internal">
<i class="fa fa-eye"></i>
<translate>Internal</translate>
</span>
<span ng-if="item.is_hidden">
<i class="fa fa-eye"></i>
<translate>Hidden</translate>
</span>
</div>
<!-- Duration -->
<div ng-style="{'visibility': (item.duration || item.hover) ? 'visible' : 'hidden'}"> <div ng-style="{'visibility': (item.duration || item.hover) ? 'visible' : 'hidden'}">
<div class="popover-wrapper" os-perms="agenda.can_manage"> <div class="popover-wrapper" os-perms="agenda.can_manage">
<i class="fa fa-clock-o"></i> <i class="fa fa-clock-o"></i>
@ -374,7 +484,7 @@
</span> </span>
</div> </div>
<div os-perms="!agenda.can_manage"> <div os-perms="!agenda.can_manage">
<div os-perms="agenda.can_see_hidden_items"> <div os-perms="agenda.can_see_internal_items">
<span ng-if="item.duration"> <span ng-if="item.duration">
<i class="fa fa-clock-o"></i> {{ item.duration | osMinutesToTime }} <i class="fa fa-clock-o"></i> {{ item.duration | osMinutesToTime }}
<translate translate-comment="'h' means time in hours">h</translate> <translate translate-comment="'h' means time in hours">h</translate>
@ -382,6 +492,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Comment -->
<div ng-style="{'visibility': (item.comment || item.hover) ? 'visible' : 'hidden'}"> <div ng-style="{'visibility': (item.comment || item.hover) ? 'visible' : 'hidden'}">
<div class="popover-wrapper" os-perms="agenda.can_manage"> <div class="popover-wrapper" os-perms="agenda.can_manage">
<i class="fa fa-info-circle"></i> <i class="fa fa-info-circle"></i>
@ -391,7 +502,8 @@
</span> </span>
</div> </div>
</div> </div>
<div ng-style="{'visibility': ((item.type == 1) && item.hover) ? 'visible' : 'hidden'}" os-perms="agenda.can_manage"> <!-- Number -->
<div ng-style="{'visibility': ((item.is_public) && item.hover) ? 'visible' : 'hidden'}" os-perms="agenda.can_manage">
<div class="popover-wrapper" os-perms="agenda.can_manage"> <div class="popover-wrapper" os-perms="agenda.can_manage">
<i class="fa fa-info-circle"></i> <i class="fa fa-info-circle"></i>
<span editable-text="item.item_number" onaftersave="save(item)"> <span editable-text="item.item_number" onaftersave="save(item)">
@ -403,22 +515,14 @@
<template-hook hook-name="agendaListAdditionalContentColumn"></template-hook> <template-hook hook-name="agendaListAdditionalContentColumn"></template-hook>
</small> </small>
</div> </div>
<div style="width: 40%; overflow: hidden;" class="pull-right"> <div style="width: 40%;" class="pull-right">
<div os-perms="agenda.can_manage"> <div os-perms="agenda.can_manage">
<div class="pointer nobr" ng-click="item.type = (item.type == 1) ? 2 : 1; save(item);" ng-show="item.hover || item.is_hidden">
<i class="fa" ng-class="item.is_hidden ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="spacer-left" translate>Internal</span>
</div>
<div class="pointer nobr" ng-click="item.closed = !item.closed; save(item);" ng-show="item.hover || item.closed"> <div class="pointer nobr" ng-click="item.closed = !item.closed; save(item);" ng-show="item.hover || item.closed">
<i class="fa" ng-class="item.closed ? 'fa-check-square-o' : 'fa-square-o'"></i> <i class="fa" ng-class="item.closed ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="spacer-left" translate>Done</span> <span class="spacer-left" translate>Done</span>
</div> </div>
</div> </div>
<div os-perms="!agenda.can_manage" > <div os-perms="!agenda.can_manage" >
<div ng-show="item.is_hidden">
<i class="fa fa-ban"></i>
<span class="spacer-left" translate>Internal</span>
</div>
<div ng-show="item.closed"> <div ng-show="item.closed">
<i class="fa fa-check-square-o"></i> <i class="fa fa-check-square-o"></i>
<span class="spacer-left" translate>Done</span> <span class="spacer-left" translate>Done</span>

View File

@ -46,7 +46,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
# done in the specific method. See below. # done in the specific method. See below.
elif self.action in ('partial_update', 'update'): elif self.action in ('partial_update', 'update'):
result = (has_perm(self.request.user, 'agenda.can_see') and result = (has_perm(self.request.user, 'agenda.can_see') and
has_perm(self.request.user, 'agenda.can_see_hidden_items') and has_perm(self.request.user, 'agenda.can_see_internal_items') and
has_perm(self.request.user, 'agenda.can_manage')) has_perm(self.request.user, 'agenda.can_manage'))
elif self.action in ('speak', 'sort_speakers'): elif self.action in ('speak', 'sort_speakers'):
result = (has_perm(self.request.user, 'agenda.can_see') and result = (has_perm(self.request.user, 'agenda.can_see') and
@ -62,13 +62,14 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
""" """
Customized view endpoint to update all children if, the item type has changed. Customized view endpoint to update all children if, the item type has changed.
""" """
hidden = self.get_object().type == Item.HIDDEN_ITEM old_type = self.get_object().type
result = super().update(*args, **kwargs) result = super().update(*args, **kwargs)
# update all children, if the item type has changed # update all children, if the item type has changed
item = self.get_object() item = self.get_object()
if hidden != (item.type == Item.HIDDEN_ITEM):
if old_type != item.type:
items_to_update = [] items_to_update = []
# rekursively add children to items_to_update # rekursively add children to items_to_update

View File

@ -200,7 +200,7 @@ class AssignmentFullSerializer(ModelSerializer):
""" """
assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True) assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True)
polls = AssignmentAllPollSerializer(many=True, read_only=True) polls = AssignmentAllPollSerializer(many=True, read_only=True)
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2) agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:

View File

@ -102,7 +102,8 @@ angular.module('OpenSlidesApp.assignments.site', [
'Assignment', 'Assignment',
'Agenda', 'Agenda',
'AgendaTree', 'AgendaTree',
function (gettextCatalog, operator, Editor, Mediafile, Tag, Assignment, Agenda, AgendaTree) { 'ShowAsAgendaItemField',
function (gettextCatalog, operator, Editor, Mediafile, Tag, Assignment, Agenda, AgendaTree, ShowAsAgendaItemField) {
return { return {
// ngDialog for assignment form // ngDialog for assignment form
getDialog: function (assignment) { getDialog: function (assignment) {
@ -159,15 +160,7 @@ angular.module('OpenSlidesApp.assignments.site', [
// show as agenda item + parent item // show as agenda item + parent item
if (isCreateForm) { if (isCreateForm) {
formFields.push({ formFields.push(ShowAsAgendaItemField('assignments.can_manage'));
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated the election appears as internal item on agenda.')
},
hide: !(operator.hasPerms('assignments.can_manage') && operator.hasPerms('agenda.can_manage'))
});
formFields.push({ formFields.push({
key: 'agenda_parent_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
@ -623,17 +616,18 @@ angular.module('OpenSlidesApp.assignments.site', [
'Assignment', 'Assignment',
'AssignmentForm', 'AssignmentForm',
'Agenda', 'Agenda',
'Config',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Assignment, AssignmentForm, Agenda, ErrorMessage) { function($scope, $state, Assignment, AssignmentForm, Agenda, Config, ErrorMessage) {
$scope.model = {}; $scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
// set default value for open posts form field // set default value for open posts form field
$scope.model.open_posts = 1; $scope.model.open_posts = 1;
// get all form fields // get all form fields
$scope.formFields = AssignmentForm.getFormFields(true); $scope.formFields = AssignmentForm.getFormFields(true);
// save assignment // save assignment
$scope.save = function(assignment, gotoDetailView) { $scope.save = function(assignment, gotoDetailView) {
assignment.agenda_type = assignment.showAsAgendaItem ? 1 : 2;
// The attribute assignment.agenda_parent_id is set by the form, see form definition.
Assignment.create(assignment).then( Assignment.create(assignment).then(
function (success) { function (success) {
if (gotoDetailView) { if (gotoDetailView) {

View File

@ -372,7 +372,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
}, },
tocCategoryTitle: { tocCategoryTitle: {
fontSize: 12, fontSize: 12,
margin: [0,0,0,0], margin: [0,0,0,4],
bold: true, bold: true,
}, },
tocCategorySection: { tocCategorySection: {
@ -960,6 +960,9 @@ angular.module('OpenSlidesApp.core.pdf', [])
// not be empty otherwise it will be removed and the empty line is not displayed // not be empty otherwise it will be removed and the empty line is not displayed
if (element.nextSibling && element.nextSibling.nodeName === 'BR') { if (element.nextSibling && element.nextSibling.nodeName === 'BR') {
currentParagraph.text.push(create('text', ' ')); currentParagraph.text.push(create('text', ' '));
} else if (isInsideAList(element) && lineNumberMode === 'none') {
// Put a spacer there, if there is one BR in a list
alreadyConverted.push(create('text', ' '));
} }
currentParagraph.lineHeight = 1.25; currentParagraph.lineHeight = 1.25;
alreadyConverted.push(currentParagraph); alreadyConverted.push(currentParagraph);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -157,6 +157,15 @@ def get_config_variables():
group='Motions', group='Motions',
subgroup='Amendments') subgroup='Amendments')
yield ConfigVariable(
name='motions_amendments_main_table',
default_value=False,
input_type='boolean',
label='Show amendments together with motions',
weight=337,
group='Motions',
subgroup='Amendments')
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_prefix', name='motions_amendments_prefix',
default_value='-', default_value='-',

View File

@ -50,7 +50,7 @@ class MotionBlockSerializer(ModelSerializer):
""" """
Serializer for motion.models.Category objects. Serializer for motion.models.Category objects.
""" """
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2) agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
@ -382,7 +382,7 @@ class MotionSerializer(ModelSerializer):
required=False, required=False,
validators=[validate_workflow_field], validators=[validate_workflow_field],
write_only=True) write_only=True)
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2) agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
submitters = SubmitterSerializer(many=True, read_only=True) submitters = SubmitterSerializer(many=True, read_only=True)

View File

@ -111,6 +111,17 @@
left: 20px; left: 20px;
} }
.motion-text.line-numbers-none li > br {
margin-top: 8px;
content: " ";
display: block;
&.os-line-break {
margin-top: 0;
content: "";
display: inline;
}
}
@mixin addChangeRecommendationBtn { @mixin addChangeRecommendationBtn {
cursor: pointer; cursor: pointer;
content: "\f067"; content: "\f067";

View File

@ -53,7 +53,8 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'gettextCatalog', 'gettextCatalog',
'Agenda', 'Agenda',
'AgendaTree', 'AgendaTree',
function ($http, operator, gettextCatalog, Agenda, AgendaTree) { 'ShowAsAgendaItemField',
function ($http, operator, gettextCatalog, Agenda, AgendaTree, ShowAsAgendaItemField) {
return { return {
// Get ngDialog configuration. // Get ngDialog configuration.
getDialog: function (motionBlock) { getDialog: function (motionBlock) {
@ -82,15 +83,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
// show as agenda item + parent item // show as agenda item + parent item
if (isCreateForm) { if (isCreateForm) {
formFields.push({ formFields.push(ShowAsAgendaItemField('motions.can_manage'));
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
},
hide: !(operator.hasPerms('motions.can_manage') && operator.hasPerms('agenda.can_manage'))
});
formFields.push({ formFields.push({
key: 'agenda_parent_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
@ -197,18 +190,18 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'$scope', '$scope',
'MotionBlock', 'MotionBlock',
'MotionBlockForm', 'MotionBlockForm',
function($scope, MotionBlock, MotionBlockForm) { 'Config',
function($scope, MotionBlock, MotionBlockForm, Config) {
// Prepare form. // Prepare form.
$scope.model = {}; $scope.model = {
$scope.model.showAsAgendaItem = true; agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
// Get all form fields. // Get all form fields.
$scope.formFields = MotionBlockForm.getFormFields(true); $scope.formFields = MotionBlockForm.getFormFields(true);
// Save form. // Save form.
$scope.save = function (motionBlock) { $scope.save = function (motionBlock) {
motionBlock.agenda_type = motionBlock.showAsAgendaItem ? 1 : 2;
// The attribute motionBlock.agenda_parent_id is set by the form, see form definition.
MotionBlock.create(motionBlock).then( MotionBlock.create(motionBlock).then(
function (success) { function (success) {
$scope.closeThisDialog(); $scope.closeThisDialog();

View File

@ -442,8 +442,9 @@ angular.module('OpenSlidesApp.motions.site', [
'Workflow', 'Workflow',
'Agenda', 'Agenda',
'AgendaTree', 'AgendaTree',
function ($filter, gettextCatalog, operator, Editor, MotionComment, Category, 'ShowAsAgendaItemField',
Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) { function ($filter, gettextCatalog, operator, Editor, MotionComment, Category, Config,
Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree, ShowAsAgendaItemField) {
return { return {
// ngDialog for motion form // ngDialog for motion form
// If motion is given and not null, we're editing an already existing motion // If motion is given and not null, we're editing an already existing motion
@ -467,6 +468,10 @@ angular.module('OpenSlidesApp.motions.site', [
}, },
// angular-formly fields for motion form // angular-formly fields for motion form
getFormFields: function (isCreateForm, isParagraphBasedAmendment) { getFormFields: function (isCreateForm, isParagraphBasedAmendment) {
if (!isParagraphBasedAmendment) { // catch null and undefined. Angular formy doesn't like this.
isParagraphBasedAmendment = false;
}
var workflows = Workflow.getAll(); var workflows = Workflow.getAll();
var images = Mediafile.getAllImages(); var images = Mediafile.getAllImages();
var formFields = []; var formFields = [];
@ -540,15 +545,7 @@ angular.module('OpenSlidesApp.motions.site', [
// show as agenda item + parent item // show as agenda item + parent item
if (isCreateForm) { if (isCreateForm) {
formFields.push({ formFields.push(ShowAsAgendaItemField('motions.can_manage'));
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated the motion appears as internal item on agenda.')
},
hide: !(operator.hasPerms('motions.can_manage') && operator.hasPerms('agenda.can_manage'))
});
formFields.push({ formFields.push({
key: 'agenda_parent_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
@ -1215,7 +1212,14 @@ angular.module('OpenSlidesApp.motions.site', [
return Motion.lastModified(); return Motion.lastModified();
}, function () { }, function () {
// get all main motions and order by identifier (after custom ordering) // get all main motions and order by identifier (after custom ordering)
$scope.motions = _.orderBy(Motion.filter({parent_id: undefined}), ['identifier']); var motions;
if (Config.get('motions_amendments_main_table').value) {
motions = Motion.getAll();
} else {
motions = Motion.filter({parent_id: undefined});
}
$scope.motions = _.orderBy(motions, ['identifier']);
_.forEach($scope.motions, function (motion) { _.forEach($scope.motions, function (motion) {
MotionComment.populateFields(motion); MotionComment.populateFields(motion);
motion.personalNote = PersonalNoteManager.getNote(motion); motion.personalNote = PersonalNoteManager.getNote(motion);
@ -2223,7 +2227,10 @@ angular.module('OpenSlidesApp.motions.site', [
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows'); Workflow.bindAll({}, $scope, 'workflows');
$scope.model = {}; $scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
$scope.alert = {}; $scope.alert = {};
// Check whether this is a new amendment. // Check whether this is a new amendment.
@ -2279,8 +2286,6 @@ angular.module('OpenSlidesApp.motions.site', [
// save motion // save motion
$scope.save = function (motion, gotoDetailView) { $scope.save = function (motion, gotoDetailView) {
motion.agenda_type = motion.showAsAgendaItem ? 1 : 2;
if (isAmendment && motion.paragraphNo !== undefined) { if (isAmendment && motion.paragraphNo !== undefined) {
var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false); var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false);
motion.amendment_paragraphs = orig_paragraphs.map(function (_, idx) { motion.amendment_paragraphs = orig_paragraphs.map(function (_, idx) {
@ -3235,6 +3240,7 @@ angular.module('OpenSlidesApp.motions.site', [
// subgroup Amendments // subgroup Amendments
gettext('Amendments'); gettext('Amendments');
gettext('Activate amendments'); gettext('Activate amendments');
gettext('Show amendments together with motions');
gettext('Prefix for the identifier for amendments'); gettext('Prefix for the identifier for amendments');
gettext('Apply text for new amendments'); gettext('Apply text for new amendments');
gettext('The title of the motion is always applied.'); gettext('The title of the motion is always applied.');

View File

@ -31,6 +31,12 @@
<span ng-class="{'hiddenDiv': !selectHover}" uib-dropdown> <span ng-class="{'hiddenDiv': !selectHover}" uib-dropdown>
<i class="fa fa-cog pointer" uib-dropdown-toggle id="selectDropdown"></i> <i class="fa fa-cog pointer" uib-dropdown-toggle id="selectDropdown"></i>
<ul class="dropdown-menu" aria-labelledby="selectDropdown"> <ul class="dropdown-menu" aria-labelledby="selectDropdown">
<li>
<a href ng-click="selectLeadMotion(null)" translate>
All motions
</a>
</li>
<li class="divider"></li>
<li ng-repeat="motion in leadMotions"> <li ng-repeat="motion in leadMotions">
<a href ng-click="selectLeadMotion(motion)"> <a href ng-click="selectLeadMotion(motion)">
<span ng-if="motion.identifier"> <span ng-if="motion.identifier">
@ -39,12 +45,6 @@
{{ motion.getTitle() | limitTo: 35 }}{{ motion.getTitle().length > 35 ? '...' : '' }} {{ motion.getTitle() | limitTo: 35 }}{{ motion.getTitle().length > 35 ? '...' : '' }}
</a> </a>
</li> </li>
<li class="divider" ng-if="amendment.state.getNextStates().length"></li>
<li>
<a href ng-click="selectLeadMotion(null)" translate>
All motions
</a>
</li>
</ul> </ul>
</span> </span>
</h3> </h3>
@ -357,10 +357,11 @@
{{ getTextPreview(amendment.getText(), 400) }} {{ getTextPreview(amendment.getText(), 400) }}
</div> </div>
</div> </div>
<!-- last column -->
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(33.33% - 120px)' : 'calc(33.33% - 70px)'}"> <div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(33.33% - 120px)' : 'calc(33.33% - 70px)'}">
<div style="width: 90%;"> <div style="width: 90%;">
<div ng-repeat="(id, field) in noSpecialCommentsFields"> <div ng-repeat="(id, field) in noSpecialCommentsFields">
<div class="nobr"> <div class="nobr" style="overflow: hidden;">
<i class="fa pointer spacer-right" ng-class="field[amendment.id] ? 'fa-caret-down' : 'fa-caret-right'" <i class="fa pointer spacer-right" ng-class="field[amendment.id] ? 'fa-caret-down' : 'fa-caret-right'"
ng-click="field[amendment.id] = !field[amendment.id]" ng-click="field[amendment.id] = !field[amendment.id]"
ng-if="isTextExpandable(amendment.comments[id], 30)"></i> ng-if="isTextExpandable(amendment.comments[id], 30)"></i>

View File

@ -79,12 +79,7 @@
<span class="change-title" ng-if="motion.isAllowed('update') && !title_change_recommendation"></span> <span class="change-title" ng-if="motion.isAllowed('update') && !title_change_recommendation"></span>
</span> </span>
<a ui-sref="motions.motion.detail({id: motion.getParentMotion().id })" ng-if="motion.isAmendment"> {{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
</a>
<span ng-if="!motion.isAmendment">
{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
</span>
<i class="fa pointer" ng-class="motion.personalNote.star ? 'fa-star' : 'fa-star-o'" <i class="fa pointer" ng-class="motion.personalNote.star ? 'fa-star' : 'fa-star-o'"
ng-if="operator.user" ng-if="operator.user"
@ -102,6 +97,14 @@
</span> </span>
<small> <small>
<translate>Sequential number</translate> {{ motion.getSequentialNumber() }} <translate>Sequential number</translate> {{ motion.getSequentialNumber() }}
<span ng-if="motion.isAmendment">
&middot;
<a ui-sref="motions.motion.detail({id: motion.getParentMotion().id })" ng-if="motion.isAmendment">
<translate>Amendment to</translate>
{{ motion.getParentMotion().identifier || motion.getParentMotion().getTitle() }}
</a>
</span>
</small> </small>
</h2> </h2>
</div> </div>
@ -552,9 +555,6 @@
</translate> </translate>
</div> </div>
<div ng-bind-html="motion.getTextByMode('agreed', version, highlight) | trusted"
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
<div style="text-align: right;" ng-if="(change_recommendations | filter:{motion_version_id:version}:true).length > 0"> <div style="text-align: right;" ng-if="(change_recommendations | filter:{motion_version_id:version}:true).length > 0">
<button class="btn btn-default" <button class="btn btn-default"
ng-bootbox-confirm="{{ 'Do you want to copy the final version to the modified final version field?' | translate }}" ng-bootbox-confirm="{{ 'Do you want to copy the final version to the modified final version field?' | translate }}"
@ -572,6 +572,9 @@
<translate>New version on these changes</translate> <translate>New version on these changes</translate>
</button> </button>
</div> </div>
<div ng-bind-html="motion.getTextByMode('agreed', version, highlight) | trusted"
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
</div> </div>
<!-- modified agreed view --> <!-- modified agreed view -->

View File

@ -5,7 +5,8 @@
<i class="fa fa-plus fa-lg"></i> <i class="fa fa-plus fa-lg"></i>
<translate>New</translate> <translate>New</translate>
</a> </a>
<a ui-sref="motions.motion.allamendments" class="btn btn-default btn-sm"> <a ui-sref="motions.motion.allamendments" ng-if="config('motions_amendments_enabled')"
class="btn btn-default btn-sm">
<i class="fa fa-book fa-lg"></i> <i class="fa fa-book fa-lg"></i>
<translate>Amendments</translate> <translate>Amendments</translate>
</a> </a>

View File

@ -8,7 +8,7 @@ class TopicSerializer(ModelSerializer):
""" """
Serializer for core.models.Topic objects. Serializer for core.models.Topic objects.
""" """
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2) agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
agenda_comment = CharField(write_only=True, required=False, allow_blank=True) agenda_comment = CharField(write_only=True, required=False, allow_blank=True)
agenda_duration = IntegerField(write_only=True, required=False, min_value=1) agenda_duration = IntegerField(write_only=True, required=False, min_value=1)

View File

@ -68,7 +68,9 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'Mediafile', 'Mediafile',
'Agenda', 'Agenda',
'AgendaTree', 'AgendaTree',
function ($filter, gettextCatalog, operator, Editor, Mediafile, Agenda, AgendaTree) { 'ShowAsAgendaItemField',
function ($filter, gettextCatalog, operator, Editor, Mediafile, Agenda,
AgendaTree, ShowAsAgendaItemField) {
return { return {
// ngDialog for topic form // ngDialog for topic form
getDialog: function (topic) { getDialog: function (topic) {
@ -120,15 +122,7 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
// show as agenda item + parent item // show as agenda item + parent item
if (isCreateForm) { if (isCreateForm) {
formFields.push({ formFields.push(ShowAsAgendaItemField('agenda.can_manage'));
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
},
hide: !operator.hasPerms('agenda.can_manage')
});
formFields.push({ formFields.push({
key: 'agenda_parent_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
@ -187,17 +181,16 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'Topic', 'Topic',
'TopicForm', 'TopicForm',
'Agenda', 'Agenda',
'Config',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Topic, TopicForm, Agenda, ErrorMessage) { function($scope, $state, Topic, TopicForm, Agenda, Config, ErrorMessage) {
$scope.topic = {}; $scope.model = {
$scope.model = {}; agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
$scope.model.showAsAgendaItem = true; };
// get all form fields // get all form fields
$scope.formFields = TopicForm.getFormFields(true); $scope.formFields = TopicForm.getFormFields(true);
// save form // save form
$scope.save = function (topic) { $scope.save = function (topic) {
topic.agenda_type = topic.showAsAgendaItem ? 1 : 2;
// The attribute topic.agenda_parent_id is set by the form, see form definition.
Topic.create(topic).then( Topic.create(topic).then(
function (success) { function (success) {
$scope.closeThisDialog(); $scope.closeThisDialog();

View File

@ -33,7 +33,7 @@ def create_builtin_groups_and_admin(**kwargs):
'agenda.can_manage', 'agenda.can_manage',
'agenda.can_manage_list_of_speakers', 'agenda.can_manage_list_of_speakers',
'agenda.can_see', 'agenda.can_see',
'agenda.can_see_hidden_items', 'agenda.can_see_internal_items',
'assignments.can_manage', 'assignments.can_manage',
'assignments.can_nominate_other', 'assignments.can_nominate_other',
'assignments.can_nominate_self', 'assignments.can_nominate_self',
@ -74,7 +74,7 @@ def create_builtin_groups_and_admin(**kwargs):
# Default (pk 1) # Default (pk 1)
base_permissions = ( base_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'], permission_dict['agenda.can_see_internal_items'],
permission_dict['assignments.can_see'], permission_dict['assignments.can_see'],
permission_dict['core.can_see_frontpage'], permission_dict['core.can_see_frontpage'],
permission_dict['core.can_see_projector'], permission_dict['core.can_see_projector'],
@ -87,7 +87,7 @@ def create_builtin_groups_and_admin(**kwargs):
# Delegates (pk 2) # Delegates (pk 2)
delegates_permissions = ( delegates_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'], permission_dict['agenda.can_see_internal_items'],
permission_dict['agenda.can_be_speaker'], permission_dict['agenda.can_be_speaker'],
permission_dict['assignments.can_see'], permission_dict['assignments.can_see'],
permission_dict['assignments.can_nominate_other'], permission_dict['assignments.can_nominate_other'],
@ -105,7 +105,7 @@ def create_builtin_groups_and_admin(**kwargs):
# Staff (pk 3) # Staff (pk 3)
staff_permissions = ( staff_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'], permission_dict['agenda.can_see_internal_items'],
permission_dict['agenda.can_be_speaker'], permission_dict['agenda.can_be_speaker'],
permission_dict['agenda.can_manage'], permission_dict['agenda.can_manage'],
permission_dict['agenda.can_manage_list_of_speakers'], permission_dict['agenda.can_manage_list_of_speakers'],
@ -136,7 +136,7 @@ def create_builtin_groups_and_admin(**kwargs):
# Admin (pk 4) # Admin (pk 4)
admin_permissions = ( admin_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'], permission_dict['agenda.can_see_internal_items'],
permission_dict['agenda.can_be_speaker'], permission_dict['agenda.can_be_speaker'],
permission_dict['agenda.can_manage'], permission_dict['agenda.can_manage'],
permission_dict['agenda.can_manage_list_of_speakers'], permission_dict['agenda.can_manage_list_of_speakers'],
@ -178,7 +178,7 @@ def create_builtin_groups_and_admin(**kwargs):
# Committees (pk 5) # Committees (pk 5)
committees_permissions = ( committees_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_hidden_items'], permission_dict['agenda.can_see_internal_items'],
permission_dict['assignments.can_see'], permission_dict['assignments.can_see'],
permission_dict['core.can_see_frontpage'], permission_dict['core.can_see_frontpage'],
permission_dict['core.can_see_projector'], permission_dict['core.can_see_projector'],

View File

@ -79,6 +79,15 @@ DATABASES = {
use_redis = False use_redis = False
if use_redis: if use_redis:
# Redis configuration for django-redis-session. Keep this synchronized to
# the caching settings
SESSION_REDIS = {
'host': '127.0.0.1',
'post': 6379,
'db': 0,
}
# Django Channels # Django Channels
# Unless you have only a small assembly uncomment the following lines to # Unless you have only a small assembly uncomment the following lines to

View File

@ -4,6 +4,6 @@
# Requirements for Redis and PostgreSQL support # Requirements for Redis and PostgreSQL support
asgi-redis>=1.3,<1.5 asgi-redis>=1.3,<1.5
django-redis>=4.7.0,<4.10 django-redis>=4.7.0,<4.10
django-redis-sessions>=0.5.6,<0.7 django-redis-sessions>=0.6.1,<0.7
psycopg2-binary>=2.7,<2.8 psycopg2-binary>=2.7,<2.8
txredisapi==1.4.4 txredisapi==1.4.4

View File

@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django_redis import get_redis_connection from django_redis import get_redis_connection
@ -25,9 +26,9 @@ class RetrieveItem(TestCase):
config['general_system_enable_anonymous'] = True config['general_system_enable_anonymous'] = True
self.item = Topic.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item self.item = Topic.objects.create(title='test_title_Idais2pheepeiz5uph1c').agenda_item
def test_normal_by_anonymous_without_perm_to_see_hidden_items(self): def test_normal_by_anonymous_without_perm_to_see_internal_items(self):
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users. group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users.
permission_string = 'agenda.can_see_hidden_items' permission_string = 'agenda.can_see_internal_items'
app_label, codename = permission_string.split('.') app_label, codename = permission_string.split('.')
permission = group.permissions.get(content_type__app_label=app_label, codename=codename) permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission) group.permissions.remove(permission)
@ -36,12 +37,27 @@ class RetrieveItem(TestCase):
response = self.client.get(reverse('item-detail', args=[self.item.pk])) response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_hidden_by_anonymous_without_perm_to_see_hidden_items(self): def test_hidden_by_anonymous_without_manage_perms(self):
response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_hidden_by_anonymous_with_manage_perms(self):
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users. group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users.
permission_string = 'agenda.can_see_hidden_items' permission_string = 'agenda.can_manage'
app_label, codename = permission_string.split('.')
permission = Permission.objects.get(content_type__app_label=app_label, codename=codename)
group.permissions.add(permission)
response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_internal_by_anonymous_without_perm_to_see_internal_items(self):
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous users.
permission_string = 'agenda.can_see_internal_items'
app_label, codename = permission_string.split('.') app_label, codename = permission_string.split('.')
permission = group.permissions.get(content_type__app_label=app_label, codename=codename) permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission) group.permissions.remove(permission)
self.item.type = Item.INTERNAL_ITEM
self.item.save()
response = self.client.get(reverse('item-detail', args=[self.item.pk])) response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(sorted(response.data.keys()), sorted(( self.assertEqual(sorted(response.data.keys()), sorted((
@ -56,6 +72,7 @@ class RetrieveItem(TestCase):
'comment', 'comment',
'closed', 'closed',
'type', 'type',
'is_internal',
'is_hidden', 'is_hidden',
'duration', 'duration',
'weight', 'weight',
@ -101,13 +118,15 @@ class TestDBQueries(TestCase):
* 1 request to get all speakers, * 1 request to get all speakers,
* 3 requests to get the assignments, motions and topics and * 3 requests to get the assignments, motions and topics and
* 1 request to get an agenda item (why?)
* 2 requests for the motionsversions. * 2 requests for the motionsversions.
TODO: The last two request for the motionsversions are a bug. TODO: The last two request for the motionsversions are a bug.
""" """
self.client.force_login(User.objects.get(pk=1)) self.client.force_login(User.objects.get(pk=1))
get_redis_connection("default").flushall() get_redis_connection("default").flushall()
with self.assertNumQueries(14): with self.assertNumQueries(15):
self.client.get(reverse('item-list')) self.client.get(reverse('item-list'))
def test_anonymous(self): def test_anonymous(self):
@ -118,12 +137,14 @@ class TestDBQueries(TestCase):
* 1 request to get all speakers, * 1 request to get all speakers,
* 3 requests to get the assignments, motions and topics and * 3 requests to get the assignments, motions and topics and
* 1 request to get an agenda item (why?)
* 2 requests for the motionsversions. * 2 requests for the motionsversions.
TODO: The last two request for the motionsversions are a bug. TODO: The last two request for the motionsversions are a bug.
""" """
get_redis_connection("default").flushall() get_redis_connection("default").flushall()
with self.assertNumQueries(10): with self.assertNumQueries(11):
self.client.get(reverse('item-list')) self.client.get(reverse('item-list'))
@ -410,8 +431,8 @@ class Numbering(TestCase):
self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, 'II.1') self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, 'II.1')
self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, 'III') self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, 'III')
def test_with_hidden_item(self): def test_with_internal_item(self):
self.item_2.type = Item.HIDDEN_ITEM self.item_2.type = Item.INTERNAL_ITEM
self.item_2.save() self.item_2.save()
response = self.client.post(reverse('item-numbering')) response = self.client.post(reverse('item-numbering'))
@ -422,9 +443,9 @@ class Numbering(TestCase):
self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '') self.assertEqual(Item.objects.get(pk=self.item_2_1.pk).item_number, '')
self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '2') self.assertEqual(Item.objects.get(pk=self.item_3.pk).item_number, '2')
def test_reset_numbering_with_hidden_item(self): def test_reset_numbering_with_internal_item(self):
self.item_2.item_number = 'test_number_Cieghae6ied5ool4hiem' self.item_2.item_number = 'test_number_Cieghae6ied5ool4hiem'
self.item_2.type = Item.HIDDEN_ITEM self.item_2.type = Item.INTERNAL_ITEM
self.item_2.save() self.item_2.save()
self.item_2_1.item_number = 'test_number_roQueTohg7fe1Is7aemu' self.item_2_1.item_number = 'test_number_roQueTohg7fe1Is7aemu'
self.item_2_1.save() self.item_2_1.save()

View File

@ -512,7 +512,7 @@ class GroupUpdate(TestCase):
'agenda.can_be_speaker', 'agenda.can_be_speaker',
'agenda.can_manage', 'agenda.can_manage',
'agenda.can_see', 'agenda.can_see',
'agenda.can_see_hidden_items', 'agenda.can_see_internal_items',
'assignments.can_manage', 'assignments.can_manage',
'assignments.can_nominate_other', 'assignments.can_nominate_other',
'assignments.can_nominate_self', 'assignments.can_nominate_self',