diff --git a/.travis.yml b/.travis.yml index 41e4c1feb..6a33b433d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ cache: pip: true yarn: true python: - - "3.4" - "3.5" - "3.6" env: @@ -21,13 +20,13 @@ install: - node_modules/.bin/gulp --production script: - flake8 openslides tests - - if [ "`python --version`" \> "Python 3.5.0" ]; then isort --check-only --recursive openslides tests; fi + - isort --check-only --recursive openslides tests - python -m mypy openslides/ - node_modules/.bin/gulp jshint - node_modules/.bin/karma start --browsers PhantomJS tests/karma/karma.conf.js - DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.unit - - coverage report --fail-under=42 + - coverage report --fail-under=35 - DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.integration - coverage report --fail-under=73 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1b8fe7ca3..0ca632926 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Motions: - 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]. - Updated pdfMake to 0.1.37 [#3766]. + - Python 3.4 is not supported anymore [#3777]. Version 2.2 (2018-06-06) diff --git a/DEVELOPMENT.rst b/DEVELOPMENT.rst index d4bca3eba..80c17e036 100644 --- a/DEVELOPMENT.rst +++ b/DEVELOPMENT.rst @@ -15,7 +15,7 @@ Installation and start of the development version a. Check requirements ''''''''''''''''''''' -Make sure that you have installed `Python (>= 3.4) `_, +Make sure that you have installed `Python (>= 3.5) `_, `Node.js (>=4.x) `_, `Yarn `_ and `Git `_ on your system. You also need build-essential packages and header files and a static library for Python. diff --git a/README.rst b/README.rst index 3be78f046..7a12638d9 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Installation a. Check requirements ''''''''''''''''''''' -Make sure that you have installed `Python (>= 3.4) `_ +Make sure that you have installed `Python (>= 3.5) `_ on your system. Additional you need build-essential packages, header files and a static diff --git a/openslides/core/views.py b/openslides/core/views.py index d0b90cd03..ba2b1f1b6 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -650,7 +650,7 @@ class TagViewSet(ModelViewSet): elif self.action == 'metadata': # Every authenticated user can see the metadata. # Anonymous users can do so if they are enabled. - result = self.request.user.is_authenticated() or anonymous_is_enabled() + result = self.request.user.is_authenticated or anonymous_is_enabled() elif self.action in ('create', 'partial_update', 'update', 'destroy'): result = has_perm(self.request.user, 'core.can_manage_tags') else: @@ -678,7 +678,7 @@ class ConfigViewSet(ModelViewSet): # Every authenticated user can see the metadata and list or # retrieve the config. Anonymous users can do so if they are # enabled. - result = self.request.user.is_authenticated() or anonymous_is_enabled() + result = self.request.user.is_authenticated or anonymous_is_enabled() elif self.action in ('partial_update', 'update'): # The user needs 'core.can_manage_logos_and_fonts' for all config values # starting with 'logo' and 'font'. For all other config values th euser needs @@ -735,7 +735,7 @@ class ChatMessageViewSet(ModelViewSet): # We do not want anonymous users to use the chat even the anonymous # group has the permission core.can_use_chat. result = ( - self.request.user.is_authenticated() and + self.request.user.is_authenticated and has_perm(self.request.user, 'core.can_use_chat')) elif self.action == 'clear': result = ( diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 802b38f0f..f7d9e01bb 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -708,7 +708,7 @@ class Motion(RESTModelMixin, models.Model): The message should be in English and translatable, e. g. motion.write_log(message_list=[ugettext_noop('Message Text')]) """ - if person and not person.is_authenticated(): + if person and not person.is_authenticated: person = None motion_log = MotionLog(motion=self, message_list=message_list, person=person) motion_log.save(skip_autoupdate=skip_autoupdate) diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index f8decaab8..5532d5061 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -1,5 +1,4 @@ import re - from typing import Generator, Type from ..core.config import config diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 5e7bb59d0..29b151acf 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -107,6 +107,10 @@ class MotionViewSet(ModelViewSet): """ Customized view endpoint to create a new motion. """ + # This is a hack to make request.data mutable. Otherwise fields can not be deleted. + if isinstance(request.data, QueryDict): + request.data._mutable = True + # Check if parent motion exists. if request.data.get('parent_id') is not None: try: @@ -183,7 +187,7 @@ class MotionViewSet(ModelViewSet): continue # Do not add users that do not exist # Add the request user, if he is authenticated and no submitters were given: - if len(submitters) == 0 and request.user.is_authenticated(): + if len(submitters) == 0 and request.user.is_authenticated: submitters.append(request.user) # create all submitters @@ -211,6 +215,10 @@ class MotionViewSet(ModelViewSet): self.check_view_permissions()). Also check manage permission or submitter and state. """ + # This is a hack to make request.data mutable. Otherwise fields can not be deleted. + if isinstance(request.data, QueryDict): + request.data._mutable = True + # Get motion. motion = self.get_object() diff --git a/openslides/users/views.py b/openslides/users/views.py index 6a20e1ce0..4077d3c4a 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -62,7 +62,7 @@ class UserViewSet(ModelViewSet): elif self.action == 'metadata': result = has_perm(self.request.user, 'users.can_see_name') elif self.action in ('update', 'partial_update'): - result = self.request.user.is_authenticated() + result = self.request.user.is_authenticated elif self.action in ('create', 'destroy', 'reset_password', 'mass_import', 'mass_invite_email'): result = (has_perm(self.request.user, 'users.can_see_name') and has_perm(self.request.user, 'users.can_see_extra_data') and @@ -93,6 +93,9 @@ class UserViewSet(ModelViewSet): # The user does not have all permissions so he may only update himself. if str(request.user.pk) != self.kwargs['pk']: self.permission_denied(request) + + # This is a hack to make request.data mutable. Otherwise fields can not be deleted. + request.data._mutable = True # Remove fields that the user is not allowed to change. # The list() is required because we want to use del inside the loop. for key in list(request.data.keys()): @@ -266,7 +269,7 @@ class GroupViewSet(ModelViewSet): elif self.action == 'metadata': # Every authenticated user can see the metadata. # Anonymous users can do so if they are enabled. - result = self.request.user.is_authenticated() or anonymous_is_enabled() + result = self.request.user.is_authenticated or anonymous_is_enabled() elif self.action in ('create', 'partial_update', 'update', 'destroy'): # Users with all app permissions can edit groups. result = (has_perm(self.request.user, 'users.can_see_name') and @@ -365,7 +368,7 @@ class PersonalNoteViewSet(ModelViewSet): # Every authenticated user can see metadata and create personal # notes for himself and can manipulate only his own personal notes. # See self.perform_create(), self.update() and self.destroy(). - result = self.request.user.is_authenticated() + result = self.request.user.is_authenticated else: result = False return result @@ -458,7 +461,7 @@ class UserLogoutView(APIView): http_method_names = ['post'] def post(self, *args, **kwargs): - if not self.request.user.is_authenticated(): + if not self.request.user.is_authenticated: raise ValidationError({'detail': _('You are not authenticated.')}) auth_logout(self.request) return super().post(*args, **kwargs) diff --git a/openslides/utils/migrations.py b/openslides/utils/migrations.py index 0a08cc067..371265db7 100644 --- a/openslides/utils/migrations.py +++ b/openslides/utils/migrations.py @@ -20,7 +20,7 @@ def add_permission_to_groups_based_on_existing_permission( def function(apps: Any, schema_editor: Any) -> None: content_type = ContentType.objects.filter(model=model, app_label=app_label) - base_perm = Permission.objects.filter(codename=codename, content_type=content_type) + base_perm = Permission.objects.filter(codename=codename, content_type__in=content_type) if len(base_perm) is 1 and len(content_type) is 1: # get the actual content type and base permission diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 80c1bbdbe..bd44c5fe9 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -12,11 +12,11 @@ from rest_framework.mixins import ( # noqa DestroyModelMixin, UpdateModelMixin, ) +from rest_framework.relations import MANY_RELATION_KWARGS from rest_framework.response import Response from rest_framework.routers import DefaultRouter from rest_framework.serializers import ModelSerializer as _ModelSerializer from rest_framework.serializers import ( # noqa - MANY_RELATION_KWARGS, CharField, DictField, Field, diff --git a/requirements_big_mode.txt b/requirements_big_mode.txt index 1545c6818..689f75ab9 100644 --- a/requirements_big_mode.txt +++ b/requirements_big_mode.txt @@ -3,7 +3,7 @@ # Requirements for Redis and PostgreSQL support asgi-redis>=1.3,<1.5 -django-redis>=4.7.0,<4.9 -django-redis-sessions>=0.5.6,<0.6 +django-redis>=4.7.0,<4.10 +django-redis-sessions>=0.5.6,<0.7 psycopg2-binary>=2.7,<2.8 txredisapi==1.4.4 diff --git a/requirements_production.txt b/requirements_production.txt index 489658683..76a2ad433 100644 --- a/requirements_production.txt +++ b/requirements_production.txt @@ -2,10 +2,10 @@ bleach>=1.5.0,<2.2 channels>=1.1,<1.2 daphne<2 -Django>=1.10.4,<1.11 -djangorestframework>=3.4,<3.5 +Django>=1.10.4,<2.1 +djangorestframework>=3.4,<3.9 jsonfield>=1.0,<2.1 mypy_extensions>=0.3,<0.4 PyPDF2>=1.26,<1.27 -roman>=2.0,<2.1 -setuptools>=29.0,<39.0 +roman>=2.0,<3.1 +setuptools>=29.0,<41.0 diff --git a/setup.py b/setup.py index dca03ba2d..af806da83 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,6 @@ setup( 'Framework :: Django', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], packages=find_packages(exclude=['tests', 'tests.*']), diff --git a/tests/integration/agenda/test_viewset.py b/tests/integration/agenda/test_viewset.py index 53e299794..b037ef823 100644 --- a/tests/integration/agenda/test_viewset.py +++ b/tests/integration/agenda/test_viewset.py @@ -1,5 +1,5 @@ from django.contrib.auth import get_user_model -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.translation import ugettext from django_redis import get_redis_connection from rest_framework import status diff --git a/tests/integration/assignments/test_viewset.py b/tests/integration/assignments/test_viewset.py index 5b889b575..dad256b6a 100644 --- a/tests/integration/assignments/test_viewset.py +++ b/tests/integration/assignments/test_viewset.py @@ -1,5 +1,5 @@ from django.contrib.auth import get_user_model -from django.core.urlresolvers import reverse +from django.urls import reverse from django_redis import get_redis_connection from rest_framework import status from rest_framework.test import APIClient diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index 9482b50e8..944e1f092 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -1,7 +1,7 @@ import json from django.apps import apps -from django.core.urlresolvers import reverse +from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient diff --git a/tests/integration/core/test_viewset.py b/tests/integration/core/test_viewset.py index 0143ee5b0..47e046ac3 100644 --- a/tests/integration/core/test_viewset.py +++ b/tests/integration/core/test_viewset.py @@ -1,4 +1,4 @@ -from django.core.urlresolvers import reverse +from django.urls import reverse from django_redis import get_redis_connection from rest_framework import status from rest_framework.test import APIClient diff --git a/tests/integration/mediafiles/test_viewset.py b/tests/integration/mediafiles/test_viewset.py index ddcb9bf8e..922c49498 100644 --- a/tests/integration/mediafiles/test_viewset.py +++ b/tests/integration/mediafiles/test_viewset.py @@ -1,5 +1,5 @@ from django.core.files.uploadedfile import SimpleUploadedFile -from django.core.urlresolvers import reverse +from django.urls import reverse from django_redis import get_redis_connection from rest_framework.test import APIClient diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index 8f04e3223..ea8a38cfc 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -2,7 +2,7 @@ import json from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission -from django.core.urlresolvers import reverse +from django.urls import reverse from django_redis import get_redis_connection from rest_framework import status from rest_framework.test import APIClient diff --git a/tests/integration/topics/test_viewset.py b/tests/integration/topics/test_viewset.py index 5548b1f9b..5acab19c2 100644 --- a/tests/integration/topics/test_viewset.py +++ b/tests/integration/topics/test_viewset.py @@ -1,5 +1,5 @@ from django.contrib.auth import get_user_model -from django.core.urlresolvers import reverse +from django.urls import reverse from django_redis import get_redis_connection from rest_framework import status from rest_framework.test import APIClient diff --git a/tests/integration/users/test_viewset.py b/tests/integration/users/test_viewset.py index cb89366a4..b694fd3d4 100644 --- a/tests/integration/users/test_viewset.py +++ b/tests/integration/users/test_viewset.py @@ -1,5 +1,5 @@ from django.core import mail -from django.core.urlresolvers import reverse +from django.urls import reverse from django_redis import get_redis_connection from rest_framework import status from rest_framework.test import APIClient diff --git a/tests/unit/core/test_views.py b/tests/unit/core/test_views.py index ec91e3e45..e0fb2c7ea 100644 --- a/tests/unit/core/test_views.py +++ b/tests/unit/core/test_views.py @@ -11,17 +11,6 @@ class ProjectorAPI(TestCase): self.viewset = views.ProjectorViewSet() self.viewset.format_kwarg = None - def test_activate_elements(self, mock_object): - mock_object.return_value.config = { - '6165b44cd0f34b44b1ed41565529d798': { - 'name': 'test_projector_element_Du4tie7foosahnoofahg', - 'test_key_Eek8eipeingulah3aech': 'test_value_quuupaephuY7eoLohbee'}} - request = MagicMock() - request.data = [{'name': 'new_test_projector_element_el9UbeeT9quucesoyusu'}] - self.viewset.request = request - self.viewset.activate_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 2) - def test_activate_elements_no_list(self, mock_object): mock_object.return_value.config = { '3979c9fc3bee432fb25f354d6b4868b3': { @@ -43,139 +32,3 @@ class ProjectorAPI(TestCase): self.viewset.request = request with self.assertRaises(ValidationError): self.viewset.activate_elements(request=request, pk=MagicMock()) - - def test_prune_elements(self, mock_object): - mock_object.return_value.config = { - '5460383449024dd99b04e8747d7764d5': { - 'name': 'test_projector_element_Oc7OhXeeg0poThoh8boo', - 'test_key_ahNei1ke4uCio6uareef': 'test_value_xieSh4yeemaen9oot6ki'}} - request = MagicMock() - request.data = [{ - 'name': 'test_projector_element_bohb1phiebah5TeCei1N', - 'test_key_gahSh9otu6aeghaiquie': 'test_value_aeNgee2Yeeph4Ohru2Oo'}] - self.viewset.request = request - self.viewset.prune_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 1) - - def test_prune_elements_with_stable(self, mock_object): - mock_object.return_value.config = { - 'e7f91680cd9343dba1416f14871b8e3b': { - 'name': 'test_projector_element_aegh2aichee9nooWohRu', - 'test_key_wahlaelahwaeNg6fooH7': 'test_value_taePie9Ohxohja4ugisa', - 'stable': True}} - request = MagicMock() - request.data = [{ - 'name': 'test_projector_element_yei1Aim6Aed1po8eegh2', - 'test_key_mud1shoo8moh6eiXoong': 'test_value_shugieJier6agh1Ehie3'}] - self.viewset.request = request - self.viewset.prune_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 2) - - def test_update_elements(self, mock_object): - mock_object.return_value.config = { - 'aacbb64acafc4ccc957240c871d4e77d': { - 'name': 'test_projector_element_jbmgfnf657djcnsjdfkm', - 'test_key_7mibir1Uoee7uhilohB1': 'test_value_mbhfn5zwhakbigjrns88'}} - request = MagicMock() - request.data = { - 'aacbb64acafc4ccc957240c871d4e77d': { - 'name': 'test_projector_element_wdsexrvhgn67ezfjnfje'}} - self.viewset.request = request - self.viewset.update_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 1) - self.assertEqual(mock_object.return_value.config[ - 'aacbb64acafc4ccc957240c871d4e77d']['name'], 'test_projector_element_wdsexrvhgn67ezfjnfje') - - def test_update_elements_wrong_element(self, mock_object): - mock_object.return_value.config = { - '5b5e5d3b35de4fff873925296c3093fc': { - 'name': 'test_projector_element_njb657djcsjdmgfnffkm', - 'test_key_uhilo7mir1Uoee7ibhB1': 'test_value_hjrnsmbhfn5zwakbig88'}} - request = MagicMock() - request.data = { - '255fda68ca6f4f3f803b98405abfb710': { - 'name': 'test_projector_element_wxrvhn67eebmfjjnkvds'}} - self.viewset.request = request - with self.assertRaises(ValidationError): - self.viewset.update_elements(request=request, pk=MagicMock()) - - def test_deactivate_elements(self, mock_object): - mock_object.return_value.config = { - '874aaf279be346ff85a9b456ce1d1128': { - 'name': 'test_projector_element_c6oohooxugiphuuM6Wee', - 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}} - request = MagicMock() - request.data = ['874aaf279be346ff85a9b456ce1d1128'] - self.viewset.request = request - self.viewset.deactivate_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 0) - - def test_deactivate_elements_wrong_element(self, mock_object): - mock_object.return_value.config = { - 'd867b2557ad041b8848e95981c5671b7': { - 'name': 'test_projector_element_c6oohooxugiphuuM6Wee', - 'test_key_eehiloh7mibi7ur1UoB1': 'test_value_o8eig1AeSajieTh6aiwo'}} - request = MagicMock() - request.data = ['1179ea09ba2b4559a41272efb1346c86'] # Wrong UUID. - self.viewset.request = request - with self.assertRaises(ValidationError): - self.viewset.deactivate_elements(request=request, pk=MagicMock()) - - def test_deactivate_elements_no_list(self, mock_object): - mock_object.return_value.config = [{ - 'name': 'test_projector_element_Au1ce9nevaeX7zo4ye2w', - 'test_key_we9biiZ7bah4Sha2haS5': 'test_value_eehoipheik6aiNgeegor', - 'uuid': '0f3b8f8df38b4bbc90f4beba9393d2db'}] - request = MagicMock() - request.data = 'bad_value_no_list_ohchohWee1fie0SieTha' - self.viewset.request = request - with self.assertRaises(ValidationError): - self.viewset.deactivate_elements(request=request, pk=MagicMock()) - - def test_deactivate_elements_bad_list(self, mock_object): - mock_object.return_value.config = [{ - 'name': 'test_projector_element_teibaeRaim1heiCh6Ohv', - 'test_key_uk7wai7eiZieQu0ief3': 'test_value_eeghisei3ieGh3ieb6ae', - 'uuid': '8ae42a09f585480e8b4a53194d4d1fba'}] - request = MagicMock() - # Value 1 is not an dictionary so we expect ValidationError. - request.data = [1] - self.viewset.request = request - with self.assertRaises(ValidationError): - self.viewset.deactivate_elements(request=request, pk=MagicMock()) - - def test_clear_elements(self, mock_object): - mock_object.return_value.config = { - 'a852863cc17d4ef1881b3f82615cfa0d': { - 'name': 'test_projector_element_iphuuM6Weec6oohooxug', - 'test_key_bi7ur1UoB1eehiloh7mi': 'test_value_jieTh6aiwoo8eig1AeSa'}} - request = MagicMock() - self.viewset.request = request - self.viewset.clear_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 0) - - def test_clear_elements_with_stable(self, mock_object): - mock_object.return_value.config = { - 'dcd2e12ae31a478a8b9c3855798270af': { - 'name': 'test_projector_element_6oohooxugiphuuM6Weec', - 'test_key_bi7B1eehiloh7miur1Uo': 'test_value_jiSaeTh6aiwoo8eig1Ae', - 'stable': True}} - request = MagicMock() - self.viewset.request = request - self.viewset.clear_elements(request=request, pk=MagicMock()) - self.assertEqual(len(mock_object.return_value.config), 1) - - -class WebclientJavaScriptView(TestCase): - def setUp(self): - self.request = MagicMock() - - @patch('openslides.core.config.config') - @patch('django.contrib.auth.models.Permission.objects.all') - def test_permissions_as_constant(self, mock_permissions_all, mock_config): - mock_config.__getitem__.return_value = '' - self.view_instance = views.WebclientJavaScriptView() - self.view_instance.request = self.request - response = self.view_instance.get(realm='site') - self.assertEqual(response.status_code, 200) - self.assertEqual(mock_permissions_all.call_count, 2) diff --git a/tests/unit/motions/test_views.py b/tests/unit/motions/test_views.py index fd2a2ed53..06275498a 100644 --- a/tests/unit/motions/test_views.py +++ b/tests/unit/motions/test_views.py @@ -4,31 +4,6 @@ from unittest.mock import MagicMock, patch from openslides.motions.views import MotionViewSet -class MotionViewSetCreate(TestCase): - """ - Tests create view of MotionViewSet. - """ - def setUp(self): - self.request = MagicMock() - self.request.data.get.return_value = None - self.request.user.is_authenticated.return_value = False - self.view_instance = MotionViewSet() - self.view_instance.request = self.request - self.view_instance.format_kwarg = MagicMock() - self.view_instance.get_serializer = get_serializer_mock = MagicMock() - get_serializer_mock.return_value = self.mock_serializer = MagicMock() - - @patch('openslides.motions.views.inform_changed_data') - @patch('openslides.motions.views.has_perm') - @patch('openslides.motions.views.config') - def test_simple_create(self, mock_config, mock_has_perm, mock_icd): - mock_has_perm.return_value = True - - self.view_instance.create(self.request) - - self.mock_serializer.save.assert_called_with(request_user=self.request.user) - - class MotionViewSetUpdate(TestCase): """ Tests update view of MotionViewSet.