From 9e1b4ac5ac905cb61bf87cfc90d3c02a58556bdf Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Mon, 11 May 2020 12:47:20 -0600 Subject: [PATCH 01/13] users apiv3 working --- seed/api/v3/urls.py | 4 + seed/landing/models.py | 4 + seed/utils/api_schema.py | 7 +- seed/views/v3/users.py | 887 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 seed/views/v3/users.py diff --git a/seed/api/v3/urls.py b/seed/api/v3/urls.py index 1882f228c8..37b3f35b0e 100644 --- a/seed/api/v3/urls.py +++ b/seed/api/v3/urls.py @@ -6,10 +6,14 @@ from seed.views.v3.data_quality import DataQualityViews from seed.views.v3.datasets import DatasetViewSet +from seed.views.v3.users import UserViewSet + api_v3_router = routers.DefaultRouter() api_v3_router.register(r'data_quality_checks', DataQualityViews, base_name='data_quality_checks') api_v3_router.register(r'datasets', DatasetViewSet, base_name='datasets') +api_v3_router.register(r'users', UserViewSet, base_name='user') + urlpatterns = [ url(r'^', include(api_v3_router.urls)), diff --git a/seed/landing/models.py b/seed/landing/models.py index be5d2b9dc2..7cfa94c298 100644 --- a/seed/landing/models.py +++ b/seed/landing/models.py @@ -124,6 +124,10 @@ def process_header_request(cls, request): def get_absolute_url(self): return "/users/%s/" % urlquote(self.username) + def deactivate_user(self): + self.is_active = False + self.save() + def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. diff --git a/seed/utils/api_schema.py b/seed/utils/api_schema.py index 50035dc6d1..f3bb19d452 100644 --- a/seed/utils/api_schema.py +++ b/seed/utils/api_schema.py @@ -12,7 +12,12 @@ class AutoSchemaHelper(SwaggerAutoSchema): type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_INTEGER) ), - 'string': openapi.Schema(type=openapi.TYPE_STRING) + 'string': openapi.Schema(type=openapi.TYPE_STRING), + + 'string_array': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema(type=openapi.TYPE_STRING) + ) } def base_field(self, name, location_attr, description, required, type): diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py new file mode 100644 index 0000000000..60847ba25b --- /dev/null +++ b/seed/views/v3/users.py @@ -0,0 +1,887 @@ +# !/usr/bin/env python +# encoding: utf-8 +""" +:copyright (c) 2014 - 2020, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Department of Energy) and contributors. All rights reserved. # NOQA +:author +""" +import logging + +from django.contrib.auth.password_validation import validate_password +from django.contrib.auth.tokens import default_token_generator +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError +from django.http import JsonResponse +from rest_framework import viewsets, status, serializers +from rest_framework.decorators import action + +from seed.decorators import ajax_request_class +from seed.landing.models import SEEDUser as User, SEEDUser +from seed.lib.superperms.orgs.decorators import PERMS +from seed.lib.superperms.orgs.decorators import has_perm_class +from seed.lib.superperms.orgs.models import ( + ROLE_OWNER, + ROLE_MEMBER, + ROLE_VIEWER, + Organization, + OrganizationUser, +) +from seed.models.data_quality import Rule +from seed.tasks import ( + invite_to_seed, +) +from seed.utils.api import api_endpoint_class +from seed.utils.api_schema import AutoSchemaHelper +from seed.utils.organizations import create_organization + +_log = logging.getLogger(__name__) + + +def _get_js_role(role): + """return the JS friendly role name for user + :param role: role as defined in superperms.models + :returns: (string) JS role name + """ + roles = { + ROLE_OWNER: 'owner', + ROLE_VIEWER: 'viewer', + ROLE_MEMBER: 'member', + } + return roles.get(role, 'viewer') + + +def _get_role_from_js(role): + """return the OrganizationUser role_level from the JS friendly role name + + :param role: 'member', 'owner', or 'viewer' + :returns: int role as defined in superperms.models + """ + roles = { + 'owner': ROLE_OWNER, + 'viewer': ROLE_VIEWER, + 'member': ROLE_MEMBER, + } + return roles[role] + + +def _get_js_rule_type(data_type): + """return the JS friendly data type name for the data data_quality rule + + :param data_type: data data_quality rule data type as defined in data_quality.models + :returns: (string) JS data type name + """ + return dict(Rule.DATA_TYPES).get(data_type) + + +def _get_rule_type_from_js(data_type): + """return the Rules TYPE from the JS friendly data type + + :param data_type: 'string', 'number', 'date', or 'year' + :returns: int data type as defined in data_quality.models + """ + d = {v: k for k, v in dict(Rule.DATA_TYPES).items()} + return d.get(data_type) + + +def _get_js_rule_severity(severity): + """return the JS friendly severity name for the data data_quality rule + + :param severity: data data_quality rule severity as defined in data_quality.models + :returns: (string) JS severity name + """ + return dict(Rule.SEVERITY).get(severity) + + +def _get_severity_from_js(severity): + """return the Rules SEVERITY from the JS friendly severity + + :param severity: 'error', or 'warning' + :returns: int severity as defined in data_quality.models + """ + d = {v: k for k, v in dict(Rule.SEVERITY).items()} + return d.get(severity) + + +class EmailAndIDSerializer(serializers.Serializer): + email = serializers.CharField(max_length=100) + user_id = serializers.IntegerField() + + +class ListUsersResponseSerializer(serializers.Serializer): + users = EmailAndIDSerializer(many=True) + +class UserSchema(AutoSchemaHelper): + def __init__(self, *args): + super().__init__(*args) + + self.manual_fields = { + ('GET', 'list'): [], + ('POST', 'create'): [ + self.org_id_field(), + self.body_field( + name='New User Fields', + required=True, + description="An object containing meta data for a new user: " + "required - [last_name, role(viewer/owner/member), email]", + params_to_formats={ + 'first_name': 'string', + 'last_name': 'string', + 'role': 'string', + 'email': 'string' + } + )], + ('GET', 'current_user_id'): [], + ('GET', 'retrieve'): [self.path_id_field(description="users PK ID")], + ('PUT', 'update'): [ + self.path_id_field(description="Updated Users PK ID"), + self.body_field( + name='Updated User Fields', + required=True, + description="An object containing meta data for a updated user: " + "required - [first_name, last_name, email]", + params_to_formats={ + 'first_name': 'string', + 'last_name': 'string', + 'email': 'string' + } + )], + ('PUT', 'default_organization'): [self.org_id_field(), self.path_id_field(description="Updated Users PK ID")], + ('POST', 'is_authorized'): [ + self.org_id_field(), + self.path_id_field(description="Users PK ID"), + self.body_field( + name='actions', + required=True, + description="A list of actions to check: examples include (requires_parent_org_owner, " + "requires_owner, requires_member, requires_viewer, " + "requires_superuser, can_create_sub_org, can_remove_org)", + params_to_formats={ + 'actions': 'string_array', + }) + ], + ('PUT', 'set_password'): [ + self.body_field( + name='Change Password', + required=True, + description="fill in the current and new matching passwords ", + params_to_formats={ + 'current_password': 'string', + 'password_1': 'string', + 'password_2': 'string' + }) + ], + ('PUT', 'role'): [ + self.org_id_field(), + self.path_id_field(description="Users PK ID"), + self.body_field( + name='role', + required=True, + description="fill in the role to be updated", + params_to_formats={ + 'role': 'string', + + }) + ], + ('DELETE', 'deactivate'): [ + self.body_field( + name='User to be deactivated', + required=True, + description="first and last name of user to be deactivated", + params_to_formats={ + 'first_name': 'string', + 'last_name': 'string' + }) + + ] + + + + } + + +class UserViewSet(viewsets.ViewSet): + raise_exception = True + + swagger_schema = UserSchema + + def validate_request_user(self, pk, request): + try: + user = User.objects.get(pk=pk) + except ObjectDoesNotExist: + return False, JsonResponse( + {'status': 'error', 'message': "Could not find user with pk = " + str(pk)}, + status=status.HTTP_404_NOT_FOUND) + if not user == request.user: + return False, JsonResponse( + {'status': 'error', 'message': "Cannot access user with pk = " + str(pk)}, + status=status.HTTP_403_FORBIDDEN) + return True, user + + @api_endpoint_class + @ajax_request_class + @has_perm_class('requires_owner') + def create(self, request): + """ + Creates a new SEED user. One of 'organization_id' or 'org_name' is needed. + Sends invitation email to the new user. + --- + parameters: + - name: organization_id + description: Organization ID if adding user to an existing organization + required: false + type: integer + - name: org_name + description: New organization name if creating a new organization for this user + required: false + type: string + - name: first_name + description: First name of new user + required: true + type: string + - name: last_name + description: Last name of new user + required: true + type: string + - name: role + description: one of owner, member, or viewer + required: true + type: string + - name: email + description: Email address of the new user + required: true + type: string + type: + status: + description: success or error + required: true + type: string + message: + description: email address of new user + required: true + type: string + org: + description: name of the new org (or existing org) + required: true + type: string + org_created: + description: True if new org created + required: true + type: string + username: + description: Username of new user + required: true + type: string + user_id: + description: User ID (pk) of new user + required: true + type: integer + """ + body = request.data + org_name = body.get('org_name') + org_id = request.query_params.get('organization_id', None) + if (org_name and org_id) or (not org_name and not org_id): + return JsonResponse({ + 'status': 'error', + 'message': 'Choose either an existing org or provide a new one' + }, status=status.HTTP_409_CONFLICT) + + first_name = body['first_name'] + last_name = body['last_name'] + email = body['email'] + username = body['email'] + user, created = User.objects.get_or_create(username=username.lower()) + + if org_id: + org = Organization.objects.get(pk=org_id) + org_created = False + else: + org, _, _ = create_organization(user, org_name) + org_created = True + + # Add the user to the org. If this is the org's first user, + # the user becomes the owner/admin automatically. + # see Organization.add_member() + if not org.is_member(user): + org.add_member(user) + + if body.get('role'): + # check if this is a dict, if so, grab the value out of 'value' + role = body['role'] + if isinstance(role, dict): + role = role['value'] + + OrganizationUser.objects.filter( + organization_id=org.pk, + user_id=user.pk + ).update(role_level=_get_role_from_js(role)) + + if created: + user.set_unusable_password() + user.email = email + user.first_name = first_name + user.last_name = last_name + user.save() + + try: + domain = request.get_host() + except Exception: + domain = 'seed-platform.org' + invite_to_seed( + domain, user.email, default_token_generator.make_token(user), user.pk, first_name + ) + + return JsonResponse({ + 'status': 'success', + 'message': user.email, + 'org': org.name, + 'org_created': org_created, + 'username': user.username, + 'user_id': user.id + }) + + @ajax_request_class + @has_perm_class('requires_superuser') + def list(self, request): + """ + Retrieves all users' email addresses and IDs. + Only usable by superusers. + --- + response_serializer: ListUsersResponseSerializer + """ + users = [] + for user in User.objects.only('id', 'email'): + users.append({'email': user.email, 'user_id': user.id}) + return JsonResponse({'users': users}) + + @ajax_request_class + @api_endpoint_class + @action(detail=False, methods=['GET']) + def current(self, request): + """ + Returns the id (primary key) for the current user to allow it + to be passed to other user related endpoints + --- + type: + pk: + description: Primary key for the current user + required: true + type: string + """ + return JsonResponse({'pk': request.user.id}) + + @api_endpoint_class + @ajax_request_class + @has_perm_class('requires_owner') + @action(detail=True, methods=['PUT']) + def role(self, request, pk=None): + """ + Updates a user's role within an organization. + --- + parameter_strategy: replace + parameters: + - name: pk + description: ID for the user to modify + type: integer + required: true + paramType: path + - name: organization_id + description: The organization ID to update this user within + type: integer + required: true + - name: role + description: one of owner, member, or viewer + type: string + required: true + type: + status: + required: true + description: success or error + type: string + message: + required: false + description: error message, if any + type: string + """ + body = request.data + role = _get_role_from_js(body['role']) + + user_id = pk + organization_id = request.query_params.get('organization_id', None) + + is_last_member = not OrganizationUser.objects.filter( + organization_id=organization_id, + ).exclude(user_id=user_id).exists() + + if is_last_member: + return JsonResponse({ + 'status': 'error', + 'message': 'an organization must have at least one member' + }, status=status.HTTP_409_CONFLICT) + + is_last_owner = not OrganizationUser.objects.filter( + organization_id=organization_id, + role_level=ROLE_OWNER, + ).exclude(user_id=user_id).exists() + + if is_last_owner: + return JsonResponse({ + 'status': 'error', + 'message': 'an organization must have at least one owner level member' + }, status=status.HTTP_409_CONFLICT) + + OrganizationUser.objects.filter( + user_id=user_id, + organization_id=body['organization_id'] + ).update(role_level=role) + + return JsonResponse({'status': 'success'}) + + @api_endpoint_class + @ajax_request_class + def retrieve(self, request, pk=None): + """ + Retrieves the a user's first_name, last_name, email + and api key if exists by user ID (pk). + --- + parameter_strategy: replace + parameters: + - name: pk + description: User ID / primary key + type: integer + required: true + paramType: path + type: + status: + description: success or error + type: string + required: true + first_name: + description: user first name + type: string + required: true + last_name: + description: user last name + type: string + required: true + email: + description: user email + type: string + required: true + api_key: + description: user API key + type: string + required: true + """ + + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + return JsonResponse({ + 'status': 'success', + 'first_name': user.first_name, + 'last_name': user.last_name, + 'email': user.email, + 'api_key': user.api_key, + }) + + @ajax_request_class + @action(detail=True, methods=['POST']) + def generate_api_key(self, request, pk=None): + """ + Generates a new API key + --- + parameter_strategy: replace + parameters: + - name: pk + description: User ID / primary key + type: integer + required: true + paramType: path + type: + status: + description: success or error + type: string + required: true + api_key: + description: the new API key for this user + type: string + required: true + """ + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + user.generate_key() + return { + 'status': 'success', + 'api_key': User.objects.get(pk=pk).api_key + } + + @api_endpoint_class + @ajax_request_class + def update(self, request, pk=None): + """ + Updates the user's first name, last name, and email + --- + parameter_strategy: replace + parameters: + - name: pk + description: User ID / primary key + type: integer + required: true + paramType: path + - name: first_name + description: New first name + type: string + required: true + - name: last_name + description: New last name + type: string + required: true + - name: email + description: New user email + type: string + required: true + type: + status: + description: success or error + type: string + required: true + first_name: + description: user first name + type: string + required: true + last_name: + description: user last name + type: string + required: true + email: + description: user email + type: string + required: true + api_key: + description: user API key + type: string + required: true + """ + body = request.data + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + json_user = body + user.first_name = json_user.get('first_name') + user.last_name = json_user.get('last_name') + user.email = json_user.get('email') + user.username = json_user.get('email') + user.save() + return JsonResponse({ + 'status': 'success', + 'first_name': user.first_name, + 'last_name': user.last_name, + 'email': user.email, + 'api_key': user.api_key, + }) + + @ajax_request_class + @action(detail=True, methods=['PUT']) + def set_password(self, request, pk=None): + """ + sets/updates a user's password, follows the min requirement of + django password validation settings in config/settings/common.py + --- + parameter_strategy: replace + parameters: + - name: current_password + description: Users current password + type: string + required: true + - name: password_1 + description: Users new password 1 + type: string + required: true + - name: password_2 + description: Users new password 2 + type: string + required: true + type: + status: + type: string + description: success or error + required: true + message: + type: string + description: error message, if any + required: false + """ + body = request.data + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + current_password = body.get('current_password') + p1 = body.get('password_1') + p2 = body.get('password_2') + if not user.check_password(current_password): + return JsonResponse({'status': 'error', 'message': 'current password is not valid'}, + status=status.HTTP_400_BAD_REQUEST) + if p1 is None or p1 != p2: + return JsonResponse({'status': 'error', 'message': 'entered password do not match'}, + status=status.HTTP_400_BAD_REQUEST) + try: + validate_password(p2) + except ValidationError as e: + return JsonResponse({'status': 'error', 'message': e.messages[0]}, + status=status.HTTP_400_BAD_REQUEST) + user.set_password(p1) + user.save() + return JsonResponse({'status': 'success'}) + + @ajax_request_class + def get_actions(self, request): + """returns all actions""" + return { + 'status': 'success', + 'actions': list(PERMS.keys()), + } + + @ajax_request_class + @action(detail=True, methods=['POST']) + def is_authorized(self, request, pk=None): + """ + Checks the auth for a given action, if user is the owner of the parent + org then True is returned for each action + --- + parameter_strategy: replace + parameters: + - name: pk + description: User ID (primary key) + type: integer + required: true + paramType: path + - name: organization_id + description: ID (primary key) for organization + type: integer + required: true + paramType: query + - name: actions + type: array[string] + required: true + description: a list of actions to check + type: + status: + type: string + description: success or error + required: true + message: + type: string + description: error message, if any + required: false + auth: + type: object + description: a dict of with keys equal to the actions, and values as bool + required: true + """ + actions, org, error, message = self._parse_is_authenticated_params(request) + if error: + return JsonResponse({ + 'status': 'error', + 'message': message + }, status=status.HTTP_400_BAD_REQUEST) + + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + + # If the only action requested is 'requires_superuser' no need to check an org affiliation + if len(actions) == 1 and actions[0] == 'requires_superuser': + return JsonResponse( + {'status': 'success', 'auth': {'requires_superuser': user.is_superuser}}) + + auth = self._try_parent_org_auth(user, org, actions) + if auth: + return JsonResponse({'status': 'success', 'auth': auth}) + + try: + ou = OrganizationUser.objects.get(user=user, organization=org) + except OrganizationUser.DoesNotExist: + return JsonResponse({'status': 'error', 'message': 'user does not exist'}) + + auth = {action: PERMS[action](ou) for action in actions} + return JsonResponse({'status': 'success', 'auth': auth}) + + def _parse_is_authenticated_params(self, request): + """checks if the org exists and if the actions are present + + :param request: the request + :returns: tuple (actions, org, error, message) + """ + error = False + message = "" + body = request.data + if not body.get('actions'): + message = 'no actions to check' + error = True + + org_id = request.query_params.get('organization_id', None) + if org_id == '': + message = 'organization id is undefined' + error = True + org = None + else: + try: + org = Organization.objects.get(pk=org_id) + except Organization.DoesNotExist: + message = 'organization does not exist' + error = True + org = None + + return body.get('actions'), org, error, message + + def _try_parent_org_auth(self, user, organization, actions): + """checks the parent org for permissions, if the user is not the owner of + the parent org, then None is returned. + + :param user: the request user + :param organization: org to check its parent + :param actions: list of str actions to check + :returns: a dict of action permission resolutions or None + """ + try: + ou = OrganizationUser.objects.get( + user=user, + organization=organization.parent_org, + role_level=ROLE_OWNER + ) + except OrganizationUser.DoesNotExist: + return None + + return { + action: PERMS['requires_owner'](ou) for action in actions + } + + @ajax_request_class + @action(detail=True, methods=['GET']) + def shared_buildings(self, request, pk=None): + """ + Get the request user's ``show_shared_buildings`` attr + --- + parameter_strategy: replace + parameters: + - name: pk + description: User ID (primary key) + type: integer + required: true + paramType: path + type: + status: + type: string + description: success or error + required: true + show_shared_buildings: + type: string + description: the user show shared buildings attribute + required: true + message: + type: string + description: error message, if any + required: false + """ + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + + return JsonResponse({ + 'status': 'success', + 'show_shared_buildings': user.show_shared_buildings, + }) + + @ajax_request_class + @action(detail=True, methods=['PUT']) + def default_organization(self, request, pk=None): + """ + Sets the user's default organization + --- + parameter_strategy: replace + parameters: + - name: pk + description: User ID (primary key) + type: integer + required: true + paramType: path + - name: organization_id + description: The new default organization ID to use for this user + type: integer + required: true + type: + status: + type: string + description: success or error + required: true + message: + type: string + description: error message, if any + required: false + """ + ok, content = self.validate_request_user(pk, request) + if ok: + user = content + else: + return content + user.default_organization_id = request.query_params.get('organization_id', None) + user.save() + return {'status': 'success'} + + @ajax_request_class + @action(detail=True, methods=['DELETE']) + def deactivate(self, request, pk=None): + """ + Deactivates a user + --- + parameter_strategy: replace + parameters: + - name: + description: User ID (primary key) + type: integer + required: true + paramType: path + - name: organization_id + description: The new default organization ID to use for this user + type: integer + required: true + type: + status: + type: string + description: success or error + required: true + message: + type: string + description: error message, if any + required: false + """ + body = request.data + first_name = body['first_name'] + last_name = body['last_name'] + + print(first_name, last_name) + # check if user exists + user = SEEDUser.objects.filter( + first_name=first_name, last_name=last_name + ) + print(user) + if not user.exists(): + return JsonResponse({ + 'status': 'error', + 'message': 'user does not exist', + }, status=status.HTTP_403_FORBIDDEN) + + user[0].deactivate_user() + return JsonResponse({'status': 'success'}) From 5adb233493598cb90c0589ffd658c0a2402fd7a2 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Mon, 11 May 2020 12:53:47 -0600 Subject: [PATCH 02/13] fixed flake8 error --- seed/views/v3/users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index 60847ba25b..13c19bfaaa 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -109,6 +109,7 @@ class EmailAndIDSerializer(serializers.Serializer): class ListUsersResponseSerializer(serializers.Serializer): users = EmailAndIDSerializer(many=True) + class UserSchema(AutoSchemaHelper): def __init__(self, *args): super().__init__(*args) @@ -144,7 +145,8 @@ def __init__(self, *args): 'email': 'string' } )], - ('PUT', 'default_organization'): [self.org_id_field(), self.path_id_field(description="Updated Users PK ID")], + ('PUT', 'default_organization'): [self.org_id_field(), + self.path_id_field(description="Updated Users PK ID")], ('POST', 'is_authorized'): [ self.org_id_field(), self.path_id_field(description="Users PK ID"), @@ -193,8 +195,6 @@ def __init__(self, *args): ] - - } From 36a72a071ea5d10fea8852ba3d45e85adb465155 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Fri, 15 May 2020 07:46:02 -0600 Subject: [PATCH 03/13] Fixed reviewed changes from Adrian --- seed/utils/api_schema.py | 3 +-- seed/views/v3/data_quality.py | 4 ++-- seed/views/v3/users.py | 30 +++++++++++++----------------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/seed/utils/api_schema.py b/seed/utils/api_schema.py index f3bb19d452..1d6dcd2b84 100644 --- a/seed/utils/api_schema.py +++ b/seed/utils/api_schema.py @@ -8,12 +8,11 @@ class AutoSchemaHelper(SwaggerAutoSchema): # Used to easily build out example values displayed on Swagger page. body_parameter_formats = { - 'interger_list': openapi.Schema( + 'interger_array': openapi.Schema( type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_INTEGER) ), 'string': openapi.Schema(type=openapi.TYPE_STRING), - 'string_array': openapi.Schema( type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING) diff --git a/seed/views/v3/data_quality.py b/seed/views/v3/data_quality.py index 02a45a39c0..8fb930b2fa 100644 --- a/seed/views/v3/data_quality.py +++ b/seed/views/v3/data_quality.py @@ -105,8 +105,8 @@ def __init__(self, *args): required=True, description="An object containing IDs of the records to perform data quality checks on. Should contain two keys- property_state_ids and taxlot_state_ids, each of which is an array of appropriate IDs.", params_to_formats={ - 'property_state_ids': 'interger_list', - 'taxlot_state_ids': 'interger_list' + 'property_state_ids': 'interger_array', + 'taxlot_state_ids': 'interger_array' } ), ], diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index 13c19bfaaa..edddc15609 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -183,15 +183,16 @@ def __init__(self, *args): }) ], - ('DELETE', 'deactivate'): [ - self.body_field( - name='User to be deactivated', - required=True, - description="first and last name of user to be deactivated", - params_to_formats={ - 'first_name': 'string', - 'last_name': 'string' - }) + ('PUT', 'deactivate'): [ + self.path_id_field(description="Users PK ID") + # self.body_field( + # name='User to be deactivated', + # required=True, + # description="first and last name of user to be deactivated", + # params_to_formats={ + # 'first_name': 'string', + # 'last_name': 'string' + # }) ] @@ -841,7 +842,7 @@ def default_organization(self, request, pk=None): return {'status': 'success'} @ajax_request_class - @action(detail=True, methods=['DELETE']) + @action(detail=True, methods=['PUT']) def deactivate(self, request, pk=None): """ Deactivates a user @@ -867,16 +868,11 @@ def deactivate(self, request, pk=None): description: error message, if any required: false """ - body = request.data - first_name = body['first_name'] - last_name = body['last_name'] - - print(first_name, last_name) + user_id = pk # check if user exists user = SEEDUser.objects.filter( - first_name=first_name, last_name=last_name + id=user_id ) - print(user) if not user.exists(): return JsonResponse({ 'status': 'error', From bf3fe3bd928c97ab8a16daf28ce715a9c720d08e Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Mon, 18 May 2020 07:35:25 -0600 Subject: [PATCH 04/13] Adrian review changes --- seed/views/v3/users.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index edddc15609..3ef00b80a8 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -32,6 +32,7 @@ from seed.utils.api import api_endpoint_class from seed.utils.api_schema import AutoSchemaHelper from seed.utils.organizations import create_organization +from rest_framework.status import HTTP_400_BAD_REQUEST _log = logging.getLogger(__name__) @@ -868,16 +869,19 @@ def deactivate(self, request, pk=None): description: error message, if any required: false """ - user_id = pk - # check if user exists - user = SEEDUser.objects.filter( - id=user_id - ) - if not user.exists(): + try: + user_id = pk + user = SEEDUser.objects.get( + id=user_id + ) + user.deactivate_user() + return JsonResponse({ + 'status': 'successfully deactivated', + 'data': user.email + }) + except Exception as e: return JsonResponse({ 'status': 'error', - 'message': 'user does not exist', - }, status=status.HTTP_403_FORBIDDEN) + 'data': str(e), + }, status=HTTP_400_BAD_REQUEST) - user[0].deactivate_user() - return JsonResponse({'status': 'success'}) From 57f62285c1b9c36236d6ece7ef38c25f7274def2 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Mon, 18 May 2020 07:48:18 -0600 Subject: [PATCH 05/13] flake8 --- seed/views/v3/users.py | 1 - 1 file changed, 1 deletion(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index 3ef00b80a8..21a53114b2 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -884,4 +884,3 @@ def deactivate(self, request, pk=None): 'status': 'error', 'data': str(e), }, status=HTTP_400_BAD_REQUEST) - From 03e7061728eb9c562e14d0a8a4ce69b7202a19e1 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Tue, 19 May 2020 07:50:57 -0600 Subject: [PATCH 06/13] review changes --- seed/api/v3/urls.py | 1 - seed/views/v3/users.py | 16 +++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/seed/api/v3/urls.py b/seed/api/v3/urls.py index 37b3f35b0e..af1fb8d757 100644 --- a/seed/api/v3/urls.py +++ b/seed/api/v3/urls.py @@ -11,7 +11,6 @@ api_v3_router = routers.DefaultRouter() api_v3_router.register(r'data_quality_checks', DataQualityViews, base_name='data_quality_checks') api_v3_router.register(r'datasets', DatasetViewSet, base_name='datasets') - api_v3_router.register(r'users', UserViewSet, base_name='user') diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index 21a53114b2..d398774528 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -129,8 +129,8 @@ def __init__(self, *args): 'last_name': 'string', 'role': 'string', 'email': 'string' - } - )], + }) + ], ('GET', 'current_user_id'): [], ('GET', 'retrieve'): [self.path_id_field(description="users PK ID")], ('PUT', 'update'): [ @@ -186,17 +186,7 @@ def __init__(self, *args): ], ('PUT', 'deactivate'): [ self.path_id_field(description="Users PK ID") - # self.body_field( - # name='User to be deactivated', - # required=True, - # description="first and last name of user to be deactivated", - # params_to_formats={ - # 'first_name': 'string', - # 'last_name': 'string' - # }) - ] - } @@ -432,7 +422,7 @@ def role(self, request, pk=None): OrganizationUser.objects.filter( user_id=user_id, - organization_id=body['organization_id'] + organization_id=organization_id ).update(role_level=role) return JsonResponse({'status': 'success'}) From 31d241f974ef976762d6971b8510666cac072146 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Tue, 19 May 2020 12:32:07 -0600 Subject: [PATCH 07/13] added clarification and default role assignment --- seed/views/v3/users.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index d398774528..d1842b18e0 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -122,8 +122,9 @@ def __init__(self, *args): self.body_field( name='New User Fields', required=True, - description="An object containing meta data for a new user: " - "required - [last_name, role(viewer/owner/member), email]", + description="An object containing meta data for a new user: \n" + "- Required - first_name, last_name, email \n" + "- Optional - role viewer(default), member, owner", params_to_formats={ 'first_name': 'string', 'last_name': 'string', @@ -138,8 +139,8 @@ def __init__(self, *args): self.body_field( name='Updated User Fields', required=True, - description="An object containing meta data for a updated user: " - "required - [first_name, last_name, email]", + description="An object containing meta data for a updated user: \n" + "- Required - first_name, last_name, email", params_to_formats={ 'first_name': 'string', 'last_name': 'string', @@ -300,6 +301,8 @@ def create(self, request): role = body['role'] if isinstance(role, dict): role = role['value'] + elif role == 'string': + role = 'viewer' OrganizationUser.objects.filter( organization_id=org.pk, From fc0c3e611df2246301eeb6176a3a446bc8db4a0a Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Tue, 19 May 2020 13:13:12 -0600 Subject: [PATCH 08/13] travis --- seed/views/v3/users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index d1842b18e0..f90ae5e65b 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -122,8 +122,8 @@ def __init__(self, *args): self.body_field( name='New User Fields', required=True, - description="An object containing meta data for a new user: \n" - "- Required - first_name, last_name, email \n" + description="An object containing meta data for a new user: " + "- Required - first_name, last_name, email " "- Optional - role viewer(default), member, owner", params_to_formats={ 'first_name': 'string', @@ -139,7 +139,7 @@ def __init__(self, *args): self.body_field( name='Updated User Fields', required=True, - description="An object containing meta data for a updated user: \n" + description="An object containing meta data for a updated user: " "- Required - first_name, last_name, email", params_to_formats={ 'first_name': 'string', From a0e9c66d0a8f5e2c96b5f682e178c8538593a0e2 Mon Sep 17 00:00:00 2001 From: Austin <31779424+aviveiros11@users.noreply.github.com> Date: Tue, 19 May 2020 15:32:13 -0600 Subject: [PATCH 09/13] Update seed/views/v3/users.py Co-authored-by: Adrian Lara <30608004+adrian-lara@users.noreply.github.com> --- seed/views/v3/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index f90ae5e65b..f9d0107c93 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -124,7 +124,7 @@ def __init__(self, *args): required=True, description="An object containing meta data for a new user: " "- Required - first_name, last_name, email " - "- Optional - role viewer(default), member, owner", + "- Optional - role ['viewer'(default), 'member', or 'owner']", params_to_formats={ 'first_name': 'string', 'last_name': 'string', From 6f015146b9cb5837f9869db1709a05e78d32bb54 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Tue, 19 May 2020 16:33:23 -0600 Subject: [PATCH 10/13] added try except --- seed/views/v3/users.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index f9d0107c93..bbb4abadb8 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -299,10 +299,12 @@ def create(self, request): if body.get('role'): # check if this is a dict, if so, grab the value out of 'value' role = body['role'] - if isinstance(role, dict): - role = role['value'] - elif role == 'string': - role = 'viewer' + try: + _get_role_from_js(role) + except Exception: + return JsonResponse({'status': 'error', 'message': 'valid arguments for role are [viewer, member, ' + 'owner]'}, + status=status.HTTP_400_BAD_REQUEST) OrganizationUser.objects.filter( organization_id=org.pk, From 6aa07df38ccea846215c383391a84031ea8a6512 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Wed, 20 May 2020 10:31:10 -0600 Subject: [PATCH 11/13] formatting --- seed/views/v3/users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index bbb4abadb8..37c446568e 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -122,8 +122,8 @@ def __init__(self, *args): self.body_field( name='New User Fields', required=True, - description="An object containing meta data for a new user: " - "- Required - first_name, last_name, email " + description="An object containing meta data for a new user: \n" + "- Required - first_name, last_name, email \n" "- Optional - role ['viewer'(default), 'member', or 'owner']", params_to_formats={ 'first_name': 'string', @@ -139,7 +139,7 @@ def __init__(self, *args): self.body_field( name='Updated User Fields', required=True, - description="An object containing meta data for a updated user: " + description="An object containing meta data for a updated user: \n" "- Required - first_name, last_name, email", params_to_formats={ 'first_name': 'string', From 4428ba61dda6c531717232e1dcc0a867cb2c2fd5 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Wed, 20 May 2020 11:00:26 -0600 Subject: [PATCH 12/13] formatting --- seed/views/v3/users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index bbb4abadb8..37c446568e 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -122,8 +122,8 @@ def __init__(self, *args): self.body_field( name='New User Fields', required=True, - description="An object containing meta data for a new user: " - "- Required - first_name, last_name, email " + description="An object containing meta data for a new user: \n" + "- Required - first_name, last_name, email \n" "- Optional - role ['viewer'(default), 'member', or 'owner']", params_to_formats={ 'first_name': 'string', @@ -139,7 +139,7 @@ def __init__(self, *args): self.body_field( name='Updated User Fields', required=True, - description="An object containing meta data for a updated user: " + description="An object containing meta data for a updated user: \n" "- Required - first_name, last_name, email", params_to_formats={ 'first_name': 'string', From b33b712e9634a1a248a0f1d60336f1752357c723 Mon Sep 17 00:00:00 2001 From: aviveiros11 Date: Thu, 21 May 2020 09:35:03 -0600 Subject: [PATCH 13/13] formatting --- seed/views/v3/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seed/views/v3/users.py b/seed/views/v3/users.py index 37c446568e..7dede3e68b 100644 --- a/seed/views/v3/users.py +++ b/seed/views/v3/users.py @@ -137,7 +137,7 @@ def __init__(self, *args): ('PUT', 'update'): [ self.path_id_field(description="Updated Users PK ID"), self.body_field( - name='Updated User Fields', + name='Updated user fields', required=True, description="An object containing meta data for a updated user: \n" "- Required - first_name, last_name, email", @@ -153,7 +153,7 @@ def __init__(self, *args): self.org_id_field(), self.path_id_field(description="Users PK ID"), self.body_field( - name='actions', + name='Actions', required=True, description="A list of actions to check: examples include (requires_parent_org_owner, " "requires_owner, requires_member, requires_viewer, "