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)
========================
Agenda:
- New item type 'hidden'. New visibilty filter in agenda [#3790].
Motions:
- New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647].
@ -17,11 +20,13 @@ Motions:
- New table of contents with page numbers and categories in PDF [#3766].
- New teporal field "modified final version" where the final version can
be edited [#3781].
- New config to show amendments also in motions table [#3792]
Core:
- 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 Django to 2.1 [#3777, #3786].
Version 2.2 (2018-06-06)

View File

@ -23,7 +23,8 @@ class ItemAccessPermissions(BaseAccessPermissions):
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(
self,
@ -33,8 +34,10 @@ class ItemAccessPermissions(BaseAccessPermissions):
Returns the restricted serialized data for the instance prepared
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
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):
"""
@ -45,38 +48,45 @@ class ItemAccessPermissions(BaseAccessPermissions):
# Parse data.
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.
data = full_data
elif has_perm(user, 'agenda.can_see_hidden_items'):
# Non managers with special permission can see everything but comments.
elif has_perm(user, 'agenda.can_see_internal_items'):
# 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',)
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:
# 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
# so that list of speakers is provided regardless.
blocked_keys_hidden_case = set(full_data[0].keys()) - set((
# In internal and hidden case managers and non managers see only some fields
# so that list of speakers is provided regardless. Hidden items can only be seen by managers.
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set((
'id',
'title',
'speakers',
'speaker_list_closed',
'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.
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:
blocked_keys_non_hidden_case = ('comment',)
blocked_keys_non_internal_hidden_case = ('comment',)
can_see_hidden = False
data = []
for full in full_data:
if full['is_hidden']:
data.append(filtered_data(full, blocked_keys_hidden_case))
else:
data.append(filtered_data(full, blocked_keys_non_hidden_case))
if full['is_hidden'] and can_see_hidden:
# Same filtering for internal and hidden items
data.append(filtered_data(full, blocked_keys_internal_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:
data = []

View File

@ -59,6 +59,19 @@ def get_config_variables():
group='Agenda',
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
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')
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
root_items, item_children = self.get_root_and_children(only_agenda_items=True)
# 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_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.
"""
for item in items:
yield item
yield from yield_items(item_children[item.pk])
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
if parent_is_not_public or item.type in (item.INTERNAL_ITEM, item.HIDDEN_ITEM):
item_is_not_public = True
yield item
else:
item_is_hidden = False
yield from yield_items(item_children[item.pk], parent_is_hidden=item_is_hidden)
item_is_not_public = False
yield from yield_items(
item_children[item.pk],
parent_is_not_public=item_is_not_public)
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
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
HIDDEN_ITEM and all of their children.
If only_item_type is given, the tree hides items with other types and
all of their children.
"""
queryset = self.order_by('weight')
item_children = defaultdict(list) # type: Dict[int, List[Item]]
root_items = []
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
if item.parent_id is not None:
item_children[item.parent_id].append(item)
@ -92,19 +77,19 @@ class ItemManager(models.Manager):
root_items.append(item)
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
and children, where id is the id of one agenda item and children is a
generator that yields dictonaries like the one discribed.
If only_agenda_items is True, the tree hides items with type
HIDDEN_ITEM and all of their children.
If only_item_type is given, the tree hides items with other types and
all of their children.
If include_content is True, the yielded dictonaries have no key 'id'
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):
"""
@ -184,10 +169,10 @@ class ItemManager(models.Manager):
walk_tree(tree_element['children'], item_number)
# 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.
for item in self.get_only_hidden_items():
for item in self.get_only_non_public_items():
item.item_number = ''
item.save()
@ -200,10 +185,12 @@ class Item(RESTModelMixin, models.Model):
objects = ItemManager()
AGENDA_ITEM = 1
HIDDEN_ITEM = 2
INTERNAL_ITEM = 2
HIDDEN_ITEM = 3
ITEM_TYPE = (
(AGENDA_ITEM, ugettext_lazy('Agenda item')),
(INTERNAL_ITEM, ugettext_lazy('Internal item')),
(HIDDEN_ITEM, ugettext_lazy('Hidden item')))
item_number = models.CharField(blank=True, max_length=255)
@ -281,7 +268,7 @@ class Item(RESTModelMixin, models.Model):
('can_see', 'Can see agenda'),
('can_manage', 'Can manage agenda'),
('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')
def __str__(self):
@ -320,6 +307,16 @@ class Item(RESTModelMixin, models.Model):
raise NotImplementedError('You have to provide a get_agenda_list_view_title '
'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):
"""
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',
'closed',
'type',
'is_internal',
'is_hidden',
'duration',
'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):
"""
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')
for permission in permissions:
# There could be only one 'agenda.can_see' and then we want to return data.
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()
break

View File

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

View File

@ -17,10 +17,9 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
// generate the item list with all subitems
var createItemList = function() {
var agenda_items = [];
angular.forEach(items, function (item) {
if (item.is_hidden === false) {
var itemIndent = item.parentCount * 20;
_.forEach(items, function (item) {
if (item.is_public) {
var itemIndent = item.parentCount * 15;
var itemStyle;
if (item.parentCount === 0) {
@ -29,13 +28,6 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
itemStyle = 'listChild';
}
var itemNumberWidth;
if (item.item_number === "") {
itemNumberWidth = 0;
} else {
itemNumberWidth = 60;
}
var agendaJsonString = {
style: itemStyle,
columns: [
@ -44,7 +36,7 @@ angular.module('OpenSlidesApp.agenda.pdf', ['OpenSlidesApp.core.pdf'])
text: ''
},
{
width: itemNumberWidth,
width: 60,
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');
}, function () {
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) {
items = _.filter(Agenda.getAll(), function (item) {
return item.type === 1;
items = _.filter(items, function (item) {
return item.is_public;
});
} else {
items = Agenda.getAll();
}
var tree = AgendaTree.getTree(items);
@ -115,7 +117,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
});
} else if ($scope.element.tree) {
items = _.filter(Agenda.getAll(), function (item) {
return item.type === 1;
return item.is_public;
});
$scope.tree = AgendaTree.getTree(items);
} else {
@ -124,7 +126,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
orderBy: 'weight'
});
items = _.filter(items, function (item) {
return item.type === 1;
return item.is_public;
});
$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', [
'$scope',
'$filter',
@ -109,6 +133,11 @@ angular.module('OpenSlidesApp.agenda.site', [
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm,
AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter,
osTablePagination, AgendaCsvExport, AgendaPdfExport, AgendaDocxExport, ErrorMessage) {
$scope.AGENDA_ITEM = 1;
$scope.INTERNAL_ITEM = 2;
$scope.HIDDEN_ITEM = 3;
// Bind agenda tree to the scope
$scope.$watch(function () {
return Agenda.lastModified();
@ -143,16 +172,31 @@ angular.module('OpenSlidesApp.agenda.site', [
$scope.filter.booleanFilters = {
closed: {
value: undefined,
defaultValue: undefined,
displayName: gettext('Closed items'),
choiceYes: gettext('Closed items'),
choiceNo: gettext('Open items'),
},
is_hidden: {
value: undefined,
displayName: gettext('Internal items'),
// The next filters are just on-off, so no undefined there
is_public: {
value: true,
defaultValue: true,
choiceYes: gettext('Public items'),
choiceNo: gettext('No public items'),
},
is_internal: {
value: true,
defaultValue: true,
choiceYes: gettext('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 = [
function (item) {return item.getListViewTitle();},
];
$scope.filter.propertyDict = {
'speakers' : function (speaker) {
return '';
},
$scope.areFiltersSet = function () {
return ($scope.areVisibilityFiltersSet() ||
$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.
@ -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
$scope.deleteMultiple = function () {
angular.forEach($scope.items, function (item) {
_.forEach($scope.items, function (item) {
if (item.selected) {
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('collapsedItemFilter', [
function () {
@ -789,6 +882,8 @@ angular.module('OpenSlidesApp.agenda.site', [
gettext('Couple countdown with the list of speakers');
gettext('[Begin speech] starts the countdown, [End speech] stops the ' +
'countdown.');
gettext('Agenda visibility');
gettext('Default visibility for new agenda items');
}
]);

View File

@ -10,7 +10,7 @@
<!-- import -->
<span os-perms="agenda.can_manage">
<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">
<i class="fa fa-download fa-lg"></i>
<translate>Import</translate>
@ -137,9 +137,45 @@
</div>
</div>
<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 -->
<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-action="deleteMultiple()"
class="btn btn-default btn-sm btn-danger">
@ -151,10 +187,10 @@
<div class="spacer-top-lg italic row">
<div class="col-md-6">
<span os-perms="agenda.can_see_hidden_items">{{ itemsFiltered.length }} /</span>
{{ items.length }} {{ "items" | translate }}<span ng-if="(items|filter:{selected:true}).length > 0">,
<span os-perms="agenda.can_see_internal_items">{{ itemsFiltered.length }} /</span>
{{ (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>
<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;
<translate>Duration</translate>:
{{ sumDurations() | osMinutesToTime }}h
@ -196,50 +232,67 @@
<div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right">
<!-- clear all filters -->
<span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
<span class="sort-spacer pointer" ng-click="resetFilters(isSelectMode)"
ng-if="areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-window-close"></i>
<translate>Filter</translate>
</span>
<!-- boolean Filters (combined!) -->
<span uib-dropdown>
<span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
ng-class="{'bold': (filter.booleanFilters.closed.value !== undefined) ||
(filter.booleanFilters.is_hidden.value !== undefined),
'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Items</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
<li>
<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>
{{ filter.booleanFilters.closed.choiceYes | translate }}
</a>
</li>
<li>
<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>
{{ filter.booleanFilters.closed.choiceNo | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.booleanFilters.is_hidden.value = (filter.booleanFilters.is_hidden.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_hidden.value === true}"></i>
{{ filter.booleanFilters.is_hidden.choiceYes | translate }}
</a>
</li>
<li>
<a href ng-click="filter.booleanFilters.is_hidden.value = (filter.booleanFilters.is_hidden.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.is_hidden.value === false}"></i>
{{ filter.booleanFilters.is_hidden.choiceNo | translate }}
</a>
</li>
</ul>
</span>
<!-- boolean Filters -->
<!-- State -->
<span uib-dropdown>
<span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
ng-class="{'bold': filter.booleanFilters.closed.value !== filter.booleanFilters.closed.defaultValue,
'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>State</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
<li>
<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>
{{ filter.booleanFilters.closed.choiceYes | translate }}
</a>
</li>
<li>
<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>
{{ filter.booleanFilters.closed.choiceNo | translate }}
</a>
</li>
</ul>
</span>
<!-- Visibility -->
<span uib-dropdown>
<span class="sort-spacer pointer" id="dropdownItems" uib-dropdown-toggle
ng-class="{'bold': areVisibilityFiltersSet(),
'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Visibility</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownItems">
<li>
<a href ng-click="filter.booleanFilters.is_public.value = !filter.booleanFilters.is_public.value; filter.save();">
<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 -->
<span class="form-group">
<span class="input-group">
@ -254,9 +307,9 @@
<span>
<!-- for all boolean Filters -->
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined"
ng-hide="booleanFilter.value === booleanFilter.defaultValue"
class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined; filter.save();"
ng-click="booleanFilter.value = booleanFilter.defaultValue; filter.save();"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
@ -273,9 +326,9 @@
ng-class="{'projected': item.isProjected().length,
'related-projected': item.isRelatedProjected().length}"
ng-repeat="item in itemsFiltered = (itemsSearched = (items
| osFilter: filter.filterString : filter.getObjectQueryString)
| filter: {closed: filter.booleanFilters.closed.value}
| filter: {is_hidden: filter.booleanFilters.is_hidden.value})
| osFilter : filter.filterString : filter.getObjectQueryString)
| filter : {closed: filter.booleanFilters.closed.value}
| itemTypeFilter : filter.booleanFilters)
| collapsedItemFilter
| 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 style="width: 60%;" class="optional">
<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 class="popover-wrapper" os-perms="agenda.can_manage">
<i class="fa fa-clock-o"></i>
@ -374,7 +484,7 @@
</span>
</div>
<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">
<i class="fa fa-clock-o"></i> {{ item.duration | osMinutesToTime }}
<translate translate-comment="'h' means time in hours">h</translate>
@ -382,6 +492,7 @@
</div>
</div>
</div>
<!-- Comment -->
<div ng-style="{'visibility': (item.comment || item.hover) ? 'visible' : 'hidden'}">
<div class="popover-wrapper" os-perms="agenda.can_manage">
<i class="fa fa-info-circle"></i>
@ -391,7 +502,8 @@
</span>
</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">
<i class="fa fa-info-circle"></i>
<span editable-text="item.item_number" onaftersave="save(item)">
@ -403,22 +515,14 @@
<template-hook hook-name="agendaListAdditionalContentColumn"></template-hook>
</small>
</div>
<div style="width: 40%; overflow: hidden;" class="pull-right">
<div style="width: 40%;" class="pull-right">
<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">
<i class="fa" ng-class="item.closed ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span class="spacer-left" translate>Done</span>
</div>
</div>
<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">
<i class="fa fa-check-square-o"></i>
<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.
elif self.action in ('partial_update', 'update'):
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'))
elif self.action in ('speak', 'sort_speakers'):
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.
"""
hidden = self.get_object().type == Item.HIDDEN_ITEM
old_type = self.get_object().type
result = super().update(*args, **kwargs)
# update all children, if the item type has changed
item = self.get_object()
if hidden != (item.type == Item.HIDDEN_ITEM):
if old_type != item.type:
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)
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)
class Meta:

View File

@ -102,7 +102,8 @@ angular.module('OpenSlidesApp.assignments.site', [
'Assignment',
'Agenda',
'AgendaTree',
function (gettextCatalog, operator, Editor, Mediafile, Tag, Assignment, Agenda, AgendaTree) {
'ShowAsAgendaItemField',
function (gettextCatalog, operator, Editor, Mediafile, Tag, Assignment, Agenda, AgendaTree, ShowAsAgendaItemField) {
return {
// ngDialog for assignment form
getDialog: function (assignment) {
@ -159,15 +160,7 @@ angular.module('OpenSlidesApp.assignments.site', [
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
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(ShowAsAgendaItemField('assignments.can_manage'));
formFields.push({
key: 'agenda_parent_id',
type: 'select-single',
@ -623,17 +616,18 @@ angular.module('OpenSlidesApp.assignments.site', [
'Assignment',
'AssignmentForm',
'Agenda',
'Config',
'ErrorMessage',
function($scope, $state, Assignment, AssignmentForm, Agenda, ErrorMessage) {
$scope.model = {};
function($scope, $state, Assignment, AssignmentForm, Agenda, Config, ErrorMessage) {
$scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
// set default value for open posts form field
$scope.model.open_posts = 1;
// get all form fields
$scope.formFields = AssignmentForm.getFormFields(true);
// save assignment
$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(
function (success) {
if (gotoDetailView) {

View File

@ -372,7 +372,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
},
tocCategoryTitle: {
fontSize: 12,
margin: [0,0,0,0],
margin: [0,0,0,4],
bold: true,
},
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
if (element.nextSibling && element.nextSibling.nodeName === 'BR') {
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;
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',
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(
name='motions_amendments_prefix',
default_value='-',

View File

@ -50,7 +50,7 @@ class MotionBlockSerializer(ModelSerializer):
"""
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)
class Meta:
@ -382,7 +382,7 @@ class MotionSerializer(ModelSerializer):
required=False,
validators=[validate_workflow_field],
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)
submitters = SubmitterSerializer(many=True, read_only=True)

View File

@ -111,6 +111,17 @@
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 {
cursor: pointer;
content: "\f067";

View File

@ -53,7 +53,8 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'gettextCatalog',
'Agenda',
'AgendaTree',
function ($http, operator, gettextCatalog, Agenda, AgendaTree) {
'ShowAsAgendaItemField',
function ($http, operator, gettextCatalog, Agenda, AgendaTree, ShowAsAgendaItemField) {
return {
// Get ngDialog configuration.
getDialog: function (motionBlock) {
@ -82,15 +83,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
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(ShowAsAgendaItemField('motions.can_manage'));
formFields.push({
key: 'agenda_parent_id',
type: 'select-single',
@ -197,18 +190,18 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'$scope',
'MotionBlock',
'MotionBlockForm',
function($scope, MotionBlock, MotionBlockForm) {
'Config',
function($scope, MotionBlock, MotionBlockForm, Config) {
// Prepare form.
$scope.model = {};
$scope.model.showAsAgendaItem = true;
$scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
// Get all form fields.
$scope.formFields = MotionBlockForm.getFormFields(true);
// Save form.
$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(
function (success) {
$scope.closeThisDialog();

View File

@ -442,8 +442,9 @@ angular.module('OpenSlidesApp.motions.site', [
'Workflow',
'Agenda',
'AgendaTree',
function ($filter, gettextCatalog, operator, Editor, MotionComment, Category,
Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) {
'ShowAsAgendaItemField',
function ($filter, gettextCatalog, operator, Editor, MotionComment, Category, Config,
Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree, ShowAsAgendaItemField) {
return {
// ngDialog for motion form
// 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
getFormFields: function (isCreateForm, isParagraphBasedAmendment) {
if (!isParagraphBasedAmendment) { // catch null and undefined. Angular formy doesn't like this.
isParagraphBasedAmendment = false;
}
var workflows = Workflow.getAll();
var images = Mediafile.getAllImages();
var formFields = [];
@ -540,15 +545,7 @@ angular.module('OpenSlidesApp.motions.site', [
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
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(ShowAsAgendaItemField('motions.can_manage'));
formFields.push({
key: 'agenda_parent_id',
type: 'select-single',
@ -1215,7 +1212,14 @@ angular.module('OpenSlidesApp.motions.site', [
return Motion.lastModified();
}, function () {
// 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) {
MotionComment.populateFields(motion);
motion.personalNote = PersonalNoteManager.getNote(motion);
@ -2223,7 +2227,10 @@ angular.module('OpenSlidesApp.motions.site', [
User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows');
$scope.model = {};
$scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
$scope.alert = {};
// Check whether this is a new amendment.
@ -2279,8 +2286,6 @@ angular.module('OpenSlidesApp.motions.site', [
// save motion
$scope.save = function (motion, gotoDetailView) {
motion.agenda_type = motion.showAsAgendaItem ? 1 : 2;
if (isAmendment && motion.paragraphNo !== undefined) {
var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false);
motion.amendment_paragraphs = orig_paragraphs.map(function (_, idx) {
@ -3235,6 +3240,7 @@ angular.module('OpenSlidesApp.motions.site', [
// subgroup Amendments
gettext('Amendments');
gettext('Activate amendments');
gettext('Show amendments together with motions');
gettext('Prefix for the identifier for amendments');
gettext('Apply text for new amendments');
gettext('The title of the motion is always applied.');

View File

@ -31,6 +31,12 @@
<span ng-class="{'hiddenDiv': !selectHover}" uib-dropdown>
<i class="fa fa-cog pointer" uib-dropdown-toggle id="selectDropdown"></i>
<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">
<a href ng-click="selectLeadMotion(motion)">
<span ng-if="motion.identifier">
@ -39,12 +45,6 @@
{{ motion.getTitle() | limitTo: 35 }}{{ motion.getTitle().length > 35 ? '...' : '' }}
</a>
</li>
<li class="divider" ng-if="amendment.state.getNextStates().length"></li>
<li>
<a href ng-click="selectLeadMotion(null)" translate>
All motions
</a>
</li>
</ul>
</span>
</h3>
@ -357,10 +357,11 @@
{{ getTextPreview(amendment.getText(), 400) }}
</div>
</div>
<!-- last column -->
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(33.33% - 120px)' : 'calc(33.33% - 70px)'}">
<div style="width: 90%;">
<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'"
ng-click="field[amendment.id] = !field[amendment.id]"
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>
<a ui-sref="motions.motion.detail({id: motion.getParentMotion().id })" ng-if="motion.isAmendment">
{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
</a>
<span ng-if="!motion.isAmendment">
{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
</span>
{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
<i class="fa pointer" ng-class="motion.personalNote.star ? 'fa-star' : 'fa-star-o'"
ng-if="operator.user"
@ -102,6 +97,14 @@
</span>
<small>
<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>
</h2>
</div>
@ -552,9 +555,6 @@
</translate>
</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">
<button class="btn btn-default"
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>
</button>
</div>
<div ng-bind-html="motion.getTextByMode('agreed', version, highlight) | trusted"
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
</div>
<!-- modified agreed view -->

View File

@ -5,7 +5,8 @@
<i class="fa fa-plus fa-lg"></i>
<translate>New</translate>
</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>
<translate>Amendments</translate>
</a>

View File

@ -8,7 +8,7 @@ class TopicSerializer(ModelSerializer):
"""
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_comment = CharField(write_only=True, required=False, allow_blank=True)
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',
'Agenda',
'AgendaTree',
function ($filter, gettextCatalog, operator, Editor, Mediafile, Agenda, AgendaTree) {
'ShowAsAgendaItemField',
function ($filter, gettextCatalog, operator, Editor, Mediafile, Agenda,
AgendaTree, ShowAsAgendaItemField) {
return {
// ngDialog for topic form
getDialog: function (topic) {
@ -120,15 +122,7 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
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(ShowAsAgendaItemField('agenda.can_manage'));
formFields.push({
key: 'agenda_parent_id',
type: 'select-single',
@ -187,17 +181,16 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'Topic',
'TopicForm',
'Agenda',
'Config',
'ErrorMessage',
function($scope, $state, Topic, TopicForm, Agenda, ErrorMessage) {
$scope.topic = {};
$scope.model = {};
$scope.model.showAsAgendaItem = true;
function($scope, $state, Topic, TopicForm, Agenda, Config, ErrorMessage) {
$scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value),
};
// get all form fields
$scope.formFields = TopicForm.getFormFields(true);
// save form
$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(
function (success) {
$scope.closeThisDialog();

View File

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

View File

@ -79,6 +79,15 @@ DATABASES = {
use_redis = False
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
# Unless you have only a small assembly uncomment the following lines to

View File

@ -4,6 +4,6 @@
# Requirements for Redis and PostgreSQL support
asgi-redis>=1.3,<1.5
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
txredisapi==1.4.4

View File

@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.urls import reverse
from django.utils.translation import ugettext
from django_redis import get_redis_connection
@ -25,9 +26,9 @@ class RetrieveItem(TestCase):
config['general_system_enable_anonymous'] = True
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.
permission_string = 'agenda.can_see_hidden_items'
permission_string = 'agenda.can_see_internal_items'
app_label, codename = permission_string.split('.')
permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission)
@ -36,12 +37,27 @@ class RetrieveItem(TestCase):
response = self.client.get(reverse('item-detail', args=[self.item.pk]))
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.
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('.')
permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission)
self.item.type = Item.INTERNAL_ITEM
self.item.save()
response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(sorted(response.data.keys()), sorted((
@ -56,6 +72,7 @@ class RetrieveItem(TestCase):
'comment',
'closed',
'type',
'is_internal',
'is_hidden',
'duration',
'weight',
@ -101,13 +118,15 @@ class TestDBQueries(TestCase):
* 1 request to get all speakers,
* 3 requests to get the assignments, motions and topics and
* 1 request to get an agenda item (why?)
* 2 requests for the motionsversions.
TODO: The last two request for the motionsversions are a bug.
"""
self.client.force_login(User.objects.get(pk=1))
get_redis_connection("default").flushall()
with self.assertNumQueries(14):
with self.assertNumQueries(15):
self.client.get(reverse('item-list'))
def test_anonymous(self):
@ -118,12 +137,14 @@ class TestDBQueries(TestCase):
* 1 request to get all speakers,
* 3 requests to get the assignments, motions and topics and
* 1 request to get an agenda item (why?)
* 2 requests for the motionsversions.
TODO: The last two request for the motionsversions are a bug.
"""
get_redis_connection("default").flushall()
with self.assertNumQueries(10):
with self.assertNumQueries(11):
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_3.pk).item_number, 'III')
def test_with_hidden_item(self):
self.item_2.type = Item.HIDDEN_ITEM
def test_with_internal_item(self):
self.item_2.type = Item.INTERNAL_ITEM
self.item_2.save()
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_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.type = Item.HIDDEN_ITEM
self.item_2.type = Item.INTERNAL_ITEM
self.item_2.save()
self.item_2_1.item_number = 'test_number_roQueTohg7fe1Is7aemu'
self.item_2_1.save()

View File

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