From 1a17862d6b5a3cbd7b942345c23cea2d8d3304e5 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Wed, 15 Aug 2018 11:15:54 +0200 Subject: [PATCH] New item type internal. The old hidden type was used as internal, so everything is changed to not be shown if the item is internal. hidden is "new", and actually behaves as hidden now. --- CHANGELOG.rst | 6 +- openslides/agenda/access_permissions.py | 44 ++-- openslides/agenda/config_variables.py | 13 + .../migrations/0005_auto_20180815_1109.py | 53 ++++ openslides/agenda/models.py | 75 +++--- openslides/agenda/serializers.py | 1 + openslides/agenda/signals.py | 4 +- openslides/agenda/static/js/agenda/base.js | 5 + openslides/agenda/static/js/agenda/pdf.js | 16 +- .../agenda/static/js/agenda/projector.js | 14 +- openslides/agenda/static/js/agenda/site.js | 113 ++++++++- .../static/templates/agenda/item-list.html | 226 +++++++++++++----- openslides/agenda/views.py | 7 +- openslides/assignments/serializers.py | 2 +- .../assignments/static/js/assignments/site.js | 22 +- openslides/motions/serializers.py | 4 +- .../motions/static/js/motions/motion-block.js | 23 +- openslides/motions/static/js/motions/site.js | 22 +- openslides/topics/serializers.py | 2 +- openslides/topics/static/js/topics/site.js | 25 +- openslides/users/signals.py | 12 +- tests/integration/agenda/test_viewset.py | 41 +++- tests/integration/users/test_viewset.py | 2 +- 23 files changed, 501 insertions(+), 231 deletions(-) create mode 100644 openslides/agenda/migrations/0005_auto_20180815_1109.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 55973fc71..c8910764e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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]. @@ -20,8 +23,9 @@ Motions: 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) diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py index 83cfb6906..89a47a3b7 100644 --- a/openslides/agenda/access_permissions.py +++ b/openslides/agenda/access_permissions.py @@ -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 = [] diff --git a/openslides/agenda/config_variables.py b/openslides/agenda/config_variables.py index 5483242c4..e47cf9c78 100644 --- a/openslides/agenda/config_variables.py +++ b/openslides/agenda/config_variables.py @@ -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( diff --git a/openslides/agenda/migrations/0005_auto_20180815_1109.py b/openslides/agenda/migrations/0005_auto_20180815_1109.py new file mode 100644 index 000000000..8438d24c4 --- /dev/null +++ b/openslides/agenda/migrations/0005_auto_20180815_1109.py @@ -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), + ] diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 8f252bb8c..fc403a116 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -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 diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index 259a3489f..13dfebc63 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -49,6 +49,7 @@ class ItemSerializer(ModelSerializer): 'comment', 'closed', 'type', + 'is_internal', 'is_hidden', 'duration', 'speakers', diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py index f9a8187fc..1b3e2e787 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -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 diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index bcf52ae1a..d2ef7c990 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -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; diff --git a/openslides/agenda/static/js/agenda/pdf.js b/openslides/agenda/static/js/agenda/pdf.js index 2f435b8db..47532d835 100644 --- a/openslides/agenda/static/js/agenda/pdf.js +++ b/openslides/agenda/static/js/agenda/pdf.js @@ -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 }, { diff --git a/openslides/agenda/static/js/agenda/projector.js b/openslides/agenda/static/js/agenda/projector.js index a63ac64c1..2c4176145 100644 --- a/openslides/agenda/static/js/agenda/projector.js +++ b/openslides/agenda/static/js/agenda/projector.js @@ -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); } diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js index 5f1341ccd..b5d41755d 100644 --- a/openslides/agenda/static/js/agenda/site.js +++ b/openslides/agenda/static/js/agenda/site.js @@ -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'); } ]); diff --git a/openslides/agenda/static/templates/agenda/item-list.html b/openslides/agenda/static/templates/agenda/item-list.html index ba4be73ed..6e60c2fd7 100644 --- a/openslides/agenda/static/templates/agenda/item-list.html +++ b/openslides/agenda/static/templates/agenda/item-list.html @@ -10,7 +10,7 @@ Import @@ -137,9 +137,43 @@
-
+
+ + + + + + + Set visibility + + + + Set closed + + + Set not closed + - @@ -151,10 +185,10 @@
- {{ itemsFiltered.length }} / - {{ items.length }} {{ "items" | translate }}, + {{ itemsFiltered.length }} / + {{ (items|filter:{is_hidden:false}).length }} {{ "items" | translate }}, {{(items|filter:{selected:true}).length}} {{ "selected" | translate }} - + · Duration: {{ sumDurations() | osMinutesToTime }}h @@ -196,50 +230,67 @@
- Filter - - - - Items - - - - + + + + + State + + + + + + + + Visibility + + + + @@ -254,9 +305,9 @@ @@ -273,9 +324,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 +412,63 @@
+ +
+
+ + + + + Public + + + + Internal + + + + Hidden + + + + + +
+
+ +
+ + + Public + + + + Internal + + + + Hidden + +
+
@@ -374,7 +482,7 @@
-
+
{{ item.duration | osMinutesToTime }} h @@ -382,6 +490,7 @@
+
@@ -391,7 +500,8 @@
-
+ +
@@ -403,22 +513,14 @@
-
+
-
- - Internal -
Done
-
- - Internal -
Done diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index da031151c..b699b1b22 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -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 diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index 66c46af43..845ca4e1c 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -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: diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js index 4efd5ca40..7ab0a8511 100644 --- a/openslides/assignments/static/js/assignments/site.js +++ b/openslides/assignments/static/js/assignments/site.js @@ -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) { diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index e226501c0..6f2791bc2 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -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) diff --git a/openslides/motions/static/js/motions/motion-block.js b/openslides/motions/static/js/motions/motion-block.js index 2a00aadc6..eb71e1f82 100644 --- a/openslides/motions/static/js/motions/motion-block.js +++ b/openslides/motions/static/js/motions/motion-block.js @@ -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(); diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index f35171f5e..abd3f46ec 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -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 @@ -540,15 +541,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', @@ -2223,7 +2216,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 +2275,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) { diff --git a/openslides/topics/serializers.py b/openslides/topics/serializers.py index 7529ee80e..6845598c1 100644 --- a/openslides/topics/serializers.py +++ b/openslides/topics/serializers.py @@ -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) diff --git a/openslides/topics/static/js/topics/site.js b/openslides/topics/static/js/topics/site.js index cc50f5d30..5c70fb084 100644 --- a/openslides/topics/static/js/topics/site.js +++ b/openslides/topics/static/js/topics/site.js @@ -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(); diff --git a/openslides/users/signals.py b/openslides/users/signals.py index 7f5b5a531..a5f72d561 100644 --- a/openslides/users/signals.py +++ b/openslides/users/signals.py @@ -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'], diff --git a/tests/integration/agenda/test_viewset.py b/tests/integration/agenda/test_viewset.py index b037ef823..0e4792309 100644 --- a/tests/integration/agenda/test_viewset.py +++ b/tests/integration/agenda/test_viewset.py @@ -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() diff --git a/tests/integration/users/test_viewset.py b/tests/integration/users/test_viewset.py index b694fd3d4..3d151786e 100644 --- a/tests/integration/users/test_viewset.py +++ b/tests/integration/users/test_viewset.py @@ -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',