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

This commit is contained in:
Oskar Hahn 2018-08-23 21:26:24 +02:00
commit 401e7821ae
37 changed files with 1162 additions and 927 deletions

View File

@ -11,6 +11,7 @@ Core:
- Change URL schema [#3798]. - Change URL schema [#3798].
- Update to channels2 [#3796]. - Update to channels2 [#3796].
- Drop Python 3.5 support[#3805]. - Drop Python 3.5 support[#3805].
- Adds a websocket protocol [#3807].
Version 2.3 (unreleased) Version 2.3 (unreleased)
@ -19,24 +20,28 @@ Version 2.3 (unreleased)
Agenda: Agenda:
- New item type 'hidden'. New visibilty filter in agenda [#3790]. - New item type 'hidden'. New visibilty filter in agenda [#3790].
Elections:
- Support to change decimal places for elections with a plugin [#3803]
Motions: Motions:
- New feature to scroll the projector to a specific line [#3748]. - New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647]. - New possibility to sort submitters [#3647].
- New representation of amendments (paragraph based creation, new diff - New representation of amendments (paragraph based creation, new diff
and list views for amendments) [#3637]. and list views for amendments) [#3637].
- New feature to customize workflows and states [#3772]. - New feature to customize workflows and states [#3772, #3785].
- New config options to show logos on the right side in PDF [#3768]. - New config options to show logos on the right side in PDF [#3768].
- New table of contents with page numbers and categories in PDF [#3766]. - New table of contents with page numbers and categories in PDF [#3766].
- New teporal field "modified final version" where the final version can - New teporal field "modified final version" where the final version can
be edited [#3781]. be edited [#3781].
- New config to show amendments also in motions table [#3792] - New config to show amendments also in motions table [#3792].
- Support to change decimal places for polls with a plugin [#3803].
Core: Core:
- Python 3.4 is not supported anymore [#3777]. - Python 3.4 is not supported anymore [#3777].
- Support Python 3.7 [#3786]. - Support Python 3.7 [#3786].
- Updated pdfMake to 0.1.37 [#3766]. - Updated pdfMake to 0.1.37 [#3766].
- Updated Django to 2.1 [#3777, #3786]. - Updated Django to 2.1 [#3777, #3786].
- Adds a websocket protocol [#3807]. - Changed behavior of collectstatic management command [#3804].
Version 2.2 (2018-06-06) Version 2.2 (2018-06-06)

View File

@ -47,7 +47,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
return {key: full_data[key] for key in whitelist} return {key: full_data[key] for key in whitelist}
# Parse data. # Parse data.
if has_perm(user, 'agenda.can_see'): if full_data and has_perm(user, 'agenda.can_see'):
if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_internal_items'): if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_internal_items'):
# Managers with special permission can see everything. # Managers with special permission can see everything.
data = full_data data = full_data
@ -62,6 +62,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
# In internal and hidden case managers and non managers see only some fields # In internal and hidden case managers and non managers see only some fields
# so that list of speakers is provided regardless. Hidden items can only be seen by managers. # so that list of speakers is provided regardless. Hidden items can only be seen by managers.
# We know that full_data has at least one entry which can be used to parse the keys.
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set(( blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set((
'id', 'id',
'title', 'title',
@ -83,7 +84,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
if full['is_hidden'] and can_see_hidden: if full['is_hidden'] and can_see_hidden:
# Same filtering for internal and hidden items # Same filtering for internal and hidden items
data.append(filtered_data(full, blocked_keys_internal_hidden_case)) data.append(filtered_data(full, blocked_keys_internal_hidden_case))
if full['is_internal']: elif full['is_internal']:
data.append(filtered_data(full, blocked_keys_internal_hidden_case)) data.append(filtered_data(full, blocked_keys_internal_hidden_case))
else: # agenda item else: # agenda item
data.append(filtered_data(full, blocked_keys_non_internal_hidden_case)) data.append(filtered_data(full, blocked_keys_non_internal_hidden_case))

View File

@ -67,7 +67,7 @@ def get_config_variables():
{'value': '1', 'display_name': 'Public item'}, {'value': '1', 'display_name': 'Public item'},
{'value': '2', 'display_name': 'Internal item'}, {'value': '2', 'display_name': 'Internal item'},
{'value': '3', 'display_name': 'Hidden item'}), {'value': '3', 'display_name': 'Hidden item'}),
label='Default visibility for new agenda items', label='Default visibility for new agenda items (except topics)',
weight=227, weight=227,
group='Agenda', group='Agenda',
subgroup='General') subgroup='General')

View File

@ -883,7 +883,7 @@ angular.module('OpenSlidesApp.agenda.site', [
gettext('[Begin speech] starts the countdown, [End speech] stops the ' + gettext('[Begin speech] starts the countdown, [End speech] stops the ' +
'countdown.'); 'countdown.');
gettext('Agenda visibility'); gettext('Agenda visibility');
gettext('Default visibility for new agenda items'); gettext('Default visibility for new agenda items (except topics)');
} }
]); ]);

View File

@ -0,0 +1,76 @@
# Generated by Django 2.1 on 2018-08-22 08:42
from decimal import Decimal
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assignments', '0004_auto_20180703_1523'),
]
operations = [
migrations.AlterField(
model_name='assignmentpoll',
name='votescast',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='assignmentpoll',
name='votesinvalid',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='assignmentpoll',
name='votesvalid',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='assignmentvote',
name='weight',
field=models.DecimalField(
decimal_places=6,
default=Decimal('1'),
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='assignmentpoll',
name='votesabstain',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='assignmentpoll',
name='votesno',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
]

View File

@ -1,8 +1,10 @@
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal
from typing import Any, Dict, List from typing import Any, Dict, List
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.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
@ -18,7 +20,7 @@ from openslides.poll.models import (
) )
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import MinMaxIntegerField, RESTModelMixin from openslides.utils.models import RESTModelMixin
from .access_permissions import AssignmentAccessPermissions from .access_permissions import AssignmentAccessPermissions
@ -422,9 +424,11 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
max_length=79, max_length=79,
blank=True) blank=True)
votesabstain = MinMaxIntegerField(null=True, blank=True, min_value=-2) votesabstain = models.DecimalField(null=True, blank=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
""" General abstain votes, used for pollmethod 'votes' """ """ General abstain votes, used for pollmethod 'votes' """
votesno = MinMaxIntegerField(null=True, blank=True, min_value=-2) votesno = models.DecimalField(null=True, blank=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
""" General no votes, used for pollmethod 'votes' """ """ General no votes, used for pollmethod 'votes' """
class Meta: class Meta:

View File

@ -3,6 +3,7 @@ from django.utils.translation import ugettext as _
from openslides.poll.serializers import default_votes_validator from openslides.poll.serializers import default_votes_validator
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
DecimalField,
DictField, DictField,
IntegerField, IntegerField,
ListField, ListField,
@ -98,7 +99,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
options = AssignmentOptionSerializer(many=True, read_only=True) options = AssignmentOptionSerializer(many=True, read_only=True)
votes = ListField( votes = ListField(
child=DictField( child=DictField(
child=IntegerField(min_value=-2)), child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)),
write_only=True, write_only=True,
required=False) required=False)
has_votes = SerializerMethodField() has_votes = SerializerMethodField()

View File

@ -14,6 +14,12 @@ angular.module('OpenSlidesApp.assignments', [])
return DS.defineResource({ return DS.defineResource({
name: 'assignments/polloption', name: 'assignments/polloption',
useClass: jsDataModel, useClass: jsDataModel,
// Change the stringified numbers to floats.
beforeInject: function (resource, instance) {
_.forEach(instance.votes, function (vote) {
vote.weight = parseFloat(vote.weight);
});
},
methods: { methods: {
getVotes: function () { getVotes: function () {
if (!this.poll.has_votes) { if (!this.poll.has_votes) {
@ -154,6 +160,15 @@ angular.module('OpenSlidesApp.assignments', [])
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
// Change the stringified numbers to floats.
beforeInject: function (resource, instance) {
var attrs = ['votescast', 'votesinvalid', 'votesvalid', 'votesabstain', 'votesno'];
_.forEach(attrs, function (attr) {
if (instance[attr] !== null) {
instance[attr] = parseFloat(instance[attr]);
}
});
},
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; return name;
@ -307,6 +322,24 @@ angular.module('OpenSlidesApp.assignments', [])
} }
]) ])
.provider('AssignmentPollDecimalPlaces', [
function () {
this.$get = ['$q', function ($q) {
return {
getPlaces: function (poll, find) {
if (find) {
return $q(function (resolve) {
resolve(0);
});
} else {
return 0;
}
},
};
}];
}
])
.factory('AssignmentRelatedUser', [ .factory('AssignmentRelatedUser', [
'DS', 'DS',
function (DS) { function (DS) {

View File

@ -9,7 +9,8 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
'HTMLValidizer', 'HTMLValidizer',
'gettextCatalog', 'gettextCatalog',
'PDFLayout', 'PDFLayout',
function($filter, HTMLValidizer, gettextCatalog, PDFLayout) { 'AssignmentPollDecimalPlaces',
function($filter, HTMLValidizer, gettextCatalog, PDFLayout, AssignmentPollDecimalPlaces) {
var createInstance = function(assignment) { var createInstance = function(assignment) {
@ -113,13 +114,13 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
}; };
//creates the voting string for the result table and differentiates between special values //creates the voting string for the result table and differentiates between special values
var parseVoteValue = function(voteObject, printLabel) { var parseVoteValue = function(voteObject, printLabel, precision) {
var voteVal = ''; var voteVal = '';
if (voteObject) { if (voteObject) {
if (printLabel) { if (printLabel) {
voteVal += voteObject.label + ': '; voteVal += voteObject.label + ': ';
} }
voteVal += voteObject.value; voteVal += $filter('number')(voteObject.value, precision);
if (voteObject.percentStr) { if (voteObject.percentStr) {
voteVal += ' ' + voteObject.percentStr; voteVal += ' ' + voteObject.percentStr;
@ -135,6 +136,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
_.forEach(assignment.polls, function(poll, pollIndex) { _.forEach(assignment.polls, function(poll, pollIndex) {
if (poll.published) { if (poll.published) {
var pollTableBody = []; var pollTableBody = [];
var precision = AssignmentPollDecimalPlaces.getPlaces(poll);
resultBody.push({ resultBody.push({
text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1), text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1),
@ -163,14 +165,14 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
if (poll.pollmethod == 'votes') { if (poll.pollmethod == 'votes') {
tableLine.push( tableLine.push(
{ {
text: parseVoteValue(votes[0], false), text: parseVoteValue(votes[0], false, precision),
style: PDFLayout.flipTableRowStyle(pollTableBody.length) style: PDFLayout.flipTableRowStyle(pollTableBody.length)
} }
); );
} else { } else {
var resultBlock = []; var resultBlock = [];
_.forEach(votes, function(vote) { _.forEach(votes, function(vote) {
resultBlock.push(parseVoteValue(vote, true)); resultBlock.push(parseVoteValue(vote, true, precision));
}); });
tableLine.push({ tableLine.push({
text: resultBlock, text: resultBlock,
@ -189,7 +191,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
style: 'tableConclude' style: 'tableConclude'
}, },
{ {
text: parseVoteValue(poll.getVote(fieldName), false), text: parseVoteValue(poll.getVote(fieldName), false, precision),
style: 'tableConclude' style: 'tableConclude'
}, },
]); ]);

View File

@ -16,15 +16,27 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment
.controller('SlideAssignmentCtrl', [ .controller('SlideAssignmentCtrl', [
'$scope', '$scope',
'Assignment', 'Assignment',
'AssignmentPoll',
'AssignmentPhases', 'AssignmentPhases',
'AssignmentPollDecimalPlaces',
'User', 'User',
function($scope, Assignment, AssignmentPhases, User) { function($scope, Assignment, AssignmentPoll, AssignmentPhases, AssignmentPollDecimalPlaces, User) {
// Attention! Each object that is used here has to be dealt on server side. // Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var id = $scope.element.id;
$scope.showResult = $scope.element.poll; $scope.showResult = $scope.element.poll;
if ($scope.showResult) {
var poll = AssignmentPoll.get($scope.showResult);
$scope.votesPrecision = 0;
if (poll) {
AssignmentPollDecimalPlaces.getPlaces(poll, true).then(function (decimalPlaces) {
$scope.votesPrecision = decimalPlaces;
});
}
}
Assignment.bindOne(id, $scope, 'assignment'); Assignment.bindOne(id, $scope, 'assignment');
$scope.phases = AssignmentPhases; $scope.phases = AssignmentPhases;
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');

View File

@ -218,11 +218,15 @@ angular.module('OpenSlidesApp.assignments.site', [
'Config', 'Config',
'AssignmentPollDetailCtrlCache', 'AssignmentPollDetailCtrlCache',
'AssignmentPoll', 'AssignmentPoll',
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache, AssignmentPoll) { 'AssignmentPollDecimalPlaces',
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache,
AssignmentPoll, AssignmentPollDecimalPlaces) {
// Define choices. // Define choices.
$scope.methodChoices = MajorityMethodChoices; $scope.methodChoices = MajorityMethodChoices;
// TODO: Get $scope.baseChoices from config_variables.py without copying them. // TODO: Get $scope.baseChoices from config_variables.py without copying them.
$scope.votesPrecision = AssignmentPollDecimalPlaces.getPlaces($scope.poll);
// Setup empty cache with default values. // Setup empty cache with default values.
if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') { if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
AssignmentPollDetailCtrlCache[$scope.poll.id] = { AssignmentPollDetailCtrlCache[$scope.poll.id] = {
@ -689,9 +693,11 @@ angular.module('OpenSlidesApp.assignments.site', [
'gettextCatalog', 'gettextCatalog',
'AssignmentPoll', 'AssignmentPoll',
'assignmentpollId', 'assignmentpollId',
'AssignmentPollDecimalPlaces',
'ballot', 'ballot',
'ErrorMessage', 'ErrorMessage',
function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpollId, ballot, ErrorMessage) { function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpollId,
AssignmentPollDecimalPlaces, ballot, ErrorMessage) {
// set initial values for form model by create deep copy of assignmentpoll object // set initial values for form model by create deep copy of assignmentpoll object
// so detail view is not updated while editing poll // so detail view is not updated while editing poll
var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId)); var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId));
@ -700,6 +706,9 @@ angular.module('OpenSlidesApp.assignments.site', [
$scope.formFields = []; $scope.formFields = [];
$scope.alert = {}; $scope.alert = {};
// For number inputs
var step = Math.pow(10, -AssignmentPollDecimalPlaces.getPlaces(assignmentpoll));
// add dynamic form fields // add dynamic form fields
var options = $filter('orderBy')(assignmentpoll.options, 'weight'); var options = $filter('orderBy')(assignmentpoll.options, 'weight');
_.forEach(options, function(option) { _.forEach(options, function(option) {
@ -720,6 +729,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: gettextCatalog.getString('Yes'), label: gettextCatalog.getString('Yes'),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true required: true
}, },
defaultValue: defaultValue.yes defaultValue: defaultValue.yes
@ -733,6 +743,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: gettextCatalog.getString('No'), label: gettextCatalog.getString('No'),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true required: true
}, },
defaultValue: defaultValue.no defaultValue: defaultValue.no
@ -747,6 +758,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: gettextCatalog.getString('Abstain'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true required: true
}, },
defaultValue: defaultValue.abstain defaultValue: defaultValue.abstain
@ -771,6 +783,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: option.candidate.get_full_name(), label: option.candidate.get_full_name(),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true, required: true,
}, },
defaultValue: defaultValue defaultValue: defaultValue
@ -785,6 +798,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Abstain'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
}, },
@ -794,6 +808,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('No'), label: gettextCatalog.getString('No'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
} }
@ -810,6 +825,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Valid ballots'), label: gettextCatalog.getString('Valid ballots'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
}, },
@ -819,6 +835,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Invalid ballots'), label: gettextCatalog.getString('Invalid ballots'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
}, },
@ -828,6 +845,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Casted ballots'), label: gettextCatalog.getString('Casted ballots'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
} }

View File

@ -234,7 +234,7 @@
<div ng-init="votes = option.getVotes()"> <div ng-init="votes = option.getVotes()">
<div ng-repeat="vote in votes"> <div ng-repeat="vote in votes">
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span> <span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
<div ng-if="vote.percentNumber >= 0"> <div ng-if="vote.percentNumber >= 0">
<uib-progressbar ng-if="$index == 0" value="vote.percentNumber" type="success"></uib-progressbar> <uib-progressbar ng-if="$index == 0" value="vote.percentNumber" type="success"></uib-progressbar>
<uib-progressbar ng-if="$index == 1" value="vote.percentNumber" type="danger"></uib-progressbar> <uib-progressbar ng-if="$index == 1" value="vote.percentNumber" type="danger"></uib-progressbar>
@ -244,10 +244,10 @@
</div> </div>
<td ng-hide="method === 'disabled'"> <td ng-hide="method === 'disabled'">
<span ng-if="option.majorityReached >= 0" class="text-success" translate> <span ng-if="option.majorityReached >= 0" class="text-success" translate>
Quorum ({{ option.getVoteYes() - option.majorityReached }}) reached. Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) reached.
</span> </span>
<span ng-if="option.majorityReached < 0" class="text-danger" translate> <span ng-if="option.majorityReached < 0" class="text-danger" translate>
Quorum ({{ option.getVoteYes() - option.majorityReached }}) not reached. Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) not reached.
</span> </span>
<!-- total votes (valid/invalid/casts) --> <!-- total votes (valid/invalid/casts) -->
@ -255,31 +255,31 @@
<td> <td>
<translate>Abstain</translate> <translate>Abstain</translate>
<td> <td>
{{ poll.getVote('votesabstain').value }} {{ poll.getVote('votesabstain').value | number:votesPrecision }}
{{ poll.getVote('votesabstain').percentStr }} {{ poll.getVote('votesabstain').percentStr }}
<tr ng-if="poll.pollmethod === 'votes'"> <tr ng-if="poll.pollmethod === 'votes'">
<td> <td>
<translate>No</translate> <translate>No</translate>
<td> <td>
{{ poll.getVote('votesno').value }} {{ poll.getVote('votesno').value | number:votesPrecision }}
{{ poll.getVote('votesno').percentStr }} {{ poll.getVote('votesno').percentStr }}
<tr> <tr>
<td> <td>
<translate>Valid ballots</translate> <translate>Valid ballots</translate>
<td> <td>
{{ poll.getVote('votesvalid').value }} {{ poll.getVote('votesvalid').value | number:votesPrecision }}
{{ poll.getVote('votesvalid').percentStr }} {{ poll.getVote('votesvalid').percentStr }}
<tr> <tr>
<td> <td>
<translate>Invalid ballots</translate> <translate>Invalid ballots</translate>
<td> <td>
{{ poll.getVote('votesinvalid').value }} {{ poll.getVote('votesinvalid').value | number:votesPrecision }}
{{ poll.getVote('votesinvalid').percentStr }} {{ poll.getVote('votesinvalid').percentStr }}
<tr class="total bg-info"> <tr class="total bg-info">
<td> <td>
<translate>Casted ballots</translate> <translate>Casted ballots</translate>
<td> <td>
{{ poll.getVote('votescast').value }} {{ poll.getVote('votescast').value | number:votesPrecision }}
{{ poll.getVote('votescast').percentStr }} {{ poll.getVote('votescast').percentStr }}
</table> </table>

View File

@ -49,16 +49,19 @@
<div ng-init="votes = option.getVotes()"> <div ng-init="votes = option.getVotes()">
<div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'"> <div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">
<span ng-show="poll.pollmethod == 'yna'"> <span ng-show="poll.pollmethod == 'yna'">
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br> {{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}<br> {{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}<br>
{{ votes[2].label | translate }}: {{ votes[2].value }} {{ votes[2].percentStr }}</span> {{ votes[2].label | translate }}: {{ votes[2].value | number:votesPrecision }} {{ votes[2].percentStr }}</span>
<span ng-show="poll.pollmethod == 'yn'"> <span ng-show="poll.pollmethod == 'yn'">
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br> {{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}</span> {{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}</span>
</div> </div>
<div ng-show="poll.pollmethod == 'votes'"> <div ng-show="poll.pollmethod == 'votes'">
<div ng-repeat="vote in votes"> <div ng-repeat="vote in votes">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
<div style="float:right; width:200px;" ng-if="vote.percentNumber >= 0">
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -68,29 +71,29 @@
<td> <td>
<translate>Abstain</translate> <translate>Abstain</translate>
<td ng-init="vote = poll.getVote('votesabstain')"> <td ng-init="vote = poll.getVote('votesabstain')">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
<tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesno').value !== null"> <tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesno').value !== null">
<td> <td>
<translate>No</translate> <translate>No</translate>
<td ng-init="vote = poll.getVote('votesno')"> <td ng-init="vote = poll.getVote('votesno')">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null"> <tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null">
<td> <td>
<translate>Valid ballots</translate> <translate>Valid ballots</translate>
<td ng-init="vote = poll.getVote('votesvalid')"> <td ng-init="vote = poll.getVote('votesvalid')">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesinvalid').value !== null"> <tr class="total" ng-if="poll.has_votes && poll.getVote('votesinvalid').value !== null">
<td> <td>
<translate>Invalid ballots</translate> <translate>Invalid ballots</translate>
<td ng-init="vote = poll.getVote('votesinvalid')"> <td ng-init="vote = poll.getVote('votesinvalid')">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
<tr class="total bg-info" ng-if="poll.has_votes && poll.getVote('votescast').value !== null"> <tr class="total bg-info" ng-if="poll.has_votes && poll.getVote('votescast').value !== null">
<td> <td>
<translate>Casted ballots</translate> <translate>Casted ballots</translate>
<td ng-init="vote = poll.getVote('votescast')"> <td ng-init="vote = poll.getVote('votescast')">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
</table> </table>
</div> </div>

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.contrib.staticfiles.management.commands.collectstatic import ( from django.contrib.staticfiles.management.commands.collectstatic import (
Command as CollectStatic, Command as CollectStatic,
) )
from django.contrib.staticfiles.utils import matches_patterns
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.db.utils import OperationalError from django.db.utils import OperationalError
@ -19,6 +20,8 @@ class Command(CollectStatic):
js_filename = 'webclient-{}.js' js_filename = 'webclient-{}.js'
def handle(self, **options: Any) -> str: def handle(self, **options: Any) -> str:
if options['link']:
raise CommandError("Option 'link' is not supported.")
try: try:
self.view = WebclientJavaScriptView() self.view = WebclientJavaScriptView()
except OperationalError: except OperationalError:
@ -27,24 +30,37 @@ class Command(CollectStatic):
return super().handle(**options) return super().handle(**options)
def collect(self) -> Dict[str, Any]: def collect(self) -> Dict[str, Any]:
result = super().collect()
try: try:
destination_dir = os.path.join(settings.STATICFILES_DIRS[0], 'js') destination_dir = os.path.join(settings.STATIC_ROOT, 'js')
except IndexError: except IndexError:
# If the user does not want do have staticfiles, he should not get # If the user does not want do have staticfiles, he should not get
# the webclient files either. # the webclient files either.
pass pass
else: else:
if not os.path.exists(destination_dir): if self.dry_run:
os.makedirs(destination_dir) self.log('Pretending to write WebclientJavaScriptView for all realms.', level=1)
else:
if not os.path.exists(destination_dir):
os.makedirs(destination_dir)
for realm in self.realms: for realm in self.realms:
filename = self.js_filename.format(realm) filename = self.js_filename.format(realm)
content = self.view.get(realm=realm).content # Matches only the basename.
path = os.path.join(destination_dir, filename) if matches_patterns(filename, self.ignore_patterns):
with open(path, 'wb+') as f: continue
f.write(content) path = os.path.join(destination_dir, filename)
self.stdout.write("Written WebclientJavaScriptView for realm {} to '{}'".format( if matches_patterns(path, self.ignore_patterns):
realm, continue
path))
return super().collect() content = self.view.get(realm=realm).content
with open(path, 'wb+') as f:
f.write(content)
message = "Written WebclientJavaScriptView for realm {} to '{}'".format(
realm,
path)
self.log(message, level=1)
result['modified'].append(path)
return result

View File

@ -962,7 +962,9 @@ angular.module('OpenSlidesApp.core.pdf', [])
currentParagraph.text.push(create('text', ' ')); currentParagraph.text.push(create('text', ' '));
} else if (isInsideAList(element) && lineNumberMode === 'none') { } else if (isInsideAList(element) && lineNumberMode === 'none') {
// Put a spacer there, if there is one BR in a list // Put a spacer there, if there is one BR in a list
alreadyConverted.push(create('text', ' ')); var spacer = create('text', ' ');
spacer.lineHeight = 0.25;
alreadyConverted.push(spacer);
} }
currentParagraph.lineHeight = 1.25; currentParagraph.lineHeight = 1.25;
alreadyConverted.push(currentParagraph); alreadyConverted.push(currentParagraph);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
# Generated by Django 2.1 on 2018-08-22 08:42
from decimal import Decimal
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('motions', '0009_motionversion_modified_final_version'),
]
operations = [
migrations.AlterField(
model_name='motionpoll',
name='votescast',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='motionpoll',
name='votesinvalid',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='motionpoll',
name='votesvalid',
field=models.DecimalField(
blank=True,
decimal_places=6,
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
migrations.AlterField(
model_name='motionvote',
name='weight',
field=models.DecimalField(
decimal_places=6,
default=Decimal('1'),
max_digits=15,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
),
]

View File

@ -1,4 +1,4 @@
from typing import Dict from typing import Dict, Optional
from django.db import transaction from django.db import transaction
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _
from ..poll.serializers import default_votes_validator from ..poll.serializers import default_votes_validator
from ..utils.rest_api import ( from ..utils.rest_api import (
CharField, CharField,
DecimalField,
DictField, DictField,
Field, Field,
IntegerField, IntegerField,
@ -101,12 +102,11 @@ class WorkflowSerializer(ModelSerializer):
Serializer for motion.models.Workflow objects. Serializer for motion.models.Workflow objects.
""" """
states = StateSerializer(many=True, read_only=True) states = StateSerializer(many=True, read_only=True)
# The first_state is checked in the update() method
first_state = PrimaryKeyRelatedField(queryset=State.objects.all(), required=False)
class Meta: class Meta:
model = Workflow model = Workflow
fields = ('id', 'name', 'states', 'first_state',) fields = ('id', 'name', 'states', 'first_state',)
read_only_fields = ('first_state',)
@transaction.atomic @transaction.atomic
def create(self, validated_data): def create(self, validated_data):
@ -127,17 +127,6 @@ class WorkflowSerializer(ModelSerializer):
workflow.save() workflow.save()
return workflow return workflow
@transaction.atomic
def update(self, workflow, validated_data):
"""
Check, if the first state is in the right workflow.
"""
first_state = validated_data.get('first_state')
if first_state is not None:
if workflow.pk != first_state.workflow.pk:
raise ValidationError({'detail': 'You cannot select a state which is not in the workflow as the first state.'})
return super().update(workflow, validated_data)
class MotionCommentsJSONSerializerField(Field): class MotionCommentsJSONSerializerField(Field):
""" """
@ -212,7 +201,7 @@ class MotionPollSerializer(ModelSerializer):
no = SerializerMethodField() no = SerializerMethodField()
abstain = SerializerMethodField() abstain = SerializerMethodField()
votes = DictField( votes = DictField(
child=IntegerField(min_value=-2, allow_null=True), child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True),
write_only=True) write_only=True)
has_votes = SerializerMethodField() has_votes = SerializerMethodField()
@ -238,21 +227,21 @@ class MotionPollSerializer(ModelSerializer):
def get_yes(self, obj): def get_yes(self, obj):
try: try:
result = self.get_votes_dict(obj)['Yes'] result: Optional[str] = str(self.get_votes_dict(obj)['Yes'])
except KeyError: except KeyError:
result = None result = None
return result return result
def get_no(self, obj): def get_no(self, obj):
try: try:
result = self.get_votes_dict(obj)['No'] result: Optional[str] = str(self.get_votes_dict(obj)['No'])
except KeyError: except KeyError:
result = None result = None
return result return result
def get_abstain(self, obj): def get_abstain(self, obj):
try: try:
result = self.get_votes_dict(obj)['Abstain'] result: Optional[str] = str(self.get_votes_dict(obj)['Abstain'])
except KeyError: except KeyError:
result = None result = None
return result return result

View File

@ -112,7 +112,7 @@
} }
.motion-text.line-numbers-none li > br { .motion-text.line-numbers-none li > br {
margin-top: 8px; margin-top: 6px;
content: " "; content: " ";
display: block; display: block;
&.os-line-break { &.os-line-break {

View File

@ -54,7 +54,7 @@ angular.module('OpenSlidesApp.motions', [
name: 'motions/workflow', name: 'motions/workflow',
methods: { methods: {
getFirstState: function () { getFirstState: function () {
return DS.get('motions/state', this.first_state); return DS.get('motions/state', this.first_state_id);
}, },
}, },
relations: { relations: {
@ -85,6 +85,14 @@ angular.module('OpenSlidesApp.motions', [
} }
} }
}, },
beforeInject: function (resource, instance) {
var attrs = ['yes', 'no', 'abstain', 'votescast', 'votesinvalid', 'votesvalid'];
_.forEach(attrs, function (attr) {
if (instance[attr] !== null) {
instance[attr] = parseFloat(instance[attr]);
}
});
},
methods: { methods: {
// Returns percent base. Returns undefined if calculation is not possible in general. // Returns percent base. Returns undefined if calculation is not possible in general.
getPercentBase: function (config, type) { getPercentBase: function (config, type) {
@ -196,6 +204,24 @@ angular.module('OpenSlidesApp.motions', [
} }
]) ])
.provider('MotionPollDecimalPlaces', [
function () {
this.$get = ['$q', function ($q) {
return {
getPlaces: function (poll, find) {
if (find) {
return $q(function (resolve) {
resolve(0);
});
} else {
return 0;
}
},
};
}];
}
])
.factory('MotionStateAndRecommendationParser', [ .factory('MotionStateAndRecommendationParser', [
'DS', 'DS',
'gettextCatalog', 'gettextCatalog',

View File

@ -17,9 +17,10 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'Config', 'Config',
'Motion', 'Motion',
'MotionComment', 'MotionComment',
'MotionPollDecimalPlaces',
'OpenSlidesSettings', 'OpenSlidesSettings',
function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter,
HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) { HTMLValidizer, Category, Config, Motion, MotionComment, MotionPollDecimalPlaces, OpenSlidesSettings) {
/** /**
* Provides the content as JS objects for Motions in pdfMake context * Provides the content as JS objects for Motions in pdfMake context
* @constructor * @constructor
@ -185,40 +186,41 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
column2.push(''); column2.push('');
column3.push(''); column3.push('');
} }
var precision = MotionPollDecimalPlaces.getPlaces(poll);
// yes // yes
var yes = poll.getVote(poll.yes, 'yes'); var yes = poll.getVote(poll.yes, 'yes');
column1.push(gettextCatalog.getString('Yes') + ':'); column1.push(gettextCatalog.getString('Yes') + ':');
column2.push(yes.value); column2.push($filter('number')(yes.value, precision));
column3.push(yes.percentStr); column3.push(yes.percentStr);
// no // no
var no = poll.getVote(poll.no, 'no'); var no = poll.getVote(poll.no, 'no');
column1.push(gettextCatalog.getString('No') + ':'); column1.push(gettextCatalog.getString('No') + ':');
column2.push(no.value); column2.push($filter('number')(no.value, precision));
column3.push(no.percentStr); column3.push(no.percentStr);
// abstain // abstain
var abstain = poll.getVote(poll.abstain, 'abstain'); var abstain = poll.getVote(poll.abstain, 'abstain');
column1.push(gettextCatalog.getString('Abstain') + ':'); column1.push(gettextCatalog.getString('Abstain') + ':');
column2.push(abstain.value); column2.push($filter('number')(abstain.value, precision));
column3.push(abstain.percentStr); column3.push(abstain.percentStr);
// votes valid // votes valid
if (poll.votesvalid) { if (poll.votesvalid) {
var valid = poll.getVote(poll.votesvalid, 'votesvalid'); var valid = poll.getVote(poll.votesvalid, 'votesvalid');
column1.push(gettextCatalog.getString('Valid votes') + ':'); column1.push(gettextCatalog.getString('Valid votes') + ':');
column2.push(valid.value); column2.push($filter('number')(valid.value, precision));
column3.push(valid.percentStr); column3.push(valid.percentStr);
} }
// votes invalid // votes invalid
if (poll.votesvalid) { if (poll.votesvalid) {
var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid'); var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid');
column1.push(gettextCatalog.getString('Invalid votes') + ':'); column1.push(gettextCatalog.getString('Invalid votes') + ':');
column2.push(invalid.value); column2.push($filter('number')(invalid.value, precision));
column3.push(invalid.percentStr); column3.push(invalid.percentStr);
} }
// votes cast // votes cast
if (poll.votescast) { if (poll.votescast) {
var cast = poll.getVote(poll.votescast, 'votescast'); var cast = poll.getVote(poll.votescast, 'votescast');
column1.push(gettextCatalog.getString('Votes cast') + ':'); column1.push(gettextCatalog.getString('Votes cast') + ':');
column2.push(cast.value); column2.push($filter('number')(cast.value, precision));
column3.push(cast.percentStr); column3.push(cast.percentStr);
} }
} }

View File

@ -26,7 +26,9 @@ angular.module('OpenSlidesApp.motions.projector', [
'User', 'User',
'Notify', 'Notify',
'ProjectorID', 'ProjectorID',
function($scope, Config, Motion, MotionChangeRecommendation, ChangeRecommendationView, User, Notify, ProjectorID) { 'MotionPollDecimalPlaces',
function($scope, Config, Motion, MotionChangeRecommendation, ChangeRecommendationView, User,
Notify, ProjectorID, MotionPollDecimalPlaces) {
// Attention! Each object that is used here has to be dealt on server side. // Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
@ -67,8 +69,21 @@ angular.module('OpenSlidesApp.motions.projector', [
$scope.motion = Motion.get(motionId); $scope.motion = Motion.get(motionId);
$scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff(); $scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
$scope.viewChangeRecommendations.setVersion($scope.motion, $scope.motion.active_version); $scope.viewChangeRecommendations.setVersion($scope.motion, $scope.motion.active_version);
_.forEach($scope.motion.polls, function (poll) {
MotionPollDecimalPlaces.getPlaces(poll, true).then(function (decimalPlaces) {
precisionCache[poll.id] = decimalPlaces;
});
});
}); });
var precisionCache = {};
$scope.getPollVotesPrecision = function (poll) {
if (!precisionCache[poll.id]) {
return 0;
}
return precisionCache[poll.id];
};
// Change recommendation viewing // Change recommendation viewing
$scope.viewChangeRecommendations = ChangeRecommendationView; $scope.viewChangeRecommendations = ChangeRecommendationView;
$scope.viewChangeRecommendations.initProjector($scope, Motion.get(motionId), $scope.mode); $scope.viewChangeRecommendations.initProjector($scope, Motion.get(motionId), $scope.mode);

View File

@ -750,7 +750,8 @@ angular.module('OpenSlidesApp.motions.site', [
'gettextCatalog', 'gettextCatalog',
function (gettextCatalog) { function (gettextCatalog) {
return { return {
getFormFields: function () { getFormFields: function (precision) {
var step = Math.pow(10, -precision);
return [ return [
{ {
key: 'yes', key: 'yes',
@ -758,6 +759,7 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Yes'), label: gettextCatalog.getString('Yes'),
type: 'number', type: 'number',
step: step,
required: true required: true
} }
}, },
@ -767,6 +769,7 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('No'), label: gettextCatalog.getString('No'),
type: 'number', type: 'number',
step: step,
required: true required: true
} }
}, },
@ -776,6 +779,7 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Abstain'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
step: step,
required: true required: true
} }
}, },
@ -784,6 +788,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Valid votes'), label: gettextCatalog.getString('Valid votes'),
step: step,
type: 'number' type: 'number'
} }
}, },
@ -792,6 +797,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Invalid votes'), label: gettextCatalog.getString('Invalid votes'),
step: step,
type: 'number' type: 'number'
} }
}, },
@ -800,6 +806,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Votes cast'), label: gettextCatalog.getString('Votes cast'),
step: step,
type: 'number' type: 'number'
} }
}]; }];
@ -836,6 +843,10 @@ angular.module('OpenSlidesApp.motions.site', [
var someMotionsHaveAmendments = _.some(motions, function (motion) { var someMotionsHaveAmendments = _.some(motions, function (motion) {
return motion.hasAmendments(); return motion.hasAmendments();
}); });
// if amendments amendments are already included. We owudl have them twice, if the option is enabled.
if (Config.get('motions_amendments_main_table').value) {
someMotionsHaveAmendments = false;
}
var getMetaInformationOptions = function (disabled) { var getMetaInformationOptions = function (disabled) {
if (!disabled) { if (!disabled) {
disabled = {}; disabled = {};
@ -1130,11 +1141,14 @@ angular.module('OpenSlidesApp.motions.site', [
'MajorityMethodChoices', 'MajorityMethodChoices',
'Config', 'Config',
'MotionPollDetailCtrlCache', 'MotionPollDetailCtrlCache',
function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache) { 'MotionPollDecimalPlaces',
function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache, MotionPollDecimalPlaces) {
// Define choices. // Define choices.
$scope.methodChoices = MajorityMethodChoices; $scope.methodChoices = MajorityMethodChoices;
// TODO: Get $scope.baseChoices from config_variables.py without copying them. // TODO: Get $scope.baseChoices from config_variables.py without copying them.
$scope.votesPrecision = MotionPollDecimalPlaces.getPlaces($scope.poll);
// Setup empty cache with default values. // Setup empty cache with default values.
if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') { if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
MotionPollDetailCtrlCache[$scope.poll.id] = { MotionPollDetailCtrlCache[$scope.poll.id] = {
@ -2517,17 +2531,19 @@ angular.module('OpenSlidesApp.motions.site', [
'gettextCatalog', 'gettextCatalog',
'MotionPoll', 'MotionPoll',
'MotionPollForm', 'MotionPollForm',
'MotionPollDecimalPlaces',
'motionpollId', 'motionpollId',
'voteNumber', 'voteNumber',
'ErrorMessage', 'ErrorMessage',
function ($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId, function ($scope, gettextCatalog, MotionPoll, MotionPollForm, MotionPollDecimalPlaces,
voteNumber, ErrorMessage) { motionpollId, voteNumber, ErrorMessage) {
// set initial values for form model by create deep copy of motionpoll object // set initial values for form model by create deep copy of motionpoll object
// so detail view is not updated while editing poll // so detail view is not updated while editing poll
var motionpoll = MotionPoll.get(motionpollId); var motionpoll = MotionPoll.get(motionpollId);
$scope.model = angular.copy(motionpoll); $scope.model = angular.copy(motionpoll);
$scope.voteNumber = voteNumber; $scope.voteNumber = voteNumber;
$scope.formFields = MotionPollForm.getFormFields(); var precision = MotionPollDecimalPlaces.getPlaces(motionpoll);
$scope.formFields = MotionPollForm.getFormFields(precision);
$scope.alert = {}; $scope.alert = {};
// save motionpoll // save motionpoll
@ -3291,6 +3307,7 @@ angular.module('OpenSlidesApp.motions.site', [
// misc strings (used dynamically in templates by translate filter) // misc strings (used dynamically in templates by translate filter)
gettext('needed'); gettext('needed');
gettext('Amendment');
} }
]); ]);

View File

@ -49,7 +49,9 @@ angular.module('OpenSlidesApp.motions.workflow', [])
return Workflow.lastModified(workflowId); return Workflow.lastModified(workflowId);
}, function () { }, function () {
$scope.workflow = Workflow.get(workflowId); $scope.workflow = Workflow.get(workflowId);
_.forEach($scope.workflow.states, function (state) { $scope.states = $scope.workflow.states;
$scope.states = _.orderBy($scope.states, 'id');
_.forEach($scope.states, function (state) {
state.newActionWord = gettextCatalog.getString(state.action_word); state.newActionWord = gettextCatalog.getString(state.action_word);
state.newRecommendationLabel = gettextCatalog.getString(state.recommendation_label); state.newRecommendationLabel = gettextCatalog.getString(state.recommendation_label);
}); });
@ -120,13 +122,6 @@ angular.module('OpenSlidesApp.motions.workflow', [])
}); });
}; };
$scope.setFirstState = function (state) {
$scope.workflow.first_state = state.id;
Workflow.save($scope.workflow).then(null, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
});
};
// Save expand state so the session // Save expand state so the session
if ($sessionStorage.motionStateTableExpandState) { if ($sessionStorage.motionStateTableExpandState) {
$scope.toggleExpandContent(); $scope.toggleExpandContent();

View File

@ -174,7 +174,7 @@
<div ng-if="!motion.isAmendment && motion.isAllowed('can_see_amendments')"> <div ng-if="!motion.isAmendment && motion.isAllowed('can_see_amendments')">
<h3 translate>Amendments</h3> <h3 translate>Amendments</h3>
<a ng-if="motion.hasAmendments()" ui-sref="motions.motion.amendment-list({id: motion.id})"> <a ng-if="motion.hasAmendments()" ui-sref="motions.motion.amendment-list({id: motion.id})">
{{ motion.getAmendments().length }} <translate>Amendments</translate><br> {{ motion.getAmendments().length }} {{ (motion.getAmendments().length === 1 ? 'Amendment' : 'Amendments') | translate }}<br>
</a> </a>
<button ng-if="motion.isAllowed('can_create_amendment')" ng-click="newAmendment()" class="btn btn-default btn-sm"> <button ng-if="motion.isAllowed('can_create_amendment')" ng-click="newAmendment()" class="btn btn-default btn-sm">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
@ -404,7 +404,7 @@
<td ng-init="voteYes = poll.getVote(poll.yes, 'yes')"> <td ng-init="voteYes = poll.getVote(poll.yes, 'yes')">
<span class="result-label"><translate>Yes</translate>:</span> <span class="result-label"><translate>Yes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteYes.value }} {{ voteYes.percentStr }} {{ voteYes.value | number:votesPrecision }} {{ voteYes.percentStr }}
</span> </span>
<div ng-if="voteYes.percentNumber"> <div ng-if="voteYes.percentNumber">
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar> <uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
@ -416,7 +416,7 @@
<td ng-init="voteNo = poll.getVote(poll.no, 'no')"> <td ng-init="voteNo = poll.getVote(poll.no, 'no')">
<span class="result-label"><translate>No</translate>:</span> <span class="result-label"><translate>No</translate>:</span>
<span class="result_value" > <span class="result_value" >
{{ voteNo.value }} {{ voteNo.percentStr }} {{ voteNo.value | number:votesPrecision }} {{ voteNo.percentStr }}
</span> </span>
<div ng-if="voteNo.percentNumber"> <div ng-if="voteNo.percentNumber">
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar> <uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
@ -428,7 +428,7 @@
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')"> <td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
<span class="result-label"><translate>Abstain</translate>:</span> <span class="result-label"><translate>Abstain</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteAbstain.value }} {{ voteAbstain.percentStr }} {{ voteAbstain.value | number:votesPrecision }} {{ voteAbstain.percentStr }}
</span> </span>
<div ng-if="voteAbstain.percentNumber"> <div ng-if="voteAbstain.percentNumber">
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar> <uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>
@ -440,7 +440,7 @@
<td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')"> <td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')">
<span class="result-label"><translate>Valid votes</translate>:</span> <span class="result-label"><translate>Valid votes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesValid.value }} {{ votesValid.percentStr }} {{ votesValid.value | number:votesPrecision }} {{ votesValid.percentStr }}
</span> </span>
<!-- invalid votes --> <!-- invalid votes -->
<tr ng-if="poll.votesinvalid !== null"> <tr ng-if="poll.votesinvalid !== null">
@ -449,7 +449,7 @@
<td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')"> <td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')">
<span class="result-label"><translate>Invalid votes</translate>:</span> <span class="result-label"><translate>Invalid votes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesInvalid.value }} {{ votesInvalid.percentStr }} {{ votesInvalid.value | number:votesPrecision }} {{ votesInvalid.percentStr }}
</span> </span>
<!-- votes cast --> <!-- votes cast -->
<tr class="total" ng-if="poll.votescast !== null"> <tr class="total" ng-if="poll.votescast !== null">
@ -458,7 +458,7 @@
<td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')"> <td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')">
<span class="result-label"><translate>Votes cast</translate>:</span> <span class="result-label"><translate>Votes cast</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesCast.value }} {{ votesCast.percentStr }} {{ votesCast.value | number:votesPrecision }} {{ votesCast.percentStr }}
</span> </span>
<!-- majority calculation --> <!-- majority calculation -->
@ -479,10 +479,10 @@
<td> <td>
<div os-perms="motions.can_manage"> <div os-perms="motions.can_manage">
<span class="text-success" ng-if="isReached() >= 0" translate> <span class="text-success" ng-if="isReached() >= 0" translate>
Quorum ({{ voteYes.value - isReached() }}) reached. Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) reached.
</span> </span>
<span class="text-danger" ng-if="isReached() < 0" translate> <span class="text-danger" ng-if="isReached() < 0" translate>
Quorum ({{ voteYes.value - isReached() }}) not reached. Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) not reached.
</span> </span>
</div> </div>
@ -556,16 +556,16 @@
</div> </div>
<div style="text-align: right;" ng-if="(change_recommendations | filter:{motion_version_id:version}:true).length > 0"> <div style="text-align: right;" ng-if="(change_recommendations | filter:{motion_version_id:version}:true).length > 0">
<button class="btn btn-default" <button class="btn btn-default btn-sm" os-perms="motions.can_manage"
ng-bootbox-confirm="{{ 'Do you want to copy the final version to the modified final version field?' | translate }}" ng-bootbox-confirm="{{ 'Do you want to copy the final version to the final print template?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.copyToModifiedFinalVersion(motion, version)"> ng-bootbox-confirm-action="viewChangeRecommendations.copyToModifiedFinalVersion(motion, version)">
<i class="fa fa-file-text"></i> <i class="fa fa-file-text"></i>
<translate>Copy to modified final version</translate> <translate>Create final print template</translate>
</button> </button>
</div> </div>
<div style="text-align: right;" ng-if="motion.state.versioning && (change_recommendations | filter:{motion_version_id:version}:true).length > 0"> <div style="text-align: right;" ng-if="motion.state.versioning && (change_recommendations | filter:{motion_version_id:version}:true).length > 0">
<button class="btn btn-default" <button class="btn btn-default btn-sm"
ng-bootbox-confirm="{{ 'Do you want to create a new version of this motion based on this changes?' | translate }}" ng-bootbox-confirm="{{ 'Do you want to create a new version of this motion based on this changes?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.newVersionIncludingChanges(motion, version, false)"> ng-bootbox-confirm-action="viewChangeRecommendations.newVersionIncludingChanges(motion, version, false)">
<i class="fa fa-file-text"></i> <i class="fa fa-file-text"></i>

View File

@ -103,13 +103,14 @@
ng-checked="viewChangeRecommendations.mode == 'agreed'"> ng-checked="viewChangeRecommendations.mode == 'agreed'">
<translate>Final version</translate> <translate>Final version</translate>
</label> </label>
<label class="btn btn-sm btn-default" ng-if="motion.getModifiedFinalVersion()" <label class="btn btn-sm btn-default" os-perms="motions.can_manage"
ng-show="motion.getModifiedFinalVersion()"
ng-class="{active: (viewChangeRecommendations.mode == 'modified_agreed')}" ng-class="{active: (viewChangeRecommendations.mode == 'modified_agreed')}"
ng-click="viewChangeRecommendations.mode = 'modified_agreed'"> ng-click="viewChangeRecommendations.mode = 'modified_agreed'">
<input type="radio" name="viewChangeRecommendations.mode" value="modified_agreed" <input type="radio" name="viewChangeRecommendations.mode" value="modified_agreed"
ng-model="viewChangeRecommendations.mode" ng-model="viewChangeRecommendations.mode"
ng-checked="viewChangeRecommendations.mode == 'modified_agreed'"> ng-checked="viewChangeRecommendations.mode == 'modified_agreed'">
<translate>Modified final version</translate> <translate>Final print template</translate>
</label> </label>
</div> </div>
@ -141,6 +142,11 @@
<i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'agreed'"></i> <i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'agreed'"></i>
<translate>Final version</translate> <translate>Final version</translate>
</a> </a>
<li os-perms="motions.can_manage">
<a href="" ng-click="viewChangeRecommendations.mode = 'modified_agreed'">
<i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'modified_agreed'"></i>
<translate>Final print template</translate>
</a>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,17 +1,17 @@
<!-- Modified agreed view --> <!-- Modified agreed view -->
<div ng-if="viewChangeRecommendations.mode == 'modified_agreed'"> <div ng-if="viewChangeRecommendations.mode == 'modified_agreed'">
<div style="text-align: right;">
<button class="btn btn-default btn-danger btn-sm"
ng-bootbox-confirm="{{ 'Do you want to delete the final print template?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.deleteModifiedFinalVersion(motion, version)">
<i class="fa fa-trash"></i>
<translate>Delete final print template</translate>
</button>
</div>
<div id="view-modified-agreed-inline-editor" ng-bind-html="motion.getModifiedFinalVersionWithLineBreaks(version) | trusted" <div id="view-modified-agreed-inline-editor" ng-bind-html="motion.getModifiedFinalVersionWithLineBreaks(version) | trusted"
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}" class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"
contenteditable="{{ modifiedFinalVersionInlineEditing.isEditable }}"> contenteditable="{{ modifiedFinalVersionInlineEditing.isEditable }}">
</div> </div>
<div style="text-align: right;">
<button class="btn btn-default btn-danger"
ng-bootbox-confirm="{{ 'Do you want to delete the modified final version?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.deleteModifiedFinalVersion(motion, version)">
<i class="fa fa-trash"></i>
<translate>Delete modified final version</translate>
</button>
</div>
<div class="motion-save-toolbar" ng-class="{ 'visible': modifiedFinalVersionInlineEditing.active && modifiedFinalVersionInlineEditing.changed }"> <div class="motion-save-toolbar" ng-class="{ 'visible': modifiedFinalVersionInlineEditing.active && modifiedFinalVersionInlineEditing.changed }">
<div class="changed-hint" translate>The modified final version have been changed.</div> <div class="changed-hint" translate>The modified final version have been changed.</div>
<button type="button" ng-click="modifiedFinalVersionInlineEditing.save()" class="btn btn-primary" translate> <button type="button" ng-click="modifiedFinalVersionInlineEditing.save()" class="btn btn-primary" translate>

View File

@ -31,7 +31,7 @@
<td ng-init="voteYes = poll.getVote(poll.yes, 'yes')"> <td ng-init="voteYes = poll.getVote(poll.yes, 'yes')">
<span class="result_label"><translate>Yes</translate>:</span> <span class="result_label"><translate>Yes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteYes.value }} {{ voteYes.percentStr }} {{ voteYes.value | number:getPollVotesPrecision(poll) }} {{ voteYes.percentStr }}
</span> </span>
<div ng-if="voteYes.percentNumber"> <div ng-if="voteYes.percentNumber">
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar> <uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
@ -43,7 +43,7 @@
<td ng-init="voteNo = poll.getVote(poll.no, 'no')"> <td ng-init="voteNo = poll.getVote(poll.no, 'no')">
<span class="result_label"><translate>No</translate>:</span> <span class="result_label"><translate>No</translate>:</span>
<span class="result_value" > <span class="result_value" >
{{ voteNo.value }} {{ voteNo.percentStr }} {{ voteNo.value | number:getPollVotesPrecision(poll) }} {{ voteNo.percentStr }}
</span> </span>
<div ng-if="voteNo.percentNumber"> <div ng-if="voteNo.percentNumber">
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar> <uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
@ -55,7 +55,7 @@
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')"> <td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
<span class="result_label"><translate>Abstain</translate>:</span> <span class="result_label"><translate>Abstain</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteAbstain.value }} {{ voteAbstain.percentStr }} {{ voteAbstain.value | number:getPollVotesPrecision(poll) }} {{ voteAbstain.percentStr }}
</span> </span>
<div ng-if="voteAbstain.percentNumber"> <div ng-if="voteAbstain.percentNumber">
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar> <uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>

View File

@ -24,22 +24,9 @@
</h1> </h1>
</div> </div>
<div class="title"> <div class="title">
<h3 ng-mouseover="firstStateHover=true" ng-mouseleave="firstStateHover=false"> <h3>
<translate>First state</translate>: <translate>First state</translate>:
{{ workflow.getFirstState().name | translate }} {{ workflow.getFirstState().name | translate }}
<span uib-dropdown>
<span id="firstStateDropdown" class="pointer" uib-dropdown-toggle>
<i class="fa fa-cog" ng-if="firstStateHover"></i>
</span>
<ul class="dropdown-menu" aria-labelledby="firstStateDropdown">
<li ng-repeat="state in workflow.states">
<a href ng-click="setFirstState(state)">
<i class="fa fa-check" ng-if="workflow.first_state === state.id"></i>
{{ state.name | translate }}
</a>
</li>
</ul>
</span>
</h3> </h3>
</div> </div>
</div> </div>
@ -53,7 +40,7 @@
<tr> <tr>
<th class="info-head small"> <th class="info-head small">
<h4 translate>Permissions</h4> <h4 translate>Permissions</h4>
<th ng-repeat="state in workflow.states" ng-mouseover="thHover=true" ng-mouseleave="thHover=false"> <th ng-repeat="state in states" ng-mouseover="thHover=true" ng-mouseleave="thHover=false">
<span class="optional"> <span class="optional">
{{ state.name | translate }} {{ state.name | translate }}
</span> </span>
@ -67,7 +54,7 @@
<i class="fa fa-pencil fa-lg"></i></a> <i class="fa fa-pencil fa-lg"></i></a>
&nbsp; &nbsp;
<!--delete--> <!--delete-->
<a href="" class="text-danger" ng-if="state.id !== workflow.first_state" <a href="" class="text-danger" ng-if="state.id !== workflow.first_state_id"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br> ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ state.name | translate }}</b>" <b>{{ state.name | translate }}</b>"
ng-bootbox-confirm-action="delete(state)"> ng-bootbox-confirm-action="delete(state)">
@ -82,7 +69,7 @@
<td> <td>
<b translate>Action word</b> <b translate>Action word</b>
</td> </td>
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false"> <td ng-repeat="state in states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
<div class="popover-wrapper"> <div class="popover-wrapper">
<span editable-text="state.newActionWord" <span editable-text="state.newActionWord"
onaftersave="setMember(state, 'action_word', state.newActionWord)"> onaftersave="setMember(state, 'action_word', state.newActionWord)">
@ -101,7 +88,7 @@
<td> <td>
<b translate>Recommendation label</b> <b translate>Recommendation label</b>
</td> </td>
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false"> <td ng-repeat="state in states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
<div class="popover-wrapper"> <div class="popover-wrapper">
<span editable-text="state.newRecommendationLabel" <span editable-text="state.newRecommendationLabel"
onaftersave="setMember(state, 'recommendation_label', state.newRecommendationLabel)"> onaftersave="setMember(state, 'recommendation_label', state.newRecommendationLabel)">
@ -120,7 +107,7 @@
<td> <td>
<b>{{ member.displayName | translate }}</b> <b>{{ member.displayName | translate }}</b>
</td> </td>
<td ng-repeat="state in workflow.states" class="pointer" <td ng-repeat="state in states" class="pointer"
ng-click="changeBooleanMember(state, member.name)"> ng-click="changeBooleanMember(state, member.name)">
<!-- Simulating a checkbox with FontAwesome icons. --> <!-- Simulating a checkbox with FontAwesome icons. -->
<i class="fa" <i class="fa"
@ -131,7 +118,7 @@
<td> <td>
<b translate>Label color</b> <b translate>Label color</b>
</td> </td>
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false"> <td ng-repeat="state in states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
<span uib-dropdown> <span uib-dropdown>
<span id="dropdownCssClass{{ state.id }}" class="pointer" uib-dropdown-toggle> <span id="dropdownCssClass{{ state.id }}" class="pointer" uib-dropdown-toggle>
<span class="label" ng-class="'label-' + state.css_class"> <span class="label" ng-class="'label-' + state.css_class">
@ -154,7 +141,7 @@
<td> <td>
<b translate>Required permission to see</b> <b translate>Required permission to see</b>
</td> </td>
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false"> <td ng-repeat="state in states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
<span uib-dropdown> <span uib-dropdown>
<span id="dropdownPermission{{ state.id }}" class="pointer" uib-dropdown-toggle> <span id="dropdownPermission{{ state.id }}" class="pointer" uib-dropdown-toggle>
<div class="no-overflow"> <div class="no-overflow">
@ -182,7 +169,7 @@
<td> <td>
<b translate>Next states</b> <b translate>Next states</b>
</td> </td>
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false"> <td ng-repeat="state in states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
<span ng-if="state.getNextStates().length === 0" class="text-muted"> <span ng-if="state.getNextStates().length === 0" class="text-muted">
&mdash; &mdash;
</span> </span>
@ -197,7 +184,7 @@
<i class="fa fa-cog" ng-if="tdHover"></i> <i class="fa fa-cog" ng-if="tdHover"></i>
</span> </span>
<ul class="dropdown-menu" aria-labelledby="dropdownNextStates{{ state.id }}"> <ul class="dropdown-menu" aria-labelledby="dropdownNextStates{{ state.id }}">
<li ng-repeat="s in workflow.states"> <li ng-repeat="s in states">
<a href ng-click="clickNextStateEntry(state, s.id)"> <a href ng-click="clickNextStateEntry(state, s.id)">
<i class="fa fa-check" ng-if="state.next_states_id.indexOf(s.id) > -1"></i> <i class="fa fa-check" ng-if="state.next_states_id.indexOf(s.id) > -1"></i>
{{ s.name | translate }} {{ s.name | translate }}

View File

@ -918,13 +918,6 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
result = False result = False
return result return result
def create(self, *args, **kwargs):
try:
response = super().create(*args, **kwargs)
except WorkflowError as e:
raise ValidationError({'detail': e.args[0]})
return response
def destroy(self, *args, **kwargs): def destroy(self, *args, **kwargs):
""" """
Customized view endpoint to delete a motion poll. Customized view endpoint to delete a motion poll.

View File

@ -1,12 +1,12 @@
import locale import locale
from decimal import Decimal
from typing import Optional, Type from typing import Optional, Type
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openslides.utils.models import MinMaxIntegerField
class BaseOption(models.Model): class BaseOption(models.Model):
""" """
@ -44,7 +44,8 @@ class BaseVote(models.Model):
Subclasses have to define an option field. This must be a ForeignKeyField Subclasses have to define an option field. This must be a ForeignKeyField
to a subclass of BasePoll. to a subclass of BasePoll.
""" """
weight = models.IntegerField(default=1, null=True) # Use MinMaxIntegerField weight = models.DecimalField(default=Decimal('1'), null=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
value = models.CharField(max_length=255, null=True) value = models.CharField(max_length=255, null=True)
class Meta: class Meta:
@ -72,9 +73,12 @@ class CollectDefaultVotesMixin(models.Model):
Mixin for a poll to collect the default vote values for valid votes, Mixin for a poll to collect the default vote values for valid votes,
invalid votes and votes cast. invalid votes and votes cast.
""" """
votesvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2) votesvalid = models.DecimalField(null=True, blank=True, validators=[
votesinvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2) MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
votescast = MinMaxIntegerField(null=True, blank=True, min_value=-2) votesinvalid = models.DecimalField(null=True, blank=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
votescast = models.DecimalField(null=True, blank=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
class Meta: class Meta:
abstract = True abstract = True

View File

@ -185,7 +185,8 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'ErrorMessage', 'ErrorMessage',
function($scope, $state, Topic, TopicForm, Agenda, Config, ErrorMessage) { function($scope, $state, Topic, TopicForm, Agenda, Config, ErrorMessage) {
$scope.model = { $scope.model = {
agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value), agenda_type: 1, // Default is a public item. The config field
// 'agenda_new_items_default_visibility' is not used.
}; };
// get all form fields // get all form fields
$scope.formFields = TopicForm.getFormFields(true); $scope.formFields = TopicForm.getFormFields(true);

View File

@ -1810,7 +1810,7 @@ angular.module('OpenSlidesApp.users.site', [
gettext('Can see agenda'); gettext('Can see agenda');
gettext('Can manage agenda'); gettext('Can manage agenda');
gettext('Can manage list of speakers'); gettext('Can manage list of speakers');
gettext('Can see hidden items and time scheduling of agenda'); gettext('Can see internal items and time scheduling of agenda');
gettext('Can put oneself on the list of speakers'); gettext('Can put oneself on the list of speakers');
// assignments // assignments
gettext('Can see elections'); gettext('Can see elections');

View File

@ -34,10 +34,11 @@
</div> </div>
</div> </div>
<div class="spacer-top"> <div class="spacer-top">
<translate>Initial password</translate>: {{ user.default_password }}
<span uib-tooltip="{{ 'Initial password can not be changed.' | translate }}"> <span uib-tooltip="{{ 'Initial password can not be changed.' | translate }}">
<i class="fa fa-info-circle"></i> <i class="fa fa-info-circle"></i>
<translate>Initial password</translate>: {{ user.default_password }} </span><br>
</span> <translate>Username</translate>: {{ user.username }}
<span class="pull-right spacer-right pointer" ng-click="showPassword = !showPassword"> <span class="pull-right spacer-right pointer" ng-click="showPassword = !showPassword">
<translate>Show password</translate> <translate>Show password</translate>
<i class="fa" ng-class="showPassword ? 'fa-check-square-o' : 'fa-square-o'"></i> <i class="fa" ng-class="showPassword ? 'fa-check-square-o' : 'fa-square-o'"></i>

View File

@ -17,6 +17,7 @@ from rest_framework.response import Response
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ( from rest_framework.serializers import (
CharField, CharField,
DecimalField,
DictField, DictField,
Field, Field,
FileField, FileField,
@ -43,9 +44,9 @@ from .auth import user_to_collection_user
from .collection import Collection, CollectionElement from .collection import Collection, CollectionElement
__all__ = ['status', 'detail_route', 'list_route', 'SimpleMetadata', 'CreateModelMixin', __all__ = ['detail_route', 'DecimalField', 'list_route', 'SimpleMetadata', 'CreateModelMixin',
'DestroyModelMixin', 'UpdateModelMixin', 'CharField', 'DictField', 'FileField', 'DestroyModelMixin', 'UpdateModelMixin', 'CharField', 'DictField', 'FileField',
'IntegerField', 'JSONField', 'ListField', 'ListSerializer', 'RelatedField', 'IntegerField', 'JSONField', 'ListField', 'ListSerializer', 'status', 'RelatedField',
'SerializerMethodField', 'ValidationError'] 'SerializerMethodField', 'ValidationError']

View File

@ -1161,21 +1161,6 @@ class CreateWorkflow(TestCase):
first_state = workflow.first_state first_state = workflow.first_state
self.assertEqual(type(first_state), State) self.assertEqual(type(first_state), State)
def test_creation_with_wrong_first_state(self):
response = self.client.post(
reverse('workflow-list'),
{'name': 'test_name_OoCoo3MeiT9li5Iengu9',
'first_state': 1})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_creation_with_not_existing_first_state(self):
Workflow.objects.all().delete()
response = self.client.post(
reverse('workflow-list'),
{'name': 'test_name_OoCoo3MeiT9li5Iengu9',
'first_state': 49})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
class UpdateWorkflow(TestCase): class UpdateWorkflow(TestCase):
""" """
@ -1195,38 +1180,6 @@ class UpdateWorkflow(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(workflow.name, 'test_name_wofi38DiWLT"8d3lwfo3') self.assertEqual(workflow.name, 'test_name_wofi38DiWLT"8d3lwfo3')
def test_change_first_state_correct(self):
first_state = self.workflow.first_state
other_workflow_state = self.workflow.states.exclude(pk=first_state.pk).first()
response = self.client.patch(
reverse('workflow-detail', args=[self.workflow.pk]),
{'first_state': other_workflow_state.pk})
workflow = Workflow.objects.get(pk=self.workflow.id)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(workflow.first_state, other_workflow_state)
def test_change_first_state_not_existing(self):
first_state = self.workflow.first_state
response = self.client.patch(
reverse('workflow-detail', args=[self.workflow.pk]),
{'first_state': 42})
workflow = Workflow.objects.get(pk=self.workflow.id)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(workflow.first_state, first_state)
def test_change_first_state_wrong_workflow(self):
first_state = self.workflow.first_state
other_workflow = Workflow.objects.exclude(pk=self.workflow.pk).first()
response = self.client.patch(
reverse('workflow-detail', args=[self.workflow.pk]),
{'first_state': other_workflow.first_state.pk})
workflow = Workflow.objects.get(pk=self.workflow.id)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(workflow.first_state, first_state)
class DeleteWorkflow(TestCase): class DeleteWorkflow(TestCase):
""" """