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',