Added config for more verbose errors on reset password

- Added settings.py docs
- Fixed left-overs from #4920
- Reworked all server messages to a new argument formet, so that the
client can translate server messages
This commit is contained in:
FinnStutzenstein 2019-09-02 11:09:03 +02:00
parent a193cc1e3f
commit 9323bdef20
20 changed files with 469 additions and 184 deletions

View File

@ -115,8 +115,17 @@ Download the latest portable version of OpenSlides for Windows from
install steps. Simply unzip the downloaded file and run ``openslides.exe``. install steps. Simply unzip the downloaded file and run ``openslides.exe``.
Configuration
=============
Please consider reading the `OpenSlides configuration
<https://github.com/OpenSlides/OpenSlides/blob/master/SETTINGS.rst>`_ page to
find out about all configurations, especially when using OpenSlides for big
assemblies.
Using the Dockerfile Using the Dockerfile
=============================== ====================
You can either pull the image ``openslides/openslides`` or build it yourself You can either pull the image ``openslides/openslides`` or build it yourself
(via `docker build -t openslides/openslides .`). You have all prequistes installed (via `docker build -t openslides/openslides .`). You have all prequistes installed

122
SETTINGS.rst Normal file
View File

@ -0,0 +1,122 @@
==========================
OpenSlides configuration
==========================
First, locate your `settings.py`. Since this is a regular python file,
experienced users can also write more advanced configurations with e.g. swithing
between two sets of configs. This also means, that the syntax need to be correct
for OpenSlides to start.
All presented settings must be written `<SETTINGS_NAME> = <value>` to follow the
correct syntax.
The `settings.py` is just an extension for Django settings. Please visit the
`Django settings documentation
<https://docs.djangoproject.com/en/2.2/ref/settings/>`_ to get an overview about
all existing settings.
SECURITY
========
For `DEBUG` and `SECRET_KEY` see the sections in the django settings
documenataion.
`RESET_PASSWORD_VERBOSE_ERRORS`: Default: `True`. Set to `False` to disable.
Controls the verbosity on errors during a reset password. If enabled, an error
will be shown, if there does not exist a user with a given email address. So one
can check, if a email is registered. If this is not wanted, disable verbose
messages. An success message will always be shown.
`AUTH_PASSWORD_VALIDATORS`: Add custom password validators, e.g. a min-length
validator. See `django auth docs
<https://docs.djangoproject.com/en/2.2/topics/auth/passwords/#module-django.contrib.auth.password_validation>`_
for mor information.
Directories
===========
`OPENSLIDES_USER_DATA_DIR`: The path, where all user data is saved, like static
files, mediafiles and the default database. This path can be different to the
location of the `settings.py`.
`STATICFILES_DIRS` and `STATIC_ROOT`: Managing static files. Because the clint
is not delivered by the server anymore, these settings are obsolete.
`MEDIA_ROOT`: The location of mediafiles. The default is a `media` folder inside
`OPENSLIDES_USER_DATA_DIR`, but can be altered to another path.
Email
=====
Please refer to the Django settings documentation. A changed email backend is
useful for debugging to print all email the the console::
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Logging
=======
To setup basic logging see `logging
<https://docs.djangoproject.com/en/2.2/topics/logging/>`_.
We recommend to enable all OpenSlides related logging with level `INFO` per
default::
LOGGING = {
'formatters':
'lessnoise': {
'format': '[{levelname}] {name} {message}',
'style': '{',
'datefmt': '[%Y-%m-%d %H:%M:%S %z]',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'lessnoise',
},
},
'loggers': {
'openslides': {
'handlers': ['console'],
'level': os.getenv('OPENSLIDES_LOG_LEVEL', 'INFO'),
},
},
}
With the environment variable `OPENSLIDES_LOG_LEVEL` the level can be adjusted
without changing the `settings.py`.
Big mode and caching
====================
When running multiple workers redis is required as a message broker between the
workers. Set `use_redis = True` to enable redis and visit `OpenSLides in big
mode
<https://github.com/OpenSlides/OpenSlides/blob/master/DEVELOPMENT.rst#openslides-in-big-mode>`_.
When seting `use_redis = True`, three settings are important:
- Caching: `REDIS_ADDRESS` is used to provide caching with redis across all
workers.
- Channels: The "message queue" for the workers. Adjust the `hosts`-part to the
redis address.
- Sessions: All sessions are managed in redis to ensure them across all workers.
Please adjust the `SESSION_REDIS` fields to point to the redis instance.
Advanced
========
`PING_INTERVAL` and `PING_TIMEOUT` are settings for the clients how frequently
to ping the server (interval) and how big is the timeout. If a ping took longer
than the timeout, the clients does a forced reconnect.
`COMPRESSION`: Enable or disables the compression when sending data. This does
not affect the client.
`PRIORITIZED_GROUP_IDS`: A list of group ids. If one client is logged in and the
operator is in one of these groups, the client disconnected and reconnects again.
All requests urls (including websockets) are now prefixed with `/prioritize`, so
these requests from "prioritized clients" can be routed to different servers.

View File

@ -17,6 +17,20 @@ export enum HTTPMethod {
DELETE = 'delete' DELETE = 'delete'
} }
export interface DetailResponse {
detail: string | string[];
args?: string[];
}
function isDetailResponse(obj: any): obj is DetailResponse {
return (
obj &&
typeof obj === 'object' &&
(typeof obj.detail === 'string' || obj.detail instanceof Array) &&
(!obj.args || obj.args instanceof Array)
);
}
/** /**
* Service for managing HTTP requests. Allows to send data for every method. Also (TODO) will do generic error handling. * Service for managing HTTP requests. Allows to send data for every method. Also (TODO) will do generic error handling.
*/ */
@ -128,13 +142,13 @@ export class HttpService {
} else if (!e.error) { } else if (!e.error) {
error += this.translate.instant("The server didn't respond."); error += this.translate.instant("The server didn't respond.");
} else if (typeof e.error === 'object') { } else if (typeof e.error === 'object') {
if (e.error.detail) { if (isDetailResponse(e.error)) {
error += this.translate.instant(this.processErrorTexts(e.error.detail)); error += this.processDetailResponse(e.error);
} else { } else {
error = Object.keys(e.error) error = Object.keys(e.error)
.map(key => { .map(key => {
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1); const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
return this.translate.instant(capitalizedKey) + ': ' + this.processErrorTexts(e.error[key]); return this.translate.instant(capitalizedKey) + ': ' + this.processDetailResponse(e.error[key]);
}) })
.join(', '); .join(', ');
} }
@ -155,12 +169,21 @@ export class HttpService {
* @param str a string or a string array to join together. * @param str a string or a string array to join together.
* @returns Error text(s) as single string * @returns Error text(s) as single string
*/ */
private processErrorTexts(str: string | string[]): string { private processDetailResponse(response: DetailResponse): string {
if (str instanceof Array) { let message: string;
return str.join(' '); if (response.detail instanceof Array) {
message = response.detail.join(' ');
} else { } else {
return str; message = response.detail;
} }
message = this.translate.instant(message);
if (response.args && response.args.length > 0) {
for (let i = 0; i < response.args.length; i++) {
message = message.replace(`{${i}}`, response.args[i].toString());
}
}
return message;
} }
/** /**

View File

@ -64,7 +64,8 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
except Item.DoesNotExist: except Item.DoesNotExist:
raise ValidationError( raise ValidationError(
{ {
"detail": f"The parent item with id {parent_id} does not exist" "detail": "The parent item with id {0} does not exist",
"args": [parent_id],
} }
) )
attrs["weight"] = parent.weight + 1 attrs["weight"] = parent.weight + 1

View File

@ -86,7 +86,7 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
try: try:
model = get_model_from_collection_string(collection) model = get_model_from_collection_string(collection)
except ValueError: except ValueError:
raise ValidationError("Invalid collection") raise ValidationError({"detail": "Invalid collection"})
try: try:
content_object = model.objects.get(pk=id) content_object = model.objects.get(pk=id)
@ -220,7 +220,10 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
parent = Item.objects.get(pk=request.data["parent_id"]) parent = Item.objects.get(pk=request.data["parent_id"])
except Item.DoesNotExist: except Item.DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"Parent item {request.data['parent_id']} does not exist"} {
"detail": "Parent item {0} does not exist",
"args": [request.data["parent_id"]],
}
) )
# Collect ancestors # Collect ancestors
@ -237,7 +240,8 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
if item_id in ancestors: if item_id in ancestors:
raise ValidationError( raise ValidationError(
{ {
"detail": f"Assigning item {item_id} to one of its children is not possible." "detail": "Assigning item {0} to one of its children is not possible.",
"args": [item_id],
} }
) )
@ -245,7 +249,9 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
try: try:
items.append(Item.objects.get(pk=item_id)) items.append(Item.objects.get(pk=item_id))
except Item.DoesNotExist: except Item.DoesNotExist:
raise ValidationError({"detail": f"Item {item_id} does not exist"}) raise ValidationError(
{"detail": "Item {0} does not exist", "args": [item_id]}
)
# OK, assign new parents. # OK, assign new parents.
for item in items: for item in items:
@ -257,7 +263,9 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
inform_changed_data(items) inform_changed_data(items)
# Send response. # Send response.
return Response({"detail": f"{len(items)} items successfully assigned."}) return Response(
{"detail": "{0} items successfully assigned.", "args": [len(items)]}
)
class ListOfSpeakersViewSet( class ListOfSpeakersViewSet(
@ -464,7 +472,8 @@ class ListOfSpeakersViewSet(
except Speaker.DoesNotExist: except Speaker.DoesNotExist:
raise ValidationError( raise ValidationError(
{ {
"detail": f"There is no one speaking at the moment according to {list_of_speakers}." "detail": "There is no one speaking at the moment according to {0}.",
"args": [list_of_speakers],
} }
) )
current_speaker.end_speech() current_speaker.end_speech()

View File

@ -147,20 +147,25 @@ class AssignmentAllPollSerializer(ModelSerializer):
if len(votes) != len(options): if len(votes) != len(options):
raise ValidationError( raise ValidationError(
{ {
"detail": f"You have to submit data for {len(options)} candidates." "detail": "You have to submit data for {0} candidates.",
"args": [len(options)],
} }
) )
for index, option in enumerate(options): for index, option in enumerate(options):
if len(votes[index]) != len(instance.get_vote_values()): if len(votes[index]) != len(instance.get_vote_values()):
raise ValidationError( raise ValidationError(
{ {
"detail": f"You have to submit data for {len(instance.get_vote_values())} vote values." "detail": "You have to submit data for {0} vote values",
"args": [len(instance.get_vote_values())],
} }
) )
for vote_value, __ in votes[index].items(): for vote_value, __ in votes[index].items():
if vote_value not in instance.get_vote_values(): if vote_value not in instance.get_vote_values():
raise ValidationError( raise ValidationError(
{"detail": f"Vote value {vote_value} is invalid."} {
"detail": "Vote value {0} is invalid.",
"args": [vote_value],
}
) )
instance.set_vote_objects_with_values( instance.set_vote_objects_with_values(
option, votes[index], skip_autoupdate=True option, votes[index], skip_autoupdate=True

View File

@ -131,8 +131,12 @@ class AssignmentViewSet(ModelViewSet):
self.mark_elected can play with it. self.mark_elected can play with it.
""" """
if not isinstance(request.data, dict): if not isinstance(request.data, dict):
detail = f"Invalid data. Expected dictionary, got {type(request.data)}." raise ValidationError(
raise ValidationError({"detail": detail}) {
"detail": "Invalid data. Expected dictionary, got {0}.",
"args": [type(request.data)],
}
)
user_str = request.data.get("user", "") user_str = request.data.get("user", "")
try: try:
user_pk = int(user_str) user_pk = int(user_str)
@ -144,7 +148,7 @@ class AssignmentViewSet(ModelViewSet):
user = get_user_model().objects.get(pk=user_pk) user = get_user_model().objects.get(pk=user_pk)
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"Invalid data. User {user_pk} does not exist."} {"detail": "Invalid data. User {0} does not exist.", "args": [user_pk]}
) )
return user return user
@ -157,46 +161,60 @@ class AssignmentViewSet(ModelViewSet):
user = self.get_user_from_request_data(request) user = self.get_user_from_request_data(request)
assignment = self.get_object() assignment = self.get_object()
if request.method == "POST": if request.method == "POST":
message = self.nominate_other(request, user, assignment) return self.nominate_other(request, user, assignment)
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
message = self.delete_other(request, user, assignment) return self.delete_other(request, user, assignment)
return Response({"detail": message})
def nominate_other(self, request, user, assignment): def nominate_other(self, request, user, assignment):
if assignment.is_elected(user): if assignment.is_elected(user):
raise ValidationError({"detail": f"User {user} is already elected."}) raise ValidationError(
if assignment.phase == assignment.PHASE_FINISHED: {"detail": "User {0} is already elected.", "args": [str(user)]}
detail = ( )
"You can not nominate someone to this election because it is finished." if assignment.phase == assignment.PHASE_FINISHED:
raise ValidationError(
{
"detail": "You can not nominate someone to this election because it is finished."
}
) )
raise ValidationError({"detail": detail})
if assignment.phase == assignment.PHASE_VOTING and not has_perm( if assignment.phase == assignment.PHASE_VOTING and not has_perm(
request.user, "assignments.can_manage" request.user, "assignments.can_manage"
): ):
# To nominate another user during voting you have to be a manager. # To nominate another user during voting you have to be a manager.
self.permission_denied(request) self.permission_denied(request)
if assignment.is_candidate(user): if assignment.is_candidate(user):
raise ValidationError({"detail": f"User {user} is already nominated."}) raise ValidationError(
{"detail": "User {0} is already nominated.", "args": [str(user)]}
)
assignment.set_candidate(user) assignment.set_candidate(user)
# Send new candidate via autoupdate because users without permission # Send new candidate via autoupdate because users without permission
# to see users may not have it but can get it now. # to see users may not have it but can get it now.
inform_changed_data(user) inform_changed_data(user)
return f"User {user} was nominated successfully." return Response(
{"detail": "User {0} was nominated successfully.", "args": [str(user)]}
)
def delete_other(self, request, user, assignment): def delete_other(self, request, user, assignment):
# To delete candidature status you have to be a manager. # To delete candidature status you have to be a manager.
if not has_perm(request.user, "assignments.can_manage"): if not has_perm(request.user, "assignments.can_manage"):
self.permission_denied(request) self.permission_denied(request)
if assignment.phase == assignment.PHASE_FINISHED: if assignment.phase == assignment.PHASE_FINISHED:
detail = "You can not delete someone's candidature to this election because it is finished." raise ValidationError(
raise ValidationError({"detail": detail}) {
"detail": "You can not delete someone's candidature to this election because it is finished."
}
)
if not assignment.is_candidate(user) and not assignment.is_elected(user): if not assignment.is_candidate(user) and not assignment.is_elected(user):
raise ValidationError( raise ValidationError(
{"detail": f"User {user} has no status in this election."} {
"detail": "User {0} has no status in this election.",
"args": [str(user)],
}
) )
assignment.delete_related_user(user) assignment.delete_related_user(user)
return f"Candidate {user} was withdrawn successfully." return Response(
{"detail": "Candidate {0} was withdrawn successfully.", "args": [str(user)]}
)
@detail_route(methods=["post", "delete"]) @detail_route(methods=["post", "delete"])
def mark_elected(self, request, pk=None): def mark_elected(self, request, pk=None):
@ -209,18 +227,25 @@ class AssignmentViewSet(ModelViewSet):
if request.method == "POST": if request.method == "POST":
if not assignment.is_candidate(user): if not assignment.is_candidate(user):
raise ValidationError( raise ValidationError(
{"detail": f"User {user} is not a candidate of this election."} {
"detail": "User {0} is not a candidate of this election.",
"args": [str(user)],
}
) )
assignment.set_elected(user) assignment.set_elected(user)
message = f"User {user} was successfully elected." message = "User {0} was successfully elected."
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
if not assignment.is_elected(user): if not assignment.is_elected(user):
detail = f"User {user} is not an elected candidate of this election." raise ValidationError(
raise ValidationError({"detail": detail}) {
"detail": "User {0} is not an elected candidate of this election.",
"args": [str(user)],
}
)
assignment.set_candidate(user) assignment.set_candidate(user)
message = f"User {user} was successfully unelected." message = "User {0} was successfully unelected."
return Response({"detail": message}) return Response({"detail": message, "args": [str(user)]})
@detail_route(methods=["post"]) @detail_route(methods=["post"])
def create_poll(self, request, pk=None): def create_poll(self, request, pk=None):

View File

@ -62,7 +62,7 @@ def elements_validator(value: Any) -> None:
) )
if element["name"] not in projector_slides: if element["name"] not in projector_slides:
raise ValidationError( raise ValidationError(
{"detail": f"Unknown projector element {element['name']},"} {"detail": "Unknown projector element {0}.", "args": [element["name"]]}
) )

View File

@ -291,8 +291,9 @@ class ProjectorViewSet(ModelViewSet):
projector_instance.scroll = request.data projector_instance.scroll = request.data
projector_instance.save() projector_instance.save()
message = f"Setting scroll to {request.data} was successful." return Response(
return Response({"detail": message}) {"detail": "Setting scroll to {0} was successful.", "args": [request.data]}
)
class ProjectionDefaultViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): class ProjectionDefaultViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):

View File

@ -126,7 +126,7 @@ class MediafileViewSet(ModelViewSet):
mediafiles.append(Mediafile.objects.get(pk=id)) mediafiles.append(Mediafile.objects.get(pk=id))
except Mediafile.DoesNotExist: except Mediafile.DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"The mediafile with id {id} does not exist"} {"detail": "The mediafile with id {0} does not exist", "args": [id]}
) )
# Search for valid parents (None is not included, but also safe!) # Search for valid parents (None is not included, but also safe!)
@ -181,7 +181,7 @@ class MediafileViewSet(ModelViewSet):
mediafiles.append(Mediafile.objects.get(pk=id)) mediafiles.append(Mediafile.objects.get(pk=id))
except Mediafile.DoesNotExist: except Mediafile.DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"The mediafile with id {id} does not exist"} {"detail": "The mediafile with id {0} does not exist", "args": [id]}
) )
if not mediafiles: if not mediafiles:
return Response() return Response()

View File

@ -1,6 +1,5 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import IntegrityError, models, transaction from django.db import IntegrityError, models, transaction
from django.db.models import Max from django.db.models import Max
from jsonfield import JSONField from jsonfield import JSONField
@ -18,6 +17,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 RESTModelMixin from openslides.utils.models import RESTModelMixin
from openslides.utils.rest_api import ValidationError
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
from .access_permissions import ( from .access_permissions import (
@ -401,17 +401,9 @@ class Motion(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model):
Returns the number used in the set_identifier method with leading Returns the number used in the set_identifier method with leading
zero charaters according to the config value. zero charaters according to the config value.
""" """
result = str(number) return "0" * (config["motions_identifier_min_digits"] - len(str(number))) + str(
if config["motions_identifier_min_digits"]: number
if not isinstance(config["motions_identifier_min_digits"], int):
raise ImproperlyConfigured(
"Config value 'motions_identifier_min_digits' must be an integer."
) )
result = (
"0" * (config["motions_identifier_min_digits"] - len(str(number)))
+ result
)
return result
def is_submitter(self, user): def is_submitter(self, user):
""" """
@ -766,7 +758,10 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
if self.collides_with_other_recommendation(recommendations): if self.collides_with_other_recommendation(recommendations):
raise ValidationError( raise ValidationError(
f"The recommendation collides with an existing one (line {self.line_from} - {self.line_to})." {
"detail": "The recommendation collides with an existing one (line {0} - {1}).",
"args": [self.line_from, self.line_to],
}
) )
result = super().save(*args, **kwargs) result = super().save(*args, **kwargs)

View File

@ -1,7 +1,6 @@
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Model from django.db.models import Model
@ -41,11 +40,8 @@ def numbering(main_category: Category) -> List[Model]:
- Both counters may be filled with leading zeros according to `Motion.extend_identifier_number` - Both counters may be filled with leading zeros according to `Motion.extend_identifier_number`
- On errors, ValidationErrors with appropriate content will be raised. - On errors, ValidationErrors with appropriate content will be raised.
""" """
# If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier. # If the config is false, don't use blanks when building identifier.
without_blank = ( without_blank = not config["motions_identifier_with_blank"]
hasattr(settings, "MOTION_IDENTIFIER_WITHOUT_BLANKS")
and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS
)
# Get child categories (to build affected categories) and precalculate all prefixes. # Get child categories (to build affected categories) and precalculate all prefixes.
child_categories = get_child_categories(main_category) child_categories = get_child_categories(main_category)
@ -171,9 +167,10 @@ def get_amendment_level_mapping(
if motion.parent_id is not None and motion.parent_id not in affected_motion_ids: if motion.parent_id is not None and motion.parent_id not in affected_motion_ids:
raise ValidationError( raise ValidationError(
{ {
"detail": f'Amendment "{motion}" cannot be numbered, because ' "detail": 'Amendment "{0}" cannot be numbered, because '
f"it's lead motion ({motion.parent}) is not in category " "it's lead motion ({1}) is not in category "
f"{main_category} or any subcategory." "{2} or any subcategory.",
"args": [str(motion), str(motion.parent), str(main_category)],
} }
) )
return max_amendment_level, amendment_level_mapping return max_amendment_level, amendment_level_mapping
@ -234,17 +231,22 @@ def check_new_identifiers_for_conflicts(
# We do have a conflict. Build a nice error message. # We do have a conflict. Build a nice error message.
conflicting_motion = conflicting_motions.first() conflicting_motion = conflicting_motions.first()
if conflicting_motion.category: if conflicting_motion.category:
error_message = ( raise ValidationError(
"Numbering aborted because the motion identifier " {
f'"{conflicting_motion.identifier}" already exists in ' "detail": 'Numbering aborted because the motion identifier "{0}" already exists in category {1}.',
f"category {conflicting_motion.category}." "args": [
conflicting_motion.identifier,
str(conflicting_motion.category),
],
}
) )
else: else:
error_message = ( raise ValidationError(
"Numbering aborted because the motion identifier " {
f'"{conflicting_motion.identifier}" already exists.' "detail": 'Numbering aborted because the motion identifier "{0}" already exists.',
"args": [conflicting_motion.identifier],
}
) )
raise ValidationError({"detail": error_message})
def update_identifiers(affected_motions, new_identifier_mapping) -> List[Model]: def update_identifiers(affected_motions, new_identifier_mapping) -> List[Model]:

View File

@ -41,7 +41,9 @@ def validate_workflow_field(value):
Validator to ensure that the workflow with the given id exists. Validator to ensure that the workflow with the given id exists.
""" """
if not Workflow.objects.filter(pk=value).exists(): if not Workflow.objects.filter(pk=value).exists():
raise ValidationError({"detail": f"Workflow {value} does not exist."}) raise ValidationError(
{"detail": "Workflow {0} does not exist.", "args": [value]}
)
class StatuteParagraphSerializer(ModelSerializer): class StatuteParagraphSerializer(ModelSerializer):
@ -307,13 +309,14 @@ class MotionPollSerializer(ModelSerializer):
if len(votes) != len(instance.get_vote_values()): if len(votes) != len(instance.get_vote_values()):
raise ValidationError( raise ValidationError(
{ {
"detail": f"You have to submit data for {len(instance.get_vote_values())} vote values." "detail": "You have to submit data for {0} vote values.",
"args": [len(instance.get_vote_values())],
} }
) )
for vote_value in votes.keys(): for vote_value in votes.keys():
if vote_value not in instance.get_vote_values(): if vote_value not in instance.get_vote_values():
raise ValidationError( raise ValidationError(
{"detail": f"Vote value {vote_value} is invalid."} {"detail": "Vote value {0} is invalid.", "args": [vote_value]}
) )
instance.set_vote_objects_with_values( instance.set_vote_objects_with_values(
instance.get_options().get(), votes, skip_autoupdate=True instance.get_options().get(), votes, skip_autoupdate=True

View File

@ -2,7 +2,6 @@ from typing import List, Set
import jsonschema import jsonschema
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import transaction from django.db import transaction
from django.db.models import Case, When from django.db.models import Case, When
from django.db.models.deletion import ProtectedError from django.db.models.deletion import ProtectedError
@ -358,7 +357,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
section = MotionCommentSection.objects.get(pk=section_id) section = MotionCommentSection.objects.get(pk=section_id)
except MotionCommentSection.DoesNotExist: except MotionCommentSection.DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"A comment section with id {section_id} does not exist."} {
"detail": "A comment section with id {0} does not exist.",
"args": [section_id],
}
) )
# the request user needs to see and write to the comment section # the request user needs to see and write to the comment section
@ -448,7 +450,9 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
try: try:
motion = Motion.objects.get(pk=item["id"]) motion = Motion.objects.get(pk=item["id"])
except Motion.DoesNotExist: except Motion.DoesNotExist:
raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) raise ValidationError(
{"detail": "Motion {0} does not exist", "args": [item["id"]]}
)
# Remove all submitters. # Remove all submitters.
Submitter.objects.filter(motion=motion).delete() Submitter.objects.filter(motion=motion).delete()
@ -459,7 +463,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
submitter = get_user_model().objects.get(pk=submitter_id) submitter = get_user_model().objects.get(pk=submitter_id)
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"Submitter {submitter_id} does not exist"} {
"detail": "Submitter {0} does not exist",
"args": [submitter_id],
}
) )
Submitter.objects.add(submitter, motion) Submitter.objects.add(submitter, motion)
new_submitters.append(submitter) new_submitters.append(submitter)
@ -479,7 +486,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
# Send response. # Send response.
return Response( return Response(
{"detail": f"{len(motion_result)} motions successfully updated."} {
"detail": "{0} motions successfully updated.",
"args": [len(motion_result)],
}
) )
@detail_route(methods=["post", "delete"]) @detail_route(methods=["post", "delete"])
@ -566,7 +576,9 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
try: try:
motion = Motion.objects.get(pk=item["id"]) motion = Motion.objects.get(pk=item["id"])
except Motion.DoesNotExist: except Motion.DoesNotExist:
raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) raise ValidationError(
{"detail": "Motion {0} does not exist", "args": [item["id"]]}
)
# Get category # Get category
category = None category = None
@ -575,7 +587,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
category = Category.objects.get(pk=item["category"]) category = Category.objects.get(pk=item["category"])
except Category.DoesNotExist: except Category.DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"Category {item['category']} does not exist"} {
"detail": "Category {0} does not exist",
"args": [item["category"]],
}
) )
# Set category # Set category
@ -601,7 +616,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
# Send response. # Send response.
return Response( return Response(
{"detail": f"Category of {len(motion_result)} motions successfully set."} {
"detail": "Category of {0} motions successfully set.",
"args": [len(motion_result)],
}
) )
@list_route(methods=["post"]) @list_route(methods=["post"])
@ -645,7 +663,9 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
try: try:
motion = Motion.objects.get(pk=item["id"]) motion = Motion.objects.get(pk=item["id"])
except Motion.DoesNotExist: except Motion.DoesNotExist:
raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) raise ValidationError(
{"detail": "Motion {0} does not exist", "args": [item["id"]]}
)
# Get motion block # Get motion block
motion_block = None motion_block = None
@ -654,7 +674,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
motion_block = MotionBlock.objects.get(pk=item["motion_block"]) motion_block = MotionBlock.objects.get(pk=item["motion_block"])
except MotionBlock.DoesNotExist: except MotionBlock.DoesNotExist:
raise ValidationError( raise ValidationError(
{"detail": f"MotionBlock {item['motion_block']} does not exist"} {
"detail": "MotionBlock {0} does not exist",
"args": [item["motion_block"]],
}
) )
# Set motion bock # Set motion bock
@ -681,7 +704,8 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
# Send response. # Send response.
return Response( return Response(
{ {
"detail": f"Motion block of {len(motion_result)} motions successfully set." "detail": "Motion block of {0} motions successfully set.",
"args": [len(motion_result)],
} }
) )
@ -714,7 +738,7 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
) )
if not motion.state.is_next_or_previous_state_id(state_id): if not motion.state.is_next_or_previous_state_id(state_id):
raise ValidationError( raise ValidationError(
{"detail": f"You can not set the state to {state_id}."} {"detail": "You can not set the state to {0}.", "args": [state_id]}
) )
motion.set_state(state_id) motion.set_state(state_id)
else: else:
@ -786,7 +810,9 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
try: try:
motion = Motion.objects.get(pk=item["id"]) motion = Motion.objects.get(pk=item["id"])
except Motion.DoesNotExist: except Motion.DoesNotExist:
raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) raise ValidationError(
{"detail": "Motion {0} does not exist", "args": [item["id"]]}
)
# Set or reset state. # Set or reset state.
state_id = item["state"] state_id = item["state"]
@ -794,7 +820,7 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
if state_id not in [item.id for item in valid_states]: if state_id not in [item.id for item in valid_states]:
# States of different workflows are not allowed. # States of different workflows are not allowed.
raise ValidationError( raise ValidationError(
{"detail": f"You can not set the state to {state_id}."} {"detail": "You can not set the state to {0}.", "args": [state_id]}
) )
motion.set_state(state_id) motion.set_state(state_id)
@ -826,7 +852,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
# Send response. # Send response.
return Response( return Response(
{"detail": f"State of {len(motion_result)} motions successfully set."} {
"detail": "State of {0} motions successfully set.",
"args": [len(motion_result)],
}
) )
@detail_route(methods=["put"]) @detail_route(methods=["put"])
@ -858,7 +887,8 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
]: ]:
raise ValidationError( raise ValidationError(
{ {
"detail": f"You can not set the recommendation to {recommendation_state_id}." "detail": "You can not set the recommendation to {0}.",
"args": [recommendation_state_id],
} }
) )
motion.set_recommendation(recommendation_state_id) motion.set_recommendation(recommendation_state_id)
@ -875,7 +905,6 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
if motion.recommendation if motion.recommendation
else "None" else "None"
) )
message = f"The recommendation of the motion was set to {label}."
# Fire autoupdate again to save information to OpenSlides history. # Fire autoupdate again to save information to OpenSlides history.
inform_changed_data( inform_changed_data(
@ -884,7 +913,12 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
user_id=request.user.pk, user_id=request.user.pk,
) )
return Response({"detail": message}) return Response(
{
"detail": "The recommendation of the motion was set to {0}.",
"args": [label],
}
)
@list_route(methods=["post"]) @list_route(methods=["post"])
@transaction.atomic @transaction.atomic
@ -927,7 +961,9 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
try: try:
motion = Motion.objects.get(pk=item["id"]) motion = Motion.objects.get(pk=item["id"])
except Motion.DoesNotExist: except Motion.DoesNotExist:
raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) raise ValidationError(
{"detail": "Motion {0} does not exist", "args": [item["id"]]}
)
# Set or reset recommendation. # Set or reset recommendation.
recommendation_state_id = item["recommendation"] recommendation_state_id = item["recommendation"]
@ -944,7 +980,8 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
]: ]:
raise ValidationError( raise ValidationError(
{ {
"detail": "You can not set the recommendation to {recommendation_state_id}." "detail": "You can not set the recommendation to {0}.",
"args": [recommendation_state_id],
} }
) )
motion.set_recommendation(recommendation_state_id) motion.set_recommendation(recommendation_state_id)
@ -971,7 +1008,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
# Send response. # Send response.
return Response( return Response(
{"detail": f"{len(motion_result)} motions successfully updated."} {
"detail": "{0} motions successfully updated.",
"args": [len(motion_result)],
}
) )
@detail_route(methods=["post"]) @detail_route(methods=["post"])
@ -1070,12 +1110,16 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
try: try:
motion = Motion.objects.get(pk=item["id"]) motion = Motion.objects.get(pk=item["id"])
except Motion.DoesNotExist: except Motion.DoesNotExist:
raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) raise ValidationError(
{"detail": "Motion {0} does not exist", "args": [item["id"]]}
)
# Set new tags # Set new tags
for tag_id in item["tags"]: for tag_id in item["tags"]:
if not Tag.objects.filter(pk=tag_id).exists(): if not Tag.objects.filter(pk=tag_id).exists():
raise ValidationError({"detail": f"Tag {tag_id} does not exist"}) raise ValidationError(
{"detail": "Tag {0} does not exist", "args": [tag_id]}
)
motion.tags.set(item["tags"]) motion.tags.set(item["tags"])
# Finish motion. # Finish motion.
@ -1086,7 +1130,10 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
# Send response. # Send response.
return Response( return Response(
{"detail": f"{len(motion_result)} motions successfully updated."} {
"detail": "{0} motions successfully updated.",
"args": [len(motion_result)],
}
) )
@ -1164,15 +1211,6 @@ class MotionChangeRecommendationViewSet(ModelViewSet):
result = False result = False
return result return result
def create(self, request, *args, **kwargs):
"""
Creating a Change Recommendation, custom exception handling
"""
try:
return super().create(request, *args, **kwargs)
except DjangoValidationError as err:
return Response({"detail": err.message}, status=400)
def perform_create(self, serializer): def perform_create(self, serializer):
""" """
Customized method to add history information. Customized method to add history information.
@ -1253,12 +1291,12 @@ class MotionCommentSectionViewSet(ModelViewSet):
motions_verbose += ", ..." motions_verbose += ", ..."
if count == 1: if count == 1:
msg = f"This section has still comments in motion {motions_verbose}." msg = "This section has still comments in motion {0}."
else: else:
msg = f"This section has still comments in motions {motions_verbose}." msg = "This section has still comments in motions {0}."
msg += " " + "Please remove all comments before deletion." msg += " " + "Please remove all comments before deletion."
raise ValidationError({"detail": msg}) raise ValidationError({"detail": msg, "args": [motions_verbose]})
return result return result
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
@ -1440,7 +1478,8 @@ class CategoryViewSet(TreeSortMixin, ModelViewSet):
) )
return Response( return Response(
{ {
"detail": f"All motions in category {main_category} numbered successfully." "detail": "All motions in category {0} numbered successfully.",
"args": [str(main_category)],
} }
) )
@ -1503,7 +1542,7 @@ class MotionBlockViewSet(ModelViewSet):
class ProtectedErrorMessageMixin: class ProtectedErrorMessageMixin:
def getProtectedErrorMessage(self, name, error): def raiseProtectedError(self, name, error):
# The protected objects can just be motions.. # The protected objects can just be motions..
motions = ['"' + str(m) + '"' for m in error.protected_objects.all()] motions = ['"' + str(m) + '"' for m in error.protected_objects.all()]
count = len(motions) count = len(motions)
@ -1512,10 +1551,15 @@ class ProtectedErrorMessageMixin:
motions_verbose += ", ..." motions_verbose += ", ..."
if count == 1: if count == 1:
msg = f"This {name} is assigned to motion {motions_verbose}." msg = f"This {0} is assigned to motion {1}."
else: else:
msg = f"This {name} is assigned to motions {motions_verbose}." msg = f"This {0} is assigned to motions {1}."
return f"{msg} Please remove all assignments before deletion." raise ValidationError(
{
"detail": f"{msg} Please remove all assignments before deletion.",
"args": [name, motions_verbose],
}
)
class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
@ -1555,8 +1599,7 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
try: try:
result = super().destroy(*args, **kwargs) result = super().destroy(*args, **kwargs)
except ProtectedError as err: except ProtectedError as err:
msg = self.getProtectedErrorMessage("workflow", err) self.raiseProtectedError("workflow", err)
raise ValidationError({"detail": msg})
# Change motion default workflows in the config # Change motion default workflows in the config
if int(config["motions_workflow"]) == workflow_pk: if int(config["motions_workflow"]) == workflow_pk:
@ -1608,8 +1651,7 @@ class StateViewSet(ModelViewSet, ProtectedErrorMessageMixin):
try: try:
result = super().destroy(*args, **kwargs) result = super().destroy(*args, **kwargs)
except ProtectedError as err: except ProtectedError as err:
msg = self.getProtectedErrorMessage("workflow", err) self.raiseProtectedError("workflow", err)
raise ValidationError({"detail": msg})
inform_changed_data(workflow) inform_changed_data(workflow)
return result return result

View File

@ -14,6 +14,6 @@ def default_votes_validator(data):
and data[key] < -2 and data[key] < -2
): ):
raise ValidationError( raise ValidationError(
{"detail": f"Value for {key} must not be less than -2"} {"detail": "Value for {0} must not be less than -2", "args": [key]}
) )
return data return data

View File

@ -227,7 +227,7 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
try: try:
message = message.format(**message_format) message = message.format(**message_format)
except KeyError as err: except KeyError as err:
raise ValidationError({"detail": f"Invalid property {err}."}) raise ValidationError({"detail": "Invalid property {0}", "args": [err]})
subject_format = format_dict( subject_format = format_dict(
{"event_name": config["general_event_name"], "username": self.username} {"event_name": config["general_event_name"], "username": self.username}
@ -235,7 +235,7 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
try: try:
subject = subject.format(**subject_format) subject = subject.format(**subject_format)
except KeyError as err: except KeyError as err:
raise ValidationError({"detail": f"Invalid property {err}."}) raise ValidationError({"detail": "Invalid property {0}", "args": [err]})
# Create an email and send it. # Create an email and send it.
email = mail.EmailMessage( email = mail.EmailMessage(
@ -255,7 +255,10 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
helptext = " Is the email sender correct?" helptext = " Is the email sender correct?"
connection.close() connection.close()
raise ValidationError( raise ValidationError(
{"detail": f"Error {error}. Cannot send email.{helptext}"} {
"detail": "Error {0}. Cannot send email.{1}",
"args": [error, helptext],
}
) )
except smtplib.SMTPRecipientsRefused: except smtplib.SMTPRecipientsRefused:
pass # Run into returning false later pass # Run into returning false later
@ -263,7 +266,8 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
# Nice error message on auth failure # Nice error message on auth failure
raise ValidationError( raise ValidationError(
{ {
"detail": f"Error {e.smtp_code}: Authentication failure. Please contact your local administrator." "detail": "Error {0}: Authentication failure. Please contact your local administrator.",
"args": [e.smtp_code],
} }
) )
else: else:

View File

@ -202,7 +202,8 @@ class UserViewSet(ModelViewSet):
errors = " ".join(errors) errors = " ".join(errors)
raise ValidationError( raise ValidationError(
{ {
"detail": f'The default password of user "{user.username}" is not valid: {errors}' "detail": 'The default password of user "{0}" is not valid: {1}',
"args": [user.username, str(errors)],
} }
) )
@ -331,7 +332,8 @@ class UserViewSet(ModelViewSet):
inform_changed_data(created_users) inform_changed_data(created_users)
return Response( return Response(
{ {
"detail": f"{len(created_users)} users successfully imported.", "detail": "{0} users successfully imported.",
"args": [len(created_users)],
"importedTrackIds": imported_track_ids, "importedTrackIds": imported_track_ids,
} }
) )
@ -362,7 +364,8 @@ class UserViewSet(ModelViewSet):
except ConnectionRefusedError: except ConnectionRefusedError:
raise ValidationError( raise ValidationError(
{ {
"detail": f"Cannot connect to SMTP server on {settings.EMAIL_HOST}:{settings.EMAIL_PORT}" "detail": "Cannot connect to SMTP server on {0}:{1}",
"args": [settings.EMAIL_HOST, settings.EMAIL_PORT],
} }
) )
except smtplib.SMTPException as err: except smtplib.SMTPException as err:
@ -391,7 +394,7 @@ class UserViewSet(ModelViewSet):
def assert_list_of_ints(self, ids, ids_name="user_ids"): def assert_list_of_ints(self, ids, ids_name="user_ids"):
""" Asserts, that ids is a list of ints. Raises a ValidationError, if not. """ """ Asserts, that ids is a list of ints. Raises a ValidationError, if not. """
if not isinstance(ids, list): if not isinstance(ids, list):
raise ValidationError({"detail": f"{ids_name} must be a list"}) raise ValidationError({"detail": "{0} must be a list", "args": [ids_name]})
for id in ids: for id in ids:
if not isinstance(id, int): if not isinstance(id, int):
raise ValidationError({"detail": "Every id must be a int"}) raise ValidationError({"detail": "Every id must be a int"})
@ -546,7 +549,10 @@ class GroupViewSet(ModelViewSet):
inform_changed_data(group) inform_changed_data(group)
return Response( return Response(
{"detail": f"Permissions of group {group.pk} successfully changed."} {
"detail": "Permissions of group {0} successfully changed.",
"args": [group.pk],
}
) )
def inform_permission_change( def inform_permission_change(
@ -627,7 +633,8 @@ class PersonalNoteViewSet(ModelViewSet):
except IntegrityError: except IntegrityError:
raise ValidationError( raise ValidationError(
{ {
"detail": f"The personal note for user {self.request.user.id} does already exist" "detail": "The personal note for user {0} does already exist",
"args": [self.request.user.id],
} }
) )
@ -812,7 +819,14 @@ class PasswordResetView(APIView):
Loop over all users and send emails. Loop over all users and send emails.
""" """
to_email = request.data.get("email") to_email = request.data.get("email")
for user in self.get_users(to_email): users = self.get_users(to_email)
if len(users) == 0 and getattr(settings, "RESET_PASSWORD_VERBOSE_ERRORS", True):
raise ValidationError(
{"detail": "No users with email {0} found.", "args": [to_email]}
)
for user in users:
current_site = get_current_site(request) current_site = get_current_site(request)
site_name = current_site.name site_name = current_site.name
if has_perm(user, "users.can_change_password") or has_perm( if has_perm(user, "users.can_change_password") or has_perm(
@ -850,14 +864,16 @@ class PasswordResetView(APIView):
except smtplib.SMTPRecipientsRefused: except smtplib.SMTPRecipientsRefused:
raise ValidationError( raise ValidationError(
{ {
"detail": f"Error: The email to {to_email} was refused by the server. Please contact your local administrator." "detail": "Error: The email to {0} was refused by the server. Please contact your local administrator.",
"args": [to_email],
} }
) )
except smtplib.SMTPAuthenticationError as e: except smtplib.SMTPAuthenticationError as e:
# Nice error message on auth failure # Nice error message on auth failure
raise ValidationError( raise ValidationError(
{ {
"detail": f"Error {e.smtp_code}: Authentication failure. Please contact your administrator." "detail": "Error {0}: Authentication failure. Please contact your administrator.",
"args": [e.smtp_code],
} }
) )
except ConnectionRefusedError: except ConnectionRefusedError:
@ -866,7 +882,7 @@ class PasswordResetView(APIView):
"detail": "Connection refused error. Please contact your administrator." "detail": "Connection refused error. Please contact your administrator."
} }
) )
return super().post(request, *args, **kwargs) return Response()
def get_users(self, email): def get_users(self, email):
"""Given an email, return matching user(s) who should receive a reset. """Given an email, return matching user(s) who should receive a reset.
@ -878,7 +894,7 @@ class PasswordResetView(APIView):
active_users = User.objects.filter( active_users = User.objects.filter(
**{"email__iexact": email, "is_active": True} **{"email__iexact": email, "is_active": True}
) )
return (u for u in active_users if u.has_usable_password()) return [u for u in active_users if u.has_usable_password()]
def get_email_body(self, **context): def get_email_body(self, **context):
""" """

View File

@ -2,10 +2,7 @@
Settings file for OpenSlides. Settings file for OpenSlides.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/ https://github.com/OpenSlides/OpenSlides/blob/master/SETTINGS.rst
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
""" """
import os import os
@ -40,6 +37,13 @@ SECRET_KEY = %(secret_key)r
DEBUG = %(debug)s DEBUG = %(debug)s
# Controls the verbosity on errors during a reset password. If enabled, an error
# will be shown, if there does not exist a user with a given email address. So one
# can check, if a email is registered. If this is not wanted, disable verbose
# messages. An success message will always be shown.
RESET_PASSWORD_VERBOSE_ERRORS = True
# Email settings # Email settings
# For SSL/TLS specific settings see https://docs.djangoproject.com/en/1.11/topics/email/#smtp-backend # For SSL/TLS specific settings see https://docs.djangoproject.com/en/1.11/topics/email/#smtp-backend

View File

@ -59,7 +59,7 @@ class TreeSortMixin:
does not have every model, the remaining models are sorted correctly. does not have every model, the remaining models are sorted correctly.
""" """
if not isinstance(request.data, list): if not isinstance(request.data, list):
raise ValidationError("The data must be a list.") raise ValidationError({"detail": "The data must be a list."})
# get all item ids to verify, that the user send all ids. # get all item ids to verify, that the user send all ids.
all_model_ids = set(model.objects.all().values_list("pk", flat=True)) all_model_ids = set(model.objects.all().values_list("pk", flat=True))
@ -91,9 +91,11 @@ class TreeSortMixin:
node[weight_key] = weight node[weight_key] = weight
weight += 2 weight += 2
if id in ids_found: if id in ids_found:
raise ValidationError(f"Duplicate id: {id}") raise ValidationError({"detail": "Duplicate id: {0}", "args": [id]})
if id not in all_model_ids: if id not in all_model_ids:
raise ValidationError(f"Id does not exist: {id}") raise ValidationError(
{"detail": "Id does not exist: {0}", "args": [id]}
)
ids_found.add(id) ids_found.add(id)
# Add children, if exist. # Add children, if exist.
@ -105,14 +107,17 @@ class TreeSortMixin:
child.get("id"), int child.get("id"), int
): ):
raise ValidationError( raise ValidationError(
"child must be a dict with an id as integer" {"detail": "child must be a dict with an id as integer"}
) )
child[parent_id_key] = id child[parent_id_key] = id
nodes_to_check.append(child) nodes_to_check.append(child)
if len(all_model_ids) != len(ids_found): if len(all_model_ids) != len(ids_found):
raise ValidationError( raise ValidationError(
f"Did not recieved {len(all_model_ids)} ids, got {len(ids_found)}." {
"detail": "Did not recieved {0} ids, got {1}.",
"args": [len(all_model_ids), len(ids_found)],
}
) )
# Do the actual update: # Do the actual update:

View File

@ -927,8 +927,9 @@ class ManageComments(TestCase):
) )
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(
response.data["detail"], "A comment section with id 42 does not exist." response.data["detail"], "A comment section with id {0} does not exist."
) )
self.assertEqual(response.data["args"][0], "42")
def test_create_comment(self): def test_create_comment(self):
response = self.client.post( response = self.client.post(
@ -1309,7 +1310,7 @@ class TestMotionCommentSection(TestCase):
response = self.client.delete( response = self.client.delete(
reverse("motioncommentsection-detail", args=[section.pk]) reverse("motioncommentsection-detail", args=[section.pk])
) )
self.assertTrue("test_title_SlqfMw(waso0saWMPqcZ" in response.data["detail"]) self.assertEqual(response.data["args"][0], '"test_title_SlqfMw(waso0saWMPqcZ"')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(MotionCommentSection.objects.count(), 1) self.assertEqual(MotionCommentSection.objects.count(), 1)
@ -1510,12 +1511,8 @@ class CreateMotionChangeRecommendation(TestCase):
}, },
) )
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(response.data["args"][0], "3")
response.data, self.assertEqual(response.data["args"][1], "6")
{
"detail": "The recommendation collides with an existing one (line 3 - 6)."
},
)
def test_no_collission_different_motions(self): def test_no_collission_different_motions(self):
""" """
@ -1635,7 +1632,10 @@ class SetState(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "You can not set the state to %d." % invalid_state_id}, {
"detail": "You can not set the state to {0}.",
"args": [str(invalid_state_id)],
},
) )
def test_reset(self): def test_reset(self):
@ -1672,7 +1672,10 @@ class SetRecommendation(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "The recommendation of the motion was set to Acceptance."}, {
"detail": "The recommendation of the motion was set to {0}.",
"args": ["Acceptance"],
},
) )
self.assertEqual( self.assertEqual(
Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted" Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted"
@ -1699,7 +1702,10 @@ class SetRecommendation(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "You can not set the recommendation to %d." % invalid_state_id}, {
"detail": "You can not set the recommendation to {0}.",
"args": [str(invalid_state_id)],
},
) )
def test_set_invalid_recommendation(self): def test_set_invalid_recommendation(self):
@ -1712,7 +1718,10 @@ class SetRecommendation(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "You can not set the recommendation to %d." % invalid_state_id}, {
"detail": "You can not set the recommendation to {0}.",
"args": [str(invalid_state_id)],
},
) )
def test_set_invalid_recommendation_2(self): def test_set_invalid_recommendation_2(self):
@ -1727,7 +1736,10 @@ class SetRecommendation(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "You can not set the recommendation to %d." % invalid_state_id}, {
"detail": "You can not set the recommendation to {0}.",
"args": [str(invalid_state_id)],
},
) )
def test_reset(self): def test_reset(self):
@ -1739,7 +1751,10 @@ class SetRecommendation(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "The recommendation of the motion was set to None."}, {
"detail": "The recommendation of the motion was set to {0}.",
"args": ["None"],
},
) )
self.assertTrue(Motion.objects.get(pk=self.motion.pk).recommendation is None) self.assertTrue(Motion.objects.get(pk=self.motion.pk).recommendation is None)
@ -1753,7 +1768,10 @@ class SetRecommendation(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data, response.data,
{"detail": "The recommendation of the motion was set to Acceptance."}, {
"detail": "The recommendation of the motion was set to {0}.",
"args": ["Acceptance"],
},
) )
self.assertEqual( self.assertEqual(
Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted" Motion.objects.get(pk=self.motion.pk).recommendation.name, "accepted"
@ -1839,6 +1857,10 @@ class NumberMotionsInCategories(TestCase):
""" """
Tests numbering motions in categories. Tests numbering motions in categories.
Default test environment:
- *without* blanks
- 1 min digit
Testdata. All names (and prefixes) are prefixed with "test_". The Testdata. All names (and prefixes) are prefixed with "test_". The
ordering is ensured with "category_weight". ordering is ensured with "category_weight".
Category tree (with motions M and amendments A): Category tree (with motions M and amendments A):
@ -1926,25 +1948,21 @@ class NumberMotionsInCategories(TestCase):
def test_numbering(self): def test_numbering(self):
response = self.client.post(reverse("category-numbering", args=[self.A.pk])) response = self.client.post(reverse("category-numbering", args=[self.A.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Motion.objects.get(pk=self.M1.pk).identifier, "test_A 1") self.assertEqual(Motion.objects.get(pk=self.M1.pk).identifier, "test_A1")
self.assertEqual(Motion.objects.get(pk=self.M3.pk).identifier, "test_A 2") self.assertEqual(Motion.objects.get(pk=self.M3.pk).identifier, "test_A2")
self.assertEqual(Motion.objects.get(pk=self.M2.pk).identifier, "test_C 3") self.assertEqual(Motion.objects.get(pk=self.M2.pk).identifier, "test_C3")
self.assertEqual(Motion.objects.get(pk=self.M2_A1.pk).identifier, "test_C3-2")
self.assertEqual( self.assertEqual(
Motion.objects.get(pk=self.M2_A1.pk).identifier, "test_C 3 - 2" Motion.objects.get(pk=self.M2_A1_A1.pk).identifier, "test_C3-2-1"
)
self.assertEqual(
Motion.objects.get(pk=self.M2_A1_A1.pk).identifier, "test_C 3 - 2 - 1"
)
self.assertEqual(
Motion.objects.get(pk=self.M2_A2.pk).identifier, "test_C 3 - 1"
) )
self.assertEqual(Motion.objects.get(pk=self.M2_A2.pk).identifier, "test_C3-1")
def test_with_blanks(self): def test_with_blanks_and_leading_zeros(self):
config["motions_amendments_prefix"] = "-X" config["motions_amendments_prefix"] = "-X"
config["motions_identifier_with_blank"] = False config["motions_identifier_with_blank"] = True
config["motions_identifier_min_digits"] = 3 config["motions_identifier_min_digits"] = 3
response = self.client.post(reverse("category-numbering", args=[self.A.pk])) response = self.client.post(reverse("category-numbering", args=[self.A.pk]))
config["motions_identifier_with_blank"] = True config["motions_identifier_with_blank"] = False
config["motions_identifier_min_digits"] = 1 config["motions_identifier_min_digits"] = 1
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1963,16 +1981,17 @@ class NumberMotionsInCategories(TestCase):
) )
def test_existing_identifier_no_category(self): def test_existing_identifier_no_category(self):
# config["motions_identifier_with_blank"] = True
conflicting_motion = Motion( conflicting_motion = Motion(
title="test_title_al2=2k21fjv1lsck3ehlWExg", title="test_title_al2=2k21fjv1lsck3ehlWExg",
text="test_text_3omvpEhnfg082ejplk1m", text="test_text_3omvpEhnfg082ejplk1m",
) )
conflicting_motion.save() conflicting_motion.save()
conflicting_motion.identifier = "test_C 3 - 2 - 1" conflicting_motion.identifier = "test_C3-2-1"
conflicting_motion.save() conflicting_motion.save()
response = self.client.post(reverse("category-numbering", args=[self.A.pk])) response = self.client.post(reverse("category-numbering", args=[self.A.pk]))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue("test_C 3 - 2 - 1" in response.data["detail"]) self.assertEqual("test_C3-2-1", response.data["args"][0])
def test_existing_identifier_with_category(self): def test_existing_identifier_with_category(self):
conflicting_category = Category.objects.create( conflicting_category = Category.objects.create(
@ -1984,20 +2003,20 @@ class NumberMotionsInCategories(TestCase):
category=conflicting_category, category=conflicting_category,
) )
conflicting_motion.save() conflicting_motion.save()
conflicting_motion.identifier = "test_C 3 - 2 - 1" conflicting_motion.identifier = "test_C3-2-1"
conflicting_motion.save() conflicting_motion.save()
response = self.client.post(reverse("category-numbering", args=[self.A.pk])) response = self.client.post(reverse("category-numbering", args=[self.A.pk]))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue("test_C 3 - 2 - 1" in response.data["detail"]) self.assertEqual("test_C3-2-1", response.data["args"][0])
self.assertTrue(conflicting_category.name in response.data["detail"]) self.assertEqual(conflicting_category.name, response.data["args"][1])
def test_incomplete_amendment_tree(self): def test_incomplete_amendment_tree(self):
self.M2_A1.category = None self.M2_A1.category = None
self.M2_A1.save() self.M2_A1.save()
response = self.client.post(reverse("category-numbering", args=[self.A.pk])) response = self.client.post(reverse("category-numbering", args=[self.A.pk]))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertTrue(self.M2_A1_A1.title in response.data["detail"]) self.assertEqual(self.M2_A1_A1.title, response.data["args"][0])
self.assertTrue(self.M2_A1.title in response.data["detail"]) self.assertEqual(self.M2_A1.title, response.data["args"][1])
class TestMotionBlock(TestCase): class TestMotionBlock(TestCase):