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].
- Update to channels2 [#3796].
- Drop Python 3.5 support[#3805].
- Adds a websocket protocol [#3807].
Version 2.3 (unreleased)
@ -19,24 +20,28 @@ Version 2.3 (unreleased)
Agenda:
- New item type 'hidden'. New visibilty filter in agenda [#3790].
Elections:
- Support to change decimal places for elections with a plugin [#3803]
Motions:
- New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647].
- New representation of amendments (paragraph based creation, new diff
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 table of contents with page numbers and categories in PDF [#3766].
- New teporal field "modified final version" where the final version can
be edited [#3781].
- New config to show amendments also in motions table [#3792]
- New config to show amendments also in motions table [#3792].
- Support to change decimal places for polls with a plugin [#3803].
Core:
- Python 3.4 is not supported anymore [#3777].
- Support Python 3.7 [#3786].
- Updated pdfMake to 0.1.37 [#3766].
- 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)

View File

@ -47,7 +47,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
return {key: full_data[key] for key in whitelist}
# 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'):
# Managers with special permission can see everything.
data = full_data
@ -62,6 +62,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
# 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.
# 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((
'id',
'title',
@ -83,7 +84,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
if full['is_hidden'] and can_see_hidden:
# Same filtering for internal and hidden items
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
if full['is_internal']:
elif full['is_internal']:
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
else: # agenda item
data.append(filtered_data(full, blocked_keys_non_internal_hidden_case))

View File

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

View File

@ -883,7 +883,7 @@ angular.module('OpenSlidesApp.agenda.site', [
gettext('[Begin speech] starts the countdown, [End speech] stops the ' +
'countdown.');
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 decimal import Decimal
from typing import Any, Dict, List
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MinValueValidator
from django.db import models
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.exceptions import OpenSlidesError
from openslides.utils.models import MinMaxIntegerField, RESTModelMixin
from openslides.utils.models import RESTModelMixin
from .access_permissions import AssignmentAccessPermissions
@ -422,9 +424,11 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
max_length=79,
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' """
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' """
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.utils.rest_api import (
DecimalField,
DictField,
IntegerField,
ListField,
@ -98,7 +99,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
options = AssignmentOptionSerializer(many=True, read_only=True)
votes = ListField(
child=DictField(
child=IntegerField(min_value=-2)),
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)),
write_only=True,
required=False)
has_votes = SerializerMethodField()

View File

@ -14,6 +14,12 @@ angular.module('OpenSlidesApp.assignments', [])
return DS.defineResource({
name: 'assignments/polloption',
useClass: jsDataModel,
// Change the stringified numbers to floats.
beforeInject: function (resource, instance) {
_.forEach(instance.votes, function (vote) {
vote.weight = parseFloat(vote.weight);
});
},
methods: {
getVotes: function () {
if (!this.poll.has_votes) {
@ -154,6 +160,15 @@ angular.module('OpenSlidesApp.assignments', [])
return DS.defineResource({
name: name,
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: {
getResourceName: function () {
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', [
'DS',
function (DS) {

View File

@ -9,7 +9,8 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
'HTMLValidizer',
'gettextCatalog',
'PDFLayout',
function($filter, HTMLValidizer, gettextCatalog, PDFLayout) {
'AssignmentPollDecimalPlaces',
function($filter, HTMLValidizer, gettextCatalog, PDFLayout, AssignmentPollDecimalPlaces) {
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
var parseVoteValue = function(voteObject, printLabel) {
var parseVoteValue = function(voteObject, printLabel, precision) {
var voteVal = '';
if (voteObject) {
if (printLabel) {
voteVal += voteObject.label + ': ';
}
voteVal += voteObject.value;
voteVal += $filter('number')(voteObject.value, precision);
if (voteObject.percentStr) {
voteVal += ' ' + voteObject.percentStr;
@ -135,6 +136,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
_.forEach(assignment.polls, function(poll, pollIndex) {
if (poll.published) {
var pollTableBody = [];
var precision = AssignmentPollDecimalPlaces.getPlaces(poll);
resultBody.push({
text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1),
@ -163,14 +165,14 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
if (poll.pollmethod == 'votes') {
tableLine.push(
{
text: parseVoteValue(votes[0], false),
text: parseVoteValue(votes[0], false, precision),
style: PDFLayout.flipTableRowStyle(pollTableBody.length)
}
);
} else {
var resultBlock = [];
_.forEach(votes, function(vote) {
resultBlock.push(parseVoteValue(vote, true));
resultBlock.push(parseVoteValue(vote, true, precision));
});
tableLine.push({
text: resultBlock,
@ -189,7 +191,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
style: 'tableConclude'
},
{
text: parseVoteValue(poll.getVote(fieldName), false),
text: parseVoteValue(poll.getVote(fieldName), false, precision),
style: 'tableConclude'
},
]);

View File

@ -16,15 +16,27 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment
.controller('SlideAssignmentCtrl', [
'$scope',
'Assignment',
'AssignmentPoll',
'AssignmentPhases',
'AssignmentPollDecimalPlaces',
'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.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
$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');
$scope.phases = AssignmentPhases;
User.bindAll({}, $scope, 'users');

View File

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

View File

@ -234,7 +234,7 @@
<div ng-init="votes = option.getVotes()">
<div ng-repeat="vote in votes">
<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">
<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>
@ -244,10 +244,10 @@
</div>
<td ng-hide="method === 'disabled'">
<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 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>
<!-- total votes (valid/invalid/casts) -->
@ -255,31 +255,31 @@
<td>
<translate>Abstain</translate>
<td>
{{ poll.getVote('votesabstain').value }}
{{ poll.getVote('votesabstain').value | number:votesPrecision }}
{{ poll.getVote('votesabstain').percentStr }}
<tr ng-if="poll.pollmethod === 'votes'">
<td>
<translate>No</translate>
<td>
{{ poll.getVote('votesno').value }}
{{ poll.getVote('votesno').value | number:votesPrecision }}
{{ poll.getVote('votesno').percentStr }}
<tr>
<td>
<translate>Valid ballots</translate>
<td>
{{ poll.getVote('votesvalid').value }}
{{ poll.getVote('votesvalid').value | number:votesPrecision }}
{{ poll.getVote('votesvalid').percentStr }}
<tr>
<td>
<translate>Invalid ballots</translate>
<td>
{{ poll.getVote('votesinvalid').value }}
{{ poll.getVote('votesinvalid').value | number:votesPrecision }}
{{ poll.getVote('votesinvalid').percentStr }}
<tr class="total bg-info">
<td>
<translate>Casted ballots</translate>
<td>
{{ poll.getVote('votescast').value }}
{{ poll.getVote('votescast').value | number:votesPrecision }}
{{ poll.getVote('votescast').percentStr }}
</table>

View File

@ -49,16 +49,19 @@
<div ng-init="votes = option.getVotes()">
<div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">
<span ng-show="poll.pollmethod == 'yna'">
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}<br>
{{ votes[2].label | translate }}: {{ votes[2].value }} {{ votes[2].percentStr }}</span>
{{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}<br>
{{ votes[2].label | translate }}: {{ votes[2].value | number:votesPrecision }} {{ votes[2].percentStr }}</span>
<span ng-show="poll.pollmethod == 'yn'">
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}</span>
{{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}</span>
</div>
<div ng-show="poll.pollmethod == '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>
@ -68,29 +71,29 @@
<td>
<translate>Abstain</translate>
<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">
<td>
<translate>No</translate>
<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">
<td>
<translate>Valid ballots</translate>
<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">
<td>
<translate>Invalid ballots</translate>
<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">
<td>
<translate>Casted ballots</translate>
<td ng-init="vote = poll.getVote('votescast')">
{{ vote.value }} {{ vote.percentStr }}
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
</table>
</div>

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.contrib.staticfiles.management.commands.collectstatic import (
Command as CollectStatic,
)
from django.contrib.staticfiles.utils import matches_patterns
from django.core.management.base import CommandError
from django.db.utils import OperationalError
@ -19,6 +20,8 @@ class Command(CollectStatic):
js_filename = 'webclient-{}.js'
def handle(self, **options: Any) -> str:
if options['link']:
raise CommandError("Option 'link' is not supported.")
try:
self.view = WebclientJavaScriptView()
except OperationalError:
@ -27,24 +30,37 @@ class Command(CollectStatic):
return super().handle(**options)
def collect(self) -> Dict[str, Any]:
result = super().collect()
try:
destination_dir = os.path.join(settings.STATICFILES_DIRS[0], 'js')
destination_dir = os.path.join(settings.STATIC_ROOT, 'js')
except IndexError:
# If the user does not want do have staticfiles, he should not get
# the webclient files either.
pass
else:
if not os.path.exists(destination_dir):
os.makedirs(destination_dir)
if self.dry_run:
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:
filename = self.js_filename.format(realm)
content = self.view.get(realm=realm).content
path = os.path.join(destination_dir, filename)
with open(path, 'wb+') as f:
f.write(content)
self.stdout.write("Written WebclientJavaScriptView for realm {} to '{}'".format(
realm,
path))
for realm in self.realms:
filename = self.js_filename.format(realm)
# Matches only the basename.
if matches_patterns(filename, self.ignore_patterns):
continue
path = os.path.join(destination_dir, filename)
if matches_patterns(path, self.ignore_patterns):
continue
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', ' '));
} else if (isInsideAList(element) && lineNumberMode === 'none') {
// 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;
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.utils.translation import ugettext as _
@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _
from ..poll.serializers import default_votes_validator
from ..utils.rest_api import (
CharField,
DecimalField,
DictField,
Field,
IntegerField,
@ -101,12 +102,11 @@ class WorkflowSerializer(ModelSerializer):
Serializer for motion.models.Workflow objects.
"""
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:
model = Workflow
fields = ('id', 'name', 'states', 'first_state',)
read_only_fields = ('first_state',)
@transaction.atomic
def create(self, validated_data):
@ -127,17 +127,6 @@ class WorkflowSerializer(ModelSerializer):
workflow.save()
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):
"""
@ -212,7 +201,7 @@ class MotionPollSerializer(ModelSerializer):
no = SerializerMethodField()
abstain = SerializerMethodField()
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)
has_votes = SerializerMethodField()
@ -238,21 +227,21 @@ class MotionPollSerializer(ModelSerializer):
def get_yes(self, obj):
try:
result = self.get_votes_dict(obj)['Yes']
result: Optional[str] = str(self.get_votes_dict(obj)['Yes'])
except KeyError:
result = None
return result
def get_no(self, obj):
try:
result = self.get_votes_dict(obj)['No']
result: Optional[str] = str(self.get_votes_dict(obj)['No'])
except KeyError:
result = None
return result
def get_abstain(self, obj):
try:
result = self.get_votes_dict(obj)['Abstain']
result: Optional[str] = str(self.get_votes_dict(obj)['Abstain'])
except KeyError:
result = None
return result

View File

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

View File

@ -54,7 +54,7 @@ angular.module('OpenSlidesApp.motions', [
name: 'motions/workflow',
methods: {
getFirstState: function () {
return DS.get('motions/state', this.first_state);
return DS.get('motions/state', this.first_state_id);
},
},
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: {
// Returns percent base. Returns undefined if calculation is not possible in general.
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', [
'DS',
'gettextCatalog',

View File

@ -17,9 +17,10 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'Config',
'Motion',
'MotionComment',
'MotionPollDecimalPlaces',
'OpenSlidesSettings',
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
* @constructor
@ -185,40 +186,41 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
column2.push('');
column3.push('');
}
var precision = MotionPollDecimalPlaces.getPlaces(poll);
// yes
var yes = poll.getVote(poll.yes, 'yes');
column1.push(gettextCatalog.getString('Yes') + ':');
column2.push(yes.value);
column2.push($filter('number')(yes.value, precision));
column3.push(yes.percentStr);
// no
var no = poll.getVote(poll.no, 'no');
column1.push(gettextCatalog.getString('No') + ':');
column2.push(no.value);
column2.push($filter('number')(no.value, precision));
column3.push(no.percentStr);
// abstain
var abstain = poll.getVote(poll.abstain, 'abstain');
column1.push(gettextCatalog.getString('Abstain') + ':');
column2.push(abstain.value);
column2.push($filter('number')(abstain.value, precision));
column3.push(abstain.percentStr);
// votes valid
if (poll.votesvalid) {
var valid = poll.getVote(poll.votesvalid, 'votesvalid');
column1.push(gettextCatalog.getString('Valid votes') + ':');
column2.push(valid.value);
column2.push($filter('number')(valid.value, precision));
column3.push(valid.percentStr);
}
// votes invalid
if (poll.votesvalid) {
var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid');
column1.push(gettextCatalog.getString('Invalid votes') + ':');
column2.push(invalid.value);
column2.push($filter('number')(invalid.value, precision));
column3.push(invalid.percentStr);
}
// votes cast
if (poll.votescast) {
var cast = poll.getVote(poll.votescast, 'votescast');
column1.push(gettextCatalog.getString('Votes cast') + ':');
column2.push(cast.value);
column2.push($filter('number')(cast.value, precision));
column3.push(cast.percentStr);
}
}

View File

@ -26,7 +26,9 @@ angular.module('OpenSlidesApp.motions.projector', [
'User',
'Notify',
'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.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
@ -67,8 +69,21 @@ angular.module('OpenSlidesApp.motions.projector', [
$scope.motion = Motion.get(motionId);
$scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
$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
$scope.viewChangeRecommendations = ChangeRecommendationView;
$scope.viewChangeRecommendations.initProjector($scope, Motion.get(motionId), $scope.mode);

View File

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

View File

@ -49,7 +49,9 @@ angular.module('OpenSlidesApp.motions.workflow', [])
return Workflow.lastModified(workflowId);
}, function () {
$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.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
if ($sessionStorage.motionStateTableExpandState) {
$scope.toggleExpandContent();

View File

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

View File

@ -103,13 +103,14 @@
ng-checked="viewChangeRecommendations.mode == 'agreed'">
<translate>Final version</translate>
</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-click="viewChangeRecommendations.mode = 'modified_agreed'">
<input type="radio" name="viewChangeRecommendations.mode" value="modified_agreed"
ng-model="viewChangeRecommendations.mode"
ng-checked="viewChangeRecommendations.mode == 'modified_agreed'">
<translate>Modified final version</translate>
<translate>Final print template</translate>
</label>
</div>
@ -141,6 +142,11 @@
<i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'agreed'"></i>
<translate>Final version</translate>
</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>
</div>
</div>

View File

@ -1,17 +1,17 @@
<!-- Modified agreed view -->
<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"
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"
contenteditable="{{ modifiedFinalVersionInlineEditing.isEditable }}">
</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="changed-hint" translate>The modified final version have been changed.</div>
<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')">
<span class="result_label"><translate>Yes</translate>:</span>
<span class="result_value">
{{ voteYes.value }} {{ voteYes.percentStr }}
{{ voteYes.value | number:getPollVotesPrecision(poll) }} {{ voteYes.percentStr }}
</span>
<div ng-if="voteYes.percentNumber">
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
@ -43,7 +43,7 @@
<td ng-init="voteNo = poll.getVote(poll.no, 'no')">
<span class="result_label"><translate>No</translate>:</span>
<span class="result_value" >
{{ voteNo.value }} {{ voteNo.percentStr }}
{{ voteNo.value | number:getPollVotesPrecision(poll) }} {{ voteNo.percentStr }}
</span>
<div ng-if="voteNo.percentNumber">
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
@ -55,7 +55,7 @@
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
<span class="result_label"><translate>Abstain</translate>:</span>
<span class="result_value">
{{ voteAbstain.value }} {{ voteAbstain.percentStr }}
{{ voteAbstain.value | number:getPollVotesPrecision(poll) }} {{ voteAbstain.percentStr }}
</span>
<div ng-if="voteAbstain.percentNumber">
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>

View File

@ -24,22 +24,9 @@
</h1>
</div>
<div class="title">
<h3 ng-mouseover="firstStateHover=true" ng-mouseleave="firstStateHover=false">
<h3>
<translate>First state</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>
</div>
</div>
@ -53,7 +40,7 @@
<tr>
<th class="info-head small">
<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">
{{ state.name | translate }}
</span>
@ -67,7 +54,7 @@
<i class="fa fa-pencil fa-lg"></i></a>
&nbsp;
<!--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>
<b>{{ state.name | translate }}</b>"
ng-bootbox-confirm-action="delete(state)">
@ -82,7 +69,7 @@
<td>
<b translate>Action word</b>
</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">
<span editable-text="state.newActionWord"
onaftersave="setMember(state, 'action_word', state.newActionWord)">
@ -101,7 +88,7 @@
<td>
<b translate>Recommendation label</b>
</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">
<span editable-text="state.newRecommendationLabel"
onaftersave="setMember(state, 'recommendation_label', state.newRecommendationLabel)">
@ -120,7 +107,7 @@
<td>
<b>{{ member.displayName | translate }}</b>
</td>
<td ng-repeat="state in workflow.states" class="pointer"
<td ng-repeat="state in states" class="pointer"
ng-click="changeBooleanMember(state, member.name)">
<!-- Simulating a checkbox with FontAwesome icons. -->
<i class="fa"
@ -131,7 +118,7 @@
<td>
<b translate>Label color</b>
</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 id="dropdownCssClass{{ state.id }}" class="pointer" uib-dropdown-toggle>
<span class="label" ng-class="'label-' + state.css_class">
@ -154,7 +141,7 @@
<td>
<b translate>Required permission to see</b>
</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 id="dropdownPermission{{ state.id }}" class="pointer" uib-dropdown-toggle>
<div class="no-overflow">
@ -182,7 +169,7 @@
<td>
<b translate>Next states</b>
</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">
&mdash;
</span>
@ -197,7 +184,7 @@
<i class="fa fa-cog" ng-if="tdHover"></i>
</span>
<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)">
<i class="fa fa-check" ng-if="state.next_states_id.indexOf(s.id) > -1"></i>
{{ s.name | translate }}

View File

@ -918,13 +918,6 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
result = False
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):
"""
Customized view endpoint to delete a motion poll.

View File

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

View File

@ -185,7 +185,8 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics', 'OpenSlides
'ErrorMessage',
function($scope, $state, Topic, TopicForm, Agenda, Config, ErrorMessage) {
$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
$scope.formFields = TopicForm.getFormFields(true);

View File

@ -1810,7 +1810,7 @@ angular.module('OpenSlidesApp.users.site', [
gettext('Can see agenda');
gettext('Can manage agenda');
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');
// assignments
gettext('Can see elections');

View File

@ -34,10 +34,11 @@
</div>
</div>
<div class="spacer-top">
<translate>Initial password</translate>: {{ user.default_password }}
<span uib-tooltip="{{ 'Initial password can not be changed.' | translate }}">
<i class="fa fa-info-circle"></i>
<translate>Initial password</translate>: {{ user.default_password }}
</span>
</span><br>
<translate>Username</translate>: {{ user.username }}
<span class="pull-right spacer-right pointer" ng-click="showPassword = !showPassword">
<translate>Show password</translate>
<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.serializers import (
CharField,
DecimalField,
DictField,
Field,
FileField,
@ -43,9 +44,9 @@ from .auth import user_to_collection_user
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',
'IntegerField', 'JSONField', 'ListField', 'ListSerializer', 'RelatedField',
'IntegerField', 'JSONField', 'ListField', 'ListSerializer', 'status', 'RelatedField',
'SerializerMethodField', 'ValidationError']

View File

@ -1161,21 +1161,6 @@ class CreateWorkflow(TestCase):
first_state = workflow.first_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):
"""
@ -1195,38 +1180,6 @@ class UpdateWorkflow(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
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):
"""