Merge pull request #3341 from normanjaeckel/BetterAgenda

Supported server side setup of new agenda items (type and parent).
This commit is contained in:
Emanuel Schütze 2018-01-29 10:38:50 +01:00 committed by GitHub
commit ec2fcbdd73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 213 additions and 184 deletions

View File

@ -12,6 +12,7 @@ Agenda:
- Fixed wrong sorting of last speakers [#3193]. - Fixed wrong sorting of last speakers [#3193].
- Fixed issue when sorting a new inserted speaker [#3210]. - Fixed issue when sorting a new inserted speaker [#3210].
- New permission for managing lists of speakers [#3366]. - New permission for managing lists of speakers [#3366].
- Fixed multiple request on creation of agenda related items [#3341].
Motions: Motions:
- New export dialog [#3185]. - New export dialog [#3185].

View File

@ -14,16 +14,24 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
Receiver function to create agenda items. It is connected to the signal Receiver function to create agenda items. It is connected to the signal
django.db.models.signals.post_save during app loading. django.db.models.signals.post_save during app loading.
Do not run caching and autoupdate if the instance as an attribute The agenda_item_update_information container may have fields like type,
skip_autoupdate (regardless of its truthy or falsy conent). parent_id, comment, duration, weight or skip_autoupdate.
Do not run caching and autoupdate if the instance has a key
skip_autoupdate in the agenda_item_update_information container.
""" """
if hasattr(instance, 'get_agenda_title'): if hasattr(instance, 'get_agenda_title'):
if created: if created:
attrs = {}
for attr in ('type', 'parent_id', 'comment', 'duration', 'weight'):
if instance.agenda_item_update_information.get(attr):
attrs[attr] = instance.agenda_item_update_information.get(attr)
Item.objects.create(content_object=instance, **attrs)
# If the object is created, the related_object has to be sent again. # If the object is created, the related_object has to be sent again.
Item.objects.create(content_object=instance) if not instance.agenda_item_update_information.get('skip_autoupdate'):
if not hasattr(instance, 'skip_autoupdate'):
inform_changed_data(instance) inform_changed_data(instance)
elif not hasattr(instance, 'skip_autoupdate'): elif not instance.agenda_item_update_information.get('skip_autoupdate'):
# If the object has changed, then also the agenda item has to be sent. # If the object has changed, then also the agenda item has to be sent.
inform_changed_data(instance.agenda_item) inform_changed_data(instance.agenda_item)

View File

@ -21,32 +21,6 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
} }
]) ])
.factory('AgendaUpdate',[
'Agenda',
'operator',
function(Agenda, operator) {
return {
saveChanges: function (item_id, changes) {
// change agenda item only if user has the permission to do that
if (operator.hasPerms('agenda.can_manage agenda.can_see_hidden_items')) {
Agenda.find(item_id).then(function (item) {
var something = false;
_.each(changes, function(change) {
if (change.value !== item[change.key]) {
item[change.key] = change.value;
something = true;
}
});
if (something === true) {
Agenda.save(item);
}
});
}
}
};
}
])
.factory('Agenda', [ .factory('Agenda', [
'$http', '$http',
'DS', 'DS',

View File

@ -330,6 +330,11 @@ class Assignment(RESTModelMixin, models.Model):
vote_results_dict[candidate].append(votes) vote_results_dict[candidate].append(votes)
return vote_results_dict return vote_results_dict
"""
Container for runtime information for agenda app (on create or update of this instance).
"""
agenda_item_update_information = {} # type: Dict[str, Any]
def get_agenda_title(self): def get_agenda_title(self):
return str(self) return str(self)

View File

@ -194,6 +194,8 @@ class AssignmentFullSerializer(ModelSerializer):
""" """
assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True) assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True)
polls = AssignmentAllPollSerializer(many=True, read_only=True) polls = AssignmentAllPollSerializer(many=True, read_only=True)
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
model = Assignment model = Assignment
@ -207,6 +209,8 @@ class AssignmentFullSerializer(ModelSerializer):
'poll_description_default', 'poll_description_default',
'polls', 'polls',
'agenda_item_id', 'agenda_item_id',
'agenda_type',
'agenda_parent_id',
'tags',) 'tags',)
validators = (posts_validator,) validators = (posts_validator,)
@ -215,6 +219,19 @@ class AssignmentFullSerializer(ModelSerializer):
data['description'] = validate_html(data['description']) data['description'] = validate_html(data['description'])
return data return data
def create(self, validated_data):
"""
Customized create method. Set information about related agenda item
into agenda_item_update_information container.
"""
agenda_type = validated_data.pop('agenda_type', None)
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
assignment = Assignment(**validated_data)
assignment.agenda_item_update_information['type'] = agenda_type
assignment.agenda_item_update_information['parent_id'] = agenda_parent_id
assignment.save()
return assignment
class AssignmentShortSerializer(AssignmentFullSerializer): class AssignmentShortSerializer(AssignmentFullSerializer):
""" """

View File

@ -155,8 +155,11 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Default comment on the ballot paper') label: gettextCatalog.getString('Default comment on the ballot paper')
} }
}, }];
{
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
key: 'showAsAgendaItem', key: 'showAsAgendaItem',
type: 'checkbox', type: 'checkbox',
templateOptions: { templateOptions: {
@ -164,12 +167,9 @@ angular.module('OpenSlidesApp.assignments.site', [
description: gettextCatalog.getString('If deactivated the election appears as internal item on agenda.') description: gettextCatalog.getString('If deactivated the election appears as internal item on agenda.')
}, },
hide: !(operator.hasPerms('assignments.can_manage') && operator.hasPerms('agenda.can_manage')) hide: !(operator.hasPerms('assignments.can_manage') && operator.hasPerms('agenda.can_manage'))
}]; });
// parent item
if (isCreateForm) {
formFields.push({ formFields.push({
key: 'agenda_parent_item_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Parent item'), label: gettextCatalog.getString('Parent item'),
@ -627,9 +627,8 @@ angular.module('OpenSlidesApp.assignments.site', [
'Assignment', 'Assignment',
'AssignmentForm', 'AssignmentForm',
'Agenda', 'Agenda',
'AgendaUpdate',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Assignment, AssignmentForm, Agenda, AgendaUpdate, ErrorMessage) { function($scope, $state, Assignment, AssignmentForm, Agenda, ErrorMessage) {
$scope.model = {}; $scope.model = {};
// set default value for open posts form field // set default value for open posts form field
$scope.model.open_posts = 1; $scope.model.open_posts = 1;
@ -637,13 +636,10 @@ angular.module('OpenSlidesApp.assignments.site', [
$scope.formFields = AssignmentForm.getFormFields(true); $scope.formFields = AssignmentForm.getFormFields(true);
// save assignment // save assignment
$scope.save = function(assignment, gotoDetailView) { $scope.save = function(assignment, gotoDetailView) {
assignment.agenda_type = assignment.showAsAgendaItem ? 1 : 2;
// The attribute assignment.agenda_parent_id is set by the form, see form definition.
Assignment.create(assignment).then( Assignment.create(assignment).then(
function (success) { function (success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (assignment.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: assignment.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
if (gotoDetailView) { if (gotoDetailView) {
$state.go('assignments.assignment.detail', {id: success.id}); $state.go('assignments.assignment.detail', {id: success.id});
} }
@ -663,10 +659,9 @@ angular.module('OpenSlidesApp.assignments.site', [
'Assignment', 'Assignment',
'AssignmentForm', 'AssignmentForm',
'Agenda', 'Agenda',
'AgendaUpdate',
'assignmentId', 'assignmentId',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Assignment, AssignmentForm, Agenda, AgendaUpdate, assignmentId, ErrorMessage) { function($scope, $state, Assignment, AssignmentForm, Agenda, assignmentId, ErrorMessage) {
var assignment = Assignment.get(assignmentId); var assignment = Assignment.get(assignmentId);
$scope.alert = {}; $scope.alert = {};
// set initial values for form model by create deep copy of assignment object // set initial values for form model by create deep copy of assignment object
@ -674,26 +669,14 @@ angular.module('OpenSlidesApp.assignments.site', [
$scope.model = angular.copy(assignment); $scope.model = angular.copy(assignment);
// get all form fields // get all form fields
$scope.formFields = AssignmentForm.getFormFields(); $scope.formFields = AssignmentForm.getFormFields();
var agenda_item = Agenda.get(assignment.agenda_item_id);
for (var i = 0; i < $scope.formFields.length; i++) {
if ($scope.formFields[i].key == "showAsAgendaItem") {
// get state from agenda item (hidden/internal or agenda item)
$scope.formFields[i].defaultValue = !assignment.agenda_item.is_hidden;
} else if($scope.formFields[i].key == 'agenda_parent_item_id') {
$scope.formFields[i].defaultValue = agenda_item.parent_id;
}
}
// save assignment // save assignment
$scope.save = function (assignment, gotoDetailView) { $scope.save = function (assignment, gotoDetailView) {
// inject the changed assignment (copy) object back into DS store // inject the changed assignment (copy) object back into DS store
Assignment.inject(assignment); Assignment.inject(assignment);
// save change assignment object on server // save changed assignment object on server
Assignment.save(assignment).then( Assignment.save(assignment).then(
function(success) { function(success) {
var changes = [{key: 'type', value: (assignment.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: assignment.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
if (gotoDetailView) { if (gotoDetailView) {
$state.go('assignments.assignment.detail', {id: success.id}); $state.go('assignments.assignment.detail', {id: success.id});
} }

View File

@ -1,3 +1,5 @@
from typing import Any, Dict # noqa
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.exceptions import ImproperlyConfigured, ValidationError
@ -628,6 +630,11 @@ class Motion(RESTModelMixin, models.Model):
if self.recommendation is not None: if self.recommendation is not None:
self.set_state(self.recommendation) self.set_state(self.recommendation)
"""
Container for runtime information for agenda app (on create or update of this instance).
"""
agenda_item_update_information = {} # type: Dict[str, Any]
def get_agenda_title(self): def get_agenda_title(self):
""" """
Return a simple title string for the agenda. Return a simple title string for the agenda.
@ -895,6 +902,11 @@ class MotionBlock(RESTModelMixin, models.Model):
id=self.pk) id=self.pk)
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
"""
Container for runtime information for agenda app (on create or update of this instance).
"""
agenda_item_update_information = {} # type: Dict[str, Any]
@property @property
def agenda_item(self): def agenda_item(self):
""" """

View File

@ -49,9 +49,25 @@ class MotionBlockSerializer(ModelSerializer):
""" """
Serializer for motion.models.Category objects. Serializer for motion.models.Category objects.
""" """
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
model = MotionBlock model = MotionBlock
fields = ('id', 'title', 'agenda_item_id',) fields = ('id', 'title', 'agenda_item_id', 'agenda_type', 'agenda_parent_id',)
def create(self, validated_data):
"""
Customized create method. Set information about related agenda item
into agenda_item_update_information container.
"""
agenda_type = validated_data.pop('agenda_type', None)
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
motion_block = MotionBlock(**validated_data)
motion_block.agenda_item_update_information['type'] = agenda_type
motion_block.agenda_item_update_information['parent_id'] = agenda_parent_id
motion_block.save()
return motion_block
class StateSerializer(ModelSerializer): class StateSerializer(ModelSerializer):
@ -288,6 +304,8 @@ class MotionSerializer(ModelSerializer):
required=False, required=False,
validators=[validate_workflow_field], validators=[validate_workflow_field],
write_only=True) write_only=True)
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
model = Motion model = Motion
@ -314,6 +332,8 @@ class MotionSerializer(ModelSerializer):
'attachments', 'attachments',
'polls', 'polls',
'agenda_item_id', 'agenda_item_id',
'agenda_type',
'agenda_parent_id',
'log_messages',) 'log_messages',)
read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above. read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above.
@ -332,6 +352,9 @@ class MotionSerializer(ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
""" """
Customized method to create a new motion from some data. Customized method to create a new motion from some data.
Set also information about related agenda item into
agenda_item_update_information container.
""" """
motion = Motion() motion = Motion()
motion.title = validated_data['title'] motion.title = validated_data['title']
@ -344,6 +367,8 @@ class MotionSerializer(ModelSerializer):
motion.comments = validated_data.get('comments') motion.comments = validated_data.get('comments')
motion.parent = validated_data.get('parent') motion.parent = validated_data.get('parent')
motion.reset_state(validated_data.get('workflow_id')) motion.reset_state(validated_data.get('workflow_id'))
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type')
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')
motion.save() motion.save()
if validated_data.get('submitters'): if validated_data.get('submitters'):
motion.submitters.add(*validated_data['submitters']) motion.submitters.add(*validated_data['submitters'])

View File

@ -69,8 +69,8 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
}; };
}, },
// Get angular-formly fields. // Get angular-formly fields.
getFormFields: function () { getFormFields: function (isCreateForm) {
return [ var formFields = [
{ {
key: 'title', key: 'title',
type: 'input', type: 'input',
@ -78,7 +78,11 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
label: gettextCatalog.getString('Title') label: gettextCatalog.getString('Title')
} }
}, },
{ ];
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
key: 'showAsAgendaItem', key: 'showAsAgendaItem',
type: 'checkbox', type: 'checkbox',
templateOptions: { templateOptions: {
@ -86,18 +90,21 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.') description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
}, },
hide: !(operator.hasPerms('motions.can_manage') && operator.hasPerms('agenda.can_manage')) hide: !(operator.hasPerms('motions.can_manage') && operator.hasPerms('agenda.can_manage'))
}, });
{ formFields.push({
key: 'agenda_parent_item_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Parent item'), label: gettextCatalog.getString('Parent item'),
options: AgendaTree.getFlatTree(Agenda.getAll()), options: AgendaTree.getFlatTree(Agenda.getAll()),
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id', ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
placeholder: gettextCatalog.getString('Select a parent item ...') placeholder: gettextCatalog.getString('Select a parent item ...')
},
hide: !operator.hasPerms('agenda.can_manage')
});
} }
}
]; return formFields;
} }
}; };
} }
@ -190,24 +197,20 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'$scope', '$scope',
'MotionBlock', 'MotionBlock',
'MotionBlockForm', 'MotionBlockForm',
'AgendaUpdate', function($scope, MotionBlock, MotionBlockForm) {
function($scope, MotionBlock, MotionBlockForm, AgendaUpdate) {
// Prepare form. // Prepare form.
$scope.model = {}; $scope.model = {};
$scope.model.showAsAgendaItem = true; $scope.model.showAsAgendaItem = true;
// Get all form fields. // Get all form fields.
$scope.formFields = MotionBlockForm.getFormFields(); $scope.formFields = MotionBlockForm.getFormFields(true);
// Save form. // Save form.
$scope.save = function (motionBlock) { $scope.save = function (motionBlock) {
motionBlock.agenda_type = motionBlock.showAsAgendaItem ? 1 : 2;
// The attribute motionBlock.agenda_parent_id is set by the form, see form definition.
MotionBlock.create(motionBlock).then( MotionBlock.create(motionBlock).then(
function (success) { function (success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (motionBlock.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: motionBlock.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id, changes);
$scope.closeThisDialog(); $scope.closeThisDialog();
}, },
function (error) { function (error) {
@ -227,11 +230,8 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'$state', '$state',
'MotionBlock', 'MotionBlock',
'MotionBlockForm', 'MotionBlockForm',
'AgendaUpdate',
'motionBlockId', 'motionBlockId',
function($scope, $state, MotionBlock, MotionBlockForm, AgendaUpdate, motionBlockId) { function($scope, $state, MotionBlock, MotionBlockForm, motionBlockId) {
// TODO: Check #2486 and remove some agenda related code.
//MotionBlock.loadRelations(motionBlock, 'agenda_item');
$scope.alert = {}; $scope.alert = {};
// Prepare form. Set initial values by creating a deep copy of // Prepare form. Set initial values by creating a deep copy of
@ -241,23 +241,14 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
// Get all form fields. // Get all form fields.
$scope.formFields = MotionBlockForm.getFormFields(); $scope.formFields = MotionBlockForm.getFormFields();
for (var i = 0; i < $scope.formFields.length; i++) {
if ($scope.formFields[i].key == 'showAsAgendaItem') {
// Get state from agenda item (hidden/internal or agenda item).
$scope.formFields[i].defaultValue = !motionBlock.agenda_item.is_hidden;
} else if ($scope.formFields[i].key == 'agenda_parent_item_id') {
$scope.formFields[i].defaultValue = motionBlock.agenda_item.parent_id;
}
}
// Save form. // Save form.
$scope.save = function (motionBlock) { $scope.save = function (motionBlock) {
// inject the changed motionBlock (copy) object back into DS store
MotionBlock.inject(motionBlock);
// save changed motionBlock object on server
MotionBlock.create(motionBlock).then( MotionBlock.create(motionBlock).then(
function (success) { function (success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (motionBlock.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: motionBlock.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
$scope.closeThisDialog(); $scope.closeThisDialog();
}, },
function (error) { function (error) {

View File

@ -376,8 +376,11 @@ angular.module('OpenSlidesApp.motions.site', [
description: gettextCatalog.getString("Don't create a new version.") description: gettextCatalog.getString("Don't create a new version.")
}, },
hide: true hide: true
}, }];
{
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
key: 'showAsAgendaItem', key: 'showAsAgendaItem',
type: 'checkbox', type: 'checkbox',
templateOptions: { templateOptions: {
@ -385,12 +388,9 @@ angular.module('OpenSlidesApp.motions.site', [
description: gettextCatalog.getString('If deactivated the motion appears as internal item on agenda.') description: gettextCatalog.getString('If deactivated the motion appears as internal item on agenda.')
}, },
hide: !(operator.hasPerms('motions.can_manage') && operator.hasPerms('agenda.can_manage')) hide: !(operator.hasPerms('motions.can_manage') && operator.hasPerms('agenda.can_manage'))
}]; });
// parent item
if (isCreateForm) {
formFields.push({ formFields.push({
key: 'agenda_parent_item_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Parent item'), label: gettextCatalog.getString('Parent item'),
@ -1890,10 +1890,9 @@ angular.module('OpenSlidesApp.motions.site', [
'User', 'User',
'Workflow', 'Workflow',
'Agenda', 'Agenda',
'AgendaUpdate',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm, function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm,
Category, Config, Mediafile, Tag, User, Workflow, Agenda, AgendaUpdate, ErrorMessage) { Category, Config, Mediafile, Tag, User, Workflow, Agenda, ErrorMessage) {
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
Tag.bindAll({}, $scope, 'tags'); Tag.bindAll({}, $scope, 'tags');
@ -1927,13 +1926,10 @@ angular.module('OpenSlidesApp.motions.site', [
// save motion // save motion
$scope.save = function (motion, gotoDetailView) { $scope.save = function (motion, gotoDetailView) {
motion.agenda_type = motion.showAsAgendaItem ? 1 : 2;
// The attribute motion.agenda_parent_id is set by the form, see form definition.
Motion.create(motion).then( Motion.create(motion).then(
function(success) { function(success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (motion.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: motion.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id, changes);
if (isAmendment || gotoDetailView) { if (isAmendment || gotoDetailView) {
$state.go('motions.motion.detail', {id: success.id}); $state.go('motions.motion.detail', {id: success.id});
} }
@ -1959,13 +1955,12 @@ angular.module('OpenSlidesApp.motions.site', [
'User', 'User',
'Workflow', 'Workflow',
'Agenda', 'Agenda',
'AgendaUpdate',
'motionId', 'motionId',
'operator', 'operator',
'ErrorMessage', 'ErrorMessage',
'EditingWarning', 'EditingWarning',
function($scope, $state, Motion, Category, Config, Mediafile, MotionForm, function($scope, $state, Motion, Category, Config, Mediafile, MotionForm,
Tag, User, Workflow, Agenda, AgendaUpdate, motionId, operator, ErrorMessage, Tag, User, Workflow, Agenda, motionId, operator, ErrorMessage,
EditingWarning) { EditingWarning) {
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
@ -2007,18 +2002,10 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.formFields[i].hide = false; $scope.formFields[i].hide = false;
} }
} }
if ($scope.formFields[i].key == "showAsAgendaItem" && motion.agenda_item) {
// get state from agenda item (hidden/internal or agenda item)
$scope.formFields[i].defaultValue = !motion.agenda_item.is_hidden;
}
if ($scope.formFields[i].key == "workflow_id") { if ($scope.formFields[i].key == "workflow_id") {
// get saved workflow id from state // get saved workflow id from state
$scope.formFields[i].defaultValue = motion.state.workflow_id; $scope.formFields[i].defaultValue = motion.state.workflow_id;
} }
if ($scope.formFields[i].key == "agenda_parent_item_id") {
// get current parent_id of the agenda item
$scope.formFields[i].defaultValue = motion.agenda_item.parent_id;
}
} }
// Displaying a warning, if other users edit this motion too // Displaying a warning, if other users edit this motion too
@ -2029,14 +2016,9 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.save = function (motion, gotoDetailView) { $scope.save = function (motion, gotoDetailView) {
// inject the changed motion (copy) object back into DS store // inject the changed motion (copy) object back into DS store
Motion.inject(motion); Motion.inject(motion);
// save change motion object on server // save changed motion object on server
Motion.save(motion).then( Motion.save(motion).then(
function(success) { function(success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (motion.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: motion.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
if (gotoDetailView) { if (gotoDetailView) {
$state.go('motions.motion.detail', {id: success.id}); $state.go('motions.motion.detail', {id: success.id});
} }

View File

@ -596,7 +596,8 @@ class CategoryViewSet(ModelViewSet):
# Remove old identifiers # Remove old identifiers
for motion in motions: for motion in motions:
motion.identifier = None motion.identifier = None
motion.skip_autoupdate = True # This line is to skip agenda item autoupdate. See agenda/signals.py. # This line is to skip agenda item autoupdate. See agenda/signals.py.
motion.agenda_item_update_information['skip_autoupdate'] = True
motion.save(skip_autoupdate=True) motion.save(skip_autoupdate=True)
# Set new identifers and change identifiers of amendments. # Set new identifers and change identifiers of amendments.
@ -615,7 +616,8 @@ class CategoryViewSet(ModelViewSet):
obj['new_identifier'], obj['new_identifier'],
child.identifier, child.identifier,
count=1) count=1)
child.skip_autoupdate = True # This line is to skip agenda item autoupdate. See agenda/signals.py. # This line is to skip agenda item autoupdate. See agenda/signals.py.
child.agenda_item_update_information['skip_autoupdate'] = True
child.save(skip_autoupdate=True) child.save(skip_autoupdate=True)
instances.append(child) instances.append(child)
instances.append(child.agenda_item) instances.append(child.agenda_item)

View File

@ -1,3 +1,5 @@
from typing import Any, Dict # noqa
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.db import models from django.db import models
@ -54,6 +56,11 @@ class Topic(RESTModelMixin, models.Model):
id=self.pk) id=self.pk)
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
"""
Container for runtime information for agenda app (on create or update of this instance).
"""
agenda_item_update_information = {} # type: Dict[str, Any]
@property @property
def agenda_item(self): def agenda_item(self):
""" """

View File

@ -1,4 +1,4 @@
from openslides.utils.rest_api import ModelSerializer from openslides.utils.rest_api import CharField, IntegerField, ModelSerializer
from openslides.utils.validate import validate_html from openslides.utils.validate import validate_html
from .models import Topic from .models import Topic
@ -8,11 +8,49 @@ class TopicSerializer(ModelSerializer):
""" """
Serializer for core.models.Topic objects. Serializer for core.models.Topic objects.
""" """
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2)
agenda_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)
agenda_weight = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
model = Topic model = Topic
fields = ('id', 'title', 'text', 'attachments', 'agenda_item_id') fields = (
'id',
'title',
'text',
'attachments',
'agenda_item_id',
'agenda_type',
'agenda_parent_id',
'agenda_comment',
'agenda_duration',
'agenda_weight',
)
def validate(self, data): def validate(self, data):
if 'text' in data: if 'text' in data:
data['text'] = validate_html(data['text']) data['text'] = validate_html(data['text'])
return data return data
def create(self, validated_data):
"""
Customized create method. Set information about related agenda item
into agenda_item_update_information container.
"""
agenda_type = validated_data.pop('agenda_type', None)
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
agenda_comment = validated_data.pop('agenda_comment', None)
agenda_duration = validated_data.pop('agenda_duration', None)
agenda_weight = validated_data.pop('agenda_weight', None)
attachments = validated_data.pop('attachments', [])
topic = Topic(**validated_data)
topic.agenda_item_update_information['type'] = agenda_type
topic.agenda_item_update_information['parent_id'] = agenda_parent_id
topic.agenda_item_update_information['comment'] = agenda_comment
topic.agenda_item_update_information['duration'] = agenda_duration
topic.agenda_item_update_information['weight'] = agenda_weight
topic.save()
topic.attachments.add(*attachments)
return topic

View File

@ -116,7 +116,9 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
} }
}); });
} }
// show as agenda item
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({ formFields.push({
key: 'showAsAgendaItem', key: 'showAsAgendaItem',
type: 'checkbox', type: 'checkbox',
@ -126,18 +128,16 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
}, },
hide: !operator.hasPerms('agenda.can_manage') hide: !operator.hasPerms('agenda.can_manage')
}); });
// parent item
if (isCreateForm) {
formFields.push({ formFields.push({
key: 'agenda_parent_item_id', key: 'agenda_parent_id',
type: 'select-single', type: 'select-single',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Parent item'), label: gettextCatalog.getString('Parent item'),
options: AgendaTree.getFlatTree(Agenda.getAll()), options: AgendaTree.getFlatTree(Agenda.getAll()),
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id', ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
placeholder: gettextCatalog.getString('Select a parent item ...') placeholder: gettextCatalog.getString('Select a parent item ...')
} },
hide: !operator.hasPerms('agenda.can_manage')
}); });
} }
@ -186,9 +186,8 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'Topic', 'Topic',
'TopicForm', 'TopicForm',
'Agenda', 'Agenda',
'AgendaUpdate',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Topic, TopicForm, Agenda, AgendaUpdate, ErrorMessage) { function($scope, $state, Topic, TopicForm, Agenda, ErrorMessage) {
$scope.topic = {}; $scope.topic = {};
$scope.model = {}; $scope.model = {};
$scope.model.showAsAgendaItem = true; $scope.model.showAsAgendaItem = true;
@ -196,13 +195,10 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
$scope.formFields = TopicForm.getFormFields(true); $scope.formFields = TopicForm.getFormFields(true);
// save form // save form
$scope.save = function (topic) { $scope.save = function (topic) {
topic.agenda_type = topic.showAsAgendaItem ? 1 : 2;
// The attribute topic.agenda_parent_id is set by the form, see form definition.
Topic.create(topic).then( Topic.create(topic).then(
function (success) { function (success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (topic.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: topic.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
$scope.closeThisDialog(); $scope.closeThisDialog();
}, function (error) { }, function (error) {
$scope.alert = ErrorMessage.forAlert(error); $scope.alert = ErrorMessage.forAlert(error);
@ -218,10 +214,9 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'Topic', 'Topic',
'TopicForm', 'TopicForm',
'Agenda', 'Agenda',
'AgendaUpdate',
'topicId', 'topicId',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Topic, TopicForm, Agenda, AgendaUpdate, topicId, ErrorMessage) { function($scope, $state, Topic, TopicForm, Agenda, topicId, ErrorMessage) {
var topic = Topic.get(topicId); var topic = Topic.get(topicId);
$scope.alert = {}; $scope.alert = {};
// set initial values for form model by create deep copy of topic object // set initial values for form model by create deep copy of topic object
@ -229,25 +224,17 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
$scope.model = angular.copy(topic); $scope.model = angular.copy(topic);
// get all form fields // get all form fields
$scope.formFields = TopicForm.getFormFields(); $scope.formFields = TopicForm.getFormFields();
for (var i = 0; i < $scope.formFields.length; i++) {
if ($scope.formFields[i].key == "showAsAgendaItem") {
// get state from agenda item (hidden/internal or agenda item)
$scope.formFields[i].defaultValue = !topic.agenda_item.is_hidden;
} else if ($scope.formFields[i].key == "agenda_parent_item_id") {
$scope.formFields[i].defaultValue = topic.agenda_item.parent_id;
}
}
// save form // save form
$scope.save = function (topic) { $scope.save = function (topic) {
Topic.create(topic).then( // inject the changed topic (copy) object back into DS store
Topic.inject(topic);
// save changed topic object on server
Topic.save(topic).then(
function(success) { function(success) {
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
// see openslides.agenda.models.Item.ITEM_TYPE.
var changes = [{key: 'type', value: (topic.showAsAgendaItem ? 1 : 2)},
{key: 'parent_id', value: topic.agenda_parent_item_id}];
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
$scope.closeThisDialog(); $scope.closeThisDialog();
}, function (error) { },
function (error) {
// save error: revert all changes by restore // save error: revert all changes by restore
// (refresh) original topic object from server // (refresh) original topic object from server
Topic.refresh(topic); Topic.refresh(topic);
@ -265,8 +252,7 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'Topic', 'Topic',
'HumanTimeConverter', 'HumanTimeConverter',
'TopicsCsvExample', 'TopicsCsvExample',
'AgendaUpdate', function($scope, gettext, Agenda, Topic, HumanTimeConverter, TopicsCsvExample) {
function($scope, gettext, Agenda, Topic, HumanTimeConverter, TopicsCsvExample, AgendaUpdate) {
// Big TODO: Change wording from "item" to "topic". // Big TODO: Change wording from "item" to "topic".
// import from textarea // import from textarea
$scope.importByLine = function () { $scope.importByLine = function () {
@ -275,12 +261,11 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
$scope.importcounter = 0; $scope.importcounter = 0;
$scope.titleItems.forEach(function(title, index) { $scope.titleItems.forEach(function(title, index) {
var item = {title: title}; var item = {title: title};
item.agenda_type = 1; // The new topic is not hidden.
item.agenda_weight = 1000 + index;
// TODO: create all items in bulk mode // TODO: create all items in bulk mode
Topic.create(item).then( Topic.create(item).then(
function(success) { function(success) {
var changes = [{key: 'type', value: 1},
{key: 'weight', value: 1000 + index}];
AgendaUpdate.saveChanges(success.agenda_item_id, changes);
$scope.importcounter++; $scope.importcounter++;
} }
); );
@ -363,13 +348,12 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
$scope.csvImporting = true; $scope.csvImporting = true;
angular.forEach($scope.items, function (item) { angular.forEach($scope.items, function (item) {
if (item.selected && !item.importerror) { if (item.selected && !item.importerror) {
item.agenda_type = item.type;
item.agenda_comment = item.comment;
item.agenda_duration = item.duration;
item.agenda_weight = item.weight;
Topic.create(item).then( Topic.create(item).then(
function(success) { function(success) {
var changes = [{key: 'duration', value: item.duration},
{key: 'comment', value: item.comment},
{key: 'type', value: item.type},
{key: 'weight', value: item.weight}];
AgendaUpdate.saveChanges(success.agenda_item_id, changes);
item.imported = true; item.imported = true;
} }
); );