From 756ddf3add86c50661d46ab14dd72c0f78515239 Mon Sep 17 00:00:00 2001 From: Ian Ross Date: Fri, 11 Mar 2016 22:26:16 +0100 Subject: [PATCH] Update for new tutelary functionality Also small change to get tests working, due to upgrade in djoser, which now tests SEND_ACTIVATION_EMAIL setting before making new users active. --- .travis.yml | 2 +- app/src/js/settings.js | 2 +- app/src/locale/gettext.po | 216 ++++++++++-------- cadasta/config/settings/dev.py | 1 + cadasta/config/settings/travis.py | 4 + cadasta/core/views.py | 50 ---- cadasta/organization/models.py | 6 +- cadasta/organization/tests/test_views.py | 20 +- cadasta/organization/views.py | 35 +-- .../roles/cadasta/application/tasks/main.yml | 10 +- requirements/common.txt | 4 +- 11 files changed, 168 insertions(+), 182 deletions(-) diff --git a/.travis.yml b/.travis.yml index cdb087cca..1bd4bf7c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ --- language: python python: - - "3.4" + - "3.5" services: - postgresql addons: diff --git a/app/src/js/settings.js b/app/src/js/settings.js index 8f56e4906..a69fa65f7 100644 --- a/app/src/js/settings.js +++ b/app/src/js/settings.js @@ -1,5 +1,5 @@ const SETTINGS = { - API_BASE: 'http://localhost:5000/v1' + API_BASE: 'http://0.0.0.0:5000/v1' }; export default SETTINGS; diff --git a/app/src/locale/gettext.po b/app/src/locale/gettext.po index 1cd194a35..2cbd58c98 100644 --- a/app/src/locale/gettext.po +++ b/app/src/locale/gettext.po @@ -3,102 +3,6 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Plural-Forms: nplurals = 2; plural = (n !== 1);\n" -#: src/js/account/components/Login.jsx:48 -msgid "Sign in to your account" -msgstr "" - -#: src/js/account/components/Profile.jsx:60 -msgid "Username" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:88 -msgid "This field is required" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:67 -msgid "Password" -msgstr "" - -#: src/js/account/components/Login.jsx:77 -msgid "Forgotten password?" -msgstr "" - -#: src/js/account/components/Login.jsx:84 -msgid "Remember me" -msgstr "" - -#: src/js/account/components/Login.jsx:93 -msgid "Sign in" -msgstr "" - -#: src/js/account/components/Profile.jsx:54 -msgid "Update your profile" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:53 -msgid "Email" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:57 -msgid "Please provide a valid email address" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:98 -msgid "First name" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:107 -msgid "Last name" -msgstr "" - -#: src/js/account/components/Profile.jsx:112 -msgid "Update profile" -msgstr "" - -#: src/js/account/components/Profile.jsx:115 -msgid "Password options" -msgstr "" - -#: src/js/account/components/Password.jsx:84 -msgid "Change password" -msgstr "" - -#: src/js/account/components/PasswordResetConfirm.jsx:81 -msgid "Reset password" -msgstr "" - -#: src/js/account/components/Password.jsx:34 -msgid "Change your password" -msgstr "" - -#: src/js/account/components/Password.jsx:40 -msgid "Current password" -msgstr "" - -#: src/js/account/components/PasswordResetConfirm.jsx:50 -msgid "New password" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:72 -msgid "Your password must be at least 6 characters long." -msgstr "" - -#: src/js/account/components/PasswordResetConfirm.jsx:65 -msgid "Repeat new password" -msgstr "" - -#: src/js/account/components/RegistrationForm.jsx:87 -msgid "Passwords must match." -msgstr "" - -#: src/js/account/components/PasswordReset.jsx:34 -msgid "Reset your password" -msgstr "" - -#: src/js/account/components/PasswordReset.jsx:41 -msgid "Enter email" -msgstr "" - #: src/js/messages/reducer.js:10 msgid "Unable to login with provided username and password." msgstr "" @@ -165,8 +69,24 @@ msgstr "" msgid "Account successfully activated." msgstr "" -#: src/js/account/components/PasswordResetConfirm.jsx:44 -msgid "Create a new password" +#: src/js/core/components/Footer.jsx:12 +msgid "About Us" +msgstr "" + +#: src/js/core/components/Footer.jsx:13 +msgid "Privacy" +msgstr "" + +#: src/js/core/components/Footer.jsx:14 +msgid "Terms of Service" +msgstr "" + +#: src/js/core/components/Footer.jsx:15 +msgid "Code of Conduct" +msgstr "" + +#: src/js/core/components/Footer.jsx:16 +msgid "Visit us on Github" msgstr "" #: src/js/core/components/Header.jsx:23 @@ -193,6 +113,106 @@ msgstr "" msgid "Choose username" msgstr "" +#: src/js/account/components/PasswordResetConfirm.jsx:71 +msgid "This field is required" +msgstr "" + +#: src/js/account/components/Profile.jsx:75 +msgid "Email" +msgstr "" + +#: src/js/account/components/PasswordReset.jsx:46 +msgid "Please provide a valid email address" +msgstr "" + +#: src/js/account/components/Login.jsx:67 +msgid "Password" +msgstr "" + +#: src/js/account/components/PasswordResetConfirm.jsx:55 +msgid "Your password must be at least 6 characters long." +msgstr "" + #: src/js/account/components/RegistrationForm.jsx:82 msgid "Confirm password" +msgstr "" + +#: src/js/account/components/PasswordResetConfirm.jsx:70 +msgid "Passwords must match." +msgstr "" + +#: src/js/account/components/Profile.jsx:91 +msgid "First name" +msgstr "" + +#: src/js/account/components/Profile.jsx:101 +msgid "Last name" +msgstr "" + +#: src/js/account/components/Login.jsx:48 +msgid "Sign in to your account" +msgstr "" + +#: src/js/account/components/Profile.jsx:60 +msgid "Username" +msgstr "" + +#: src/js/account/components/Login.jsx:77 +msgid "Forgotten password?" +msgstr "" + +#: src/js/account/components/Login.jsx:84 +msgid "Remember me" +msgstr "" + +#: src/js/account/components/Login.jsx:93 +msgid "Sign in" +msgstr "" + +#: src/js/account/components/Profile.jsx:54 +msgid "Update your profile" +msgstr "" + +#: src/js/account/components/Profile.jsx:112 +msgid "Update profile" +msgstr "" + +#: src/js/account/components/Profile.jsx:115 +msgid "Password options" +msgstr "" + +#: src/js/account/components/Password.jsx:84 +msgid "Change password" +msgstr "" + +#: src/js/account/components/PasswordResetConfirm.jsx:81 +msgid "Reset password" +msgstr "" + +#: src/js/account/components/Password.jsx:34 +msgid "Change your password" +msgstr "" + +#: src/js/account/components/Password.jsx:40 +msgid "Current password" +msgstr "" + +#: src/js/account/components/PasswordResetConfirm.jsx:50 +msgid "New password" +msgstr "" + +#: src/js/account/components/PasswordResetConfirm.jsx:65 +msgid "Repeat new password" +msgstr "" + +#: src/js/account/components/PasswordReset.jsx:34 +msgid "Reset your password" +msgstr "" + +#: src/js/account/components/PasswordReset.jsx:41 +msgid "Enter email" +msgstr "" + +#: src/js/account/components/PasswordResetConfirm.jsx:44 +msgid "Create a new password" msgstr "" \ No newline at end of file diff --git a/cadasta/config/settings/dev.py b/cadasta/config/settings/dev.py index b995067b9..f8402c024 100644 --- a/cadasta/config/settings/dev.py +++ b/cadasta/config/settings/dev.py @@ -22,6 +22,7 @@ DJOSER.update({ 'DOMAIN': 'localhost:8080', + 'SEND_ACTIVATION_EMAIL': False, }) # devserver settings diff --git a/cadasta/config/settings/travis.py b/cadasta/config/settings/travis.py index 0bf4b6d77..b4ae63dd7 100644 --- a/cadasta/config/settings/travis.py +++ b/cadasta/config/settings/travis.py @@ -14,3 +14,7 @@ 'HOST': '', } } + +DJOSER.update({ + 'SEND_ACTIVATION_EMAIL': False, +}) diff --git a/cadasta/core/views.py b/cadasta/core/views.py index 8e7c23ec3..f3d0dfdf3 100644 --- a/cadasta/core/views.py +++ b/cadasta/core/views.py @@ -2,15 +2,12 @@ import re from django.http import Http404 -from django.core.exceptions import ImproperlyConfigured from django.utils import six from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import NotFound from rest_framework.views import exception_handler as drf_exception_handler -from tutelary.engine import Object - def set_exception(exception): if (type(exception) is Http404): @@ -53,50 +50,3 @@ def exception_handler(exception, context): response.data = eval_json(response.data) return response - - -class PermissionRequiredMixin: - def get_permission_required(self): - """ - Override this method to override the permission_required attribute. - Must return an iterable. - """ - if self.permission_required is None: - raise ImproperlyConfigured( - '{0} is missing the permission_required attribute. Define ' - '{0}.permission_required, or override ' - '{0}.get_permission_required().'.format( - self.__class__.__name__) - ) - if isinstance(self.permission_required, dict): - perms = self.permission_required[self.request.method] - else: - perms = self.permission_required - - if isinstance(perms, six.string_types): - perms = (perms, ) - - if hasattr(self, 'add_permission_required'): - perms = perms + self.add_permission_required - - return perms - - def check_permissions(self, request): - obj = None - allowed = {} - if hasattr(self, 'model') and hasattr(self.model, 'TutelaryMeta'): - obj = Object(self.model.TutelaryMeta.perm_type) - allowed = self.model.TutelaryMeta.allowed_methods - try: - if hasattr(self, 'get_object') and self.get_object() is not None: - obj = self.get_object().get_permissions_object() - except: - pass - perms = self.get_permission_required() - - has_perm = all(self.request.user.has_perm(p, obj) for p in perms - if not (p in allowed and - self.request.method in allowed[p])) - - if not has_perm: - self.permission_denied(request, message="Permission denied.") diff --git a/cadasta/organization/models.py b/cadasta/organization/models.py index 119755871..6e39a38ea 100644 --- a/cadasta/organization/models.py +++ b/cadasta/organization/models.py @@ -21,9 +21,11 @@ class Organization(RandomIDModel): class TutelaryMeta: perm_type = 'organization' path_fields = ('slug',) - actions = (('org.list', "List existing organisations"), + actions = (('org.list', {'description': "List existing organisations", + 'permissions_object': None}), ('org.view', "View existing organisations"), - ('org.create', "Create organisations"), + ('org.create', {'description': "Create organisations", + 'permissions_object': None}), ('org.update', "Update an existing organization"), ('org.archive', "Archive an existing organization"), ('org.unarchive', "Unarchive an existing organization"), diff --git a/cadasta/organization/tests/test_views.py b/cadasta/organization/tests/test_views.py index cf2e58a8d..3ee593c49 100644 --- a/cadasta/organization/tests/test_views.py +++ b/cadasta/organization/tests/test_views.py @@ -4,6 +4,7 @@ from django.http import QueryDict from django.contrib.auth.models import AnonymousUser from rest_framework.test import APIRequestFactory, force_authenticate +from rest_framework.exceptions import PermissionDenied from tutelary.models import Policy, assign_user_policies from accounts.tests.factories import UserFactory @@ -89,7 +90,7 @@ def test_full_list(self): def test_full_list_with_unautorized_user(self): """ - It should 403 Permission denied. + It should 403 "You do not have permission to perform this action." """ OrganizationFactory.create_batch(2) request = APIRequestFactory().get('/v1/organizations/') @@ -99,7 +100,7 @@ def test_full_list_with_unautorized_user(self): content = json.loads(response.content.decode('utf-8')) assert response.status_code == 403 - assert content['detail'] == "Permission denied." + assert content['detail'] == PermissionDenied.default_detail def test_filter_active(self): """ @@ -256,6 +257,7 @@ def test_create_organization_with_unauthorized_user(self): assign_user_policies(unauthorized_user, policy) data = { + 'name': 'new_org', 'description': 'Org description' } request = APIRequestFactory().post('/v1/organizations/', data) @@ -265,7 +267,7 @@ def test_create_organization_with_unauthorized_user(self): content = json.loads(response.content.decode('utf-8')) assert response.status_code == 403 - assert content['detail'] == 'Permission denied.' + assert content['detail'] == PermissionDenied.default_detail assert Organization.objects.count() == 0 @@ -317,7 +319,7 @@ def test_get_organization_with_unauthorized_user(self): content = json.loads(response.content.decode('utf-8')) assert response.status_code == 403 - assert content['detail'] == "Permission denied." + assert content['detail'] == PermissionDenied.default_detail def test_get_organization_that_does_not_exist(self): request = APIRequestFactory().get('/v1/organizations/some-org/') @@ -495,7 +497,7 @@ def setUp(self): }, { "effect": "allow", "object": ["organization/*"], - "action": ["org.*"] + "action": ["org.*", "org.*.*"] } ] } @@ -533,7 +535,7 @@ def test_get_users_with_unauthorized_user(self): content = json.loads(response.content.decode('utf-8')) assert response.status_code == 403 - assert content['detail'] == 'Permission denied.' + assert content['detail'] == PermissionDenied.default_detail def test_add_user(self): org_users = UserFactory.create_batch(2) @@ -566,7 +568,7 @@ def test_add_user_with_unauthorized_user(self): content = json.loads(response.content.decode('utf-8')) assert response.status_code == 403 - assert content['detail'] == 'Permission denied.' + assert content['detail'] == PermissionDenied.default_detail assert org.users.count() == 2 def test_add_user_that_does_not_exist(self): @@ -615,7 +617,7 @@ def setUp(self): }, { "effect": "allow", "object": ["organization/*"], - "action": ["org.*"] + "action": ["org.*", "org.*.*"] } ] } @@ -665,7 +667,7 @@ def test_remove_with_unauthorized_user(self): assert response.status_code == 403 assert org.users.count() == 2 - assert content['detail'] == 'Permission denied.' + assert content['detail'] == PermissionDenied.default_detail def test_remove_user_that_does_not_exist(self): user = UserFactory.create() diff --git a/cadasta/organization/views.py b/cadasta/organization/views.py index 9d82e7c9c..94708b36f 100644 --- a/cadasta/organization/views.py +++ b/cadasta/organization/views.py @@ -2,7 +2,7 @@ from rest_framework import generics from rest_framework import filters, status -from core.views import PermissionRequiredMixin +from tutelary.mixins import PermissionRequiredMixin from accounts.serializers import UserSerializer from accounts.models import User from .models import Organization @@ -27,28 +27,23 @@ class OrganizationList(PermissionRequiredMixin, generics.ListCreateAPIView): class OrganizationDetail(PermissionRequiredMixin, generics.RetrieveUpdateAPIView): - queryset = Organization.objects.all() - serializer_class = OrganizationSerializer - lookup_field = 'slug' - permission_required = { - 'GET': 'org.view', - 'PATCH': 'org.update', - } - - def initial(self, request, *args, **kwargs): + def patch_actions(self, request): if hasattr(request, 'data'): is_archived = self.get_object().archived new_archived = request.data.get('archived', is_archived) - if not is_archived and (is_archived != new_archived): - # Add required permission when archiving - self.add_permission_required = ('org.archive', ) + return ('org.update', 'org.archive') elif is_archived and (is_archived != new_archived): - # Add required permission when unarchiving - self.add_permission_required = ('org.unarchive', ) + return ('org.update', 'org.unarchive') + return 'org.update' - return super(OrganizationDetail, self).initial( - request, *args, **kwargs) + queryset = Organization.objects.all() + serializer_class = OrganizationSerializer + lookup_field = 'slug' + permission_required = { + 'GET': 'org.view', + 'PATCH': patch_actions, + } class OrganizationUsers(PermissionRequiredMixin, @@ -60,6 +55,9 @@ class OrganizationUsers(PermissionRequiredMixin, 'POST': 'org.users.add', } + def get_perms_objects(self): + return [self.get_organization(self.kwargs['slug'])] + def create(self, request, *args, **kwargs): try: new_user = User.objects.get(username=request.POST['username']) @@ -83,6 +81,9 @@ class OrganizationUsersDetail(PermissionRequiredMixin, generics.DestroyAPIView): permission_required = 'org.users.remove' + def get_perms_objects(self): + return [self.get_organization(self.kwargs['slug'])] + def destroy(self, request, *args, **kwargs): user = self.get_object() self.org.users.remove(user) diff --git a/provision/roles/cadasta/application/tasks/main.yml b/provision/roles/cadasta/application/tasks/main.yml index c51cc6602..8d70d3f72 100644 --- a/provision/roles/cadasta/application/tasks/main.yml +++ b/provision/roles/cadasta/application/tasks/main.yml @@ -1,9 +1,15 @@ +- name: Add deadsnakes Python repository + become: yes + become_user: root + apt_repository: repo='ppa:fkrull/deadsnakes' + - name: Install packages become: yes become_user: root apt: pkg={{ item }} state=installed update_cache=yes with_items: - - python3-dev + - python3.5 + - python3.5-dev - python-virtualenv - git @@ -16,7 +22,7 @@ - name: Manually create the initial virtualenv become: yes become_user: "{{ app_user }}" - command: virtualenv {{ virtualenv_path }} --python=python3 + command: virtualenv {{ virtualenv_path }} --python=python3.5 creates="{{ virtualenv_path }}bin" - name: Install requirements diff --git a/requirements/common.txt b/requirements/common.txt index 628e65033..97d0737bb 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,11 +1,11 @@ Django==1.9.2 psycopg2==2.6.1 -djoser==0.4.0 +djoser==0.4.3 django-cors-headers==1.1.0 django-filter==0.12.0 django-crispy-forms==1.6.0 jsonschema==2.5.1 rfc3987==1.3.5 --e git+https://github.com/Cadasta/django-tutelary.git#egg=tutelary +django-tutelary==0.1.3 django-audit-log==0.7.0 simplejson==3.8.1