diff --git a/api/views.py b/api/views.py index c4f9e1271..2037ce7d7 100644 --- a/api/views.py +++ b/api/views.py @@ -10,6 +10,7 @@ from django.utils.decorators import method_decorator from django.contrib.auth import authenticate from django.contrib.auth.models import User +from django.conf import settings from django.views import View from django.db.models.functions import TruncMonth, TruncYear from django.db.models import Count, Sum @@ -26,7 +27,7 @@ from deployments.models import Heop from notifications.models import Subscription from notifications.notification import send_notification -from registrations.models import Recovery +from registrations.models import Recovery, Pending from main.frontend import frontend_url @@ -91,24 +92,14 @@ def post(self, request): return Response({'data': 'Success'}) -class PublicJsonRequestView(View): - http_method_names = ['get', 'head', 'options'] - - def handle_get(self, request, *args, **kwargs): - print(pretty_request(request)) - - def get(self, request, *args, **kwargs): - return self.handle_get(request, *args, **kwargs) - - -class EsPageHealth(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class EsPageHealth(APIView): + def get(self, request): health = ES_CLIENT.cluster.health() return JsonResponse(health) -class EsPageSearch(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class EsPageSearch(APIView): + def get(self, request): page_type = request.GET.get('type', None) phrase = request.GET.get('keyword', None) if phrase is None: @@ -151,8 +142,8 @@ def handle_get(self, request, *args, **kwargs): return JsonResponse(results['hits']) -class AreaAggregate(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class AreaAggregate(APIView): + def get(self, request): region_type = request.GET.get('type', None) region_id = request.GET.get('id', None) @@ -169,8 +160,8 @@ def handle_get(self, request, *args, **kwargs): return JsonResponse(dict(aggregate)) -class AggregateByDtype(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class AggregateByDtype(APIView): + def get(self, request): models = { 'appeal': Appeal, 'event': Event, @@ -191,8 +182,8 @@ def handle_get(self, request, *args, **kwargs): return JsonResponse(dict(aggregate=list(aggregate))) -class AggregateByTime(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class AggregateByTime(APIView): + def get(self, request): models = { 'appeal': Appeal, 'event': Event, @@ -273,59 +264,20 @@ def handle_get(self, request, *args, **kwargs): return JsonResponse(dict(aggregate=list(aggregate))) -@method_decorator(csrf_exempt, name='dispatch') -class PublicJsonPostView(View): - http_method_names = ['post'] - - def decode_auth_header(self, auth_header): - parts = auth_header[7:].split(':') - return parts[0], parts[1] - - def get_authenticated_user(self, request): - auth_header = request.META.get('HTTP_AUTHORIZATION') - if not auth_header: - return None - - # Parse the authorization header - username, key = self.decode_auth_header(auth_header) - if not username or not key: - return None - - # Query the user - try: - user = User.objects.get(username=username) - except ObjectDoesNotExist: - return None - - # Query the key - try: - Token.objects.get(user=user, key=key) - except ObjectDoesNotExist: - return None - - return user - - def handle_post(self, request, *args, **kwargs): - print(pretty_request(request)) - - def post(self, request, *args, **kwargs): - if request.META.get('CONTENT_TYPE').find('application/json') == -1: - return bad_request('Content-type must be `application/json`') - return self.handle_post(request, *args, **kwargs) +class GetAuthToken(APIView): + permission_classes = [] + def post(self, request): + username = request.data.get('username', None) + password = request.data.get('password', None) -class GetAuthToken(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - body = json.loads(request.body.decode('utf-8')) - if 'username' not in body or 'password' not in body: + if username is None or password is None: return bad_request('Body must contain `username` and `password`') - username = body['username'] - password = body['password'] # Get the case-correct username for authenticate() casecorr_uname = User.objects.filter(username__iexact=username).values_list('username', flat=True).first() - if not casecorr_uname: - return bad_request('Invalid username or password') # definately username issue + if casecorr_uname is None: + return bad_request('Invalid username or password') # definitely username issue # User model's __str__ is its username user = authenticate(username=casecorr_uname, password=password) @@ -353,48 +305,52 @@ def handle_post(self, request, *args, **kwargs): return bad_request('Invalid username or password') # most probably password issue -class ChangePassword(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - body = json.loads(request.body.decode('utf-8')) - if 'username' not in body or ('password' not in body and 'token' not in body): +class ChangePassword(APIView): + permissions_classes = [] + + def post(self, request): + username = request.data.get('username', None) + password = request.data.get('password', None) + new_pass = request.data.get('new_password', None) + token = request.data.get('token', None) + if username is None or password is None: return bad_request('Must include a `username` and either a `password` or `token`') - try: - user = User.objects.get(username__iexact=body['username']) - except ObjectDoesNotExist: + user = User.objects.filter(username__iexact=username).first() + if user is None: return bad_request('Could not authenticate') - if 'password' in body and not user.check_password(body['password']): + if password and not user.check_password(password): return bad_request('Could not authenticate') - elif 'token' in body: - try: - recovery = Recovery.objects.get(user=user) - except ObjectDoesNotExist: + elif token: + recovery = Recovery.objects.filter(user=user).first() + if recovery is None: return bad_request('Could not authenticate') - if recovery.token != body['token']: + if recovery.token != token: return bad_request('Could not authenticate') recovery.delete() # TODO validate password - if 'new_password' not in body: + if new_pass is None: return bad_request('Must include a `new_password` property') - user.set_password(body['new_password']) + user.set_password(new_pass) user.save() return JsonResponse({'status': 'ok'}) -class RecoverPassword(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - body = json.loads(request.body.decode('utf-8')) - if 'email' not in body: +class RecoverPassword(APIView): + permission_classes = [] + + def post(self, request): + email = request.data.get('email', None) + if email is None: return bad_request('Must include an `email` property') - try: - user = User.objects.get(email__iexact=body['email']) - except ObjectDoesNotExist: + user = User.objects.filter(email__iexact=email).first() + if user is None: return bad_request('That email is not associated with a user') token = get_random_string(length=32) @@ -413,15 +369,16 @@ def handle_post(self, request, *args, **kwargs): return JsonResponse({'status': 'ok'}) -class ShowUsername(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - body = json.loads(request.body.decode('utf-8')) - if 'email' not in body: +class ShowUsername(APIView): + permission_classes = [] + + def post(self, request): + email = request.data.get('email', None) + if email is None: return bad_request('Must include an `email` property') - try: - user = User.objects.get(email__iexact=body['email']) - except ObjectDoesNotExist: + user = User.objects.filter(email__iexact=email).first() + if user is None: return bad_request('That email is not associated with a user') email_context = { @@ -435,6 +392,49 @@ def handle_post(self, request, *args, **kwargs): return JsonResponse({'status': 'ok'}) +class ResendValidation(APIView): + permission_classes = [] + + def post(self, request): + username = request.data.get('username', None) + + if username: + pending_user = Pending.objects.select_related('user').filter(user__username__iexact=username).first() + if pending_user: + if pending_user.user.is_active is True: + return bad_request('Your registration is already active, \ + you can try logging in with your registered username and password') + if pending_user.created_at < timezone.now() - timedelta(days=1): + return bad_request('The verification period is expired. \ + You must verify your email within 24 hours. \ + Please contact your system administrator.') + + # Construct and re-send the email + email_context = { + 'confirmation_link': 'https://%s/verify_email/?token=%s&user=%s' % ( + settings.BASE_URL, # on PROD it should point to goadmin... + pending_user.token, + username, + ) + } + + if pending_user.user.is_staff: + template = 'email/registration/verify-staff-email.html' + else: + template = 'email/registration/verify-outside-email.html' + + send_notification('Validate your account', + [pending_user.user.email], + render_to_string(template, email_context), + 'Validate account - ' + username) + return Response({'data': 'Success'}) + else: + return bad_request('No pending registration found with the provided username. \ + Please check your input.') + else: + return bad_request('Please provide your username in the request.') + + class AddCronJobLog(APIView): authentication_classes = (authentication.TokenAuthentication,) permissions_classes = (permissions.IsAuthenticated,) diff --git a/deployments/drf_views.py b/deployments/drf_views.py index 04dd29489..104276967 100644 --- a/deployments/drf_views.py +++ b/deployments/drf_views.py @@ -1,7 +1,5 @@ from collections import defaultdict -from rest_framework.authentication import ( - TokenAuthentication, -) +from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import action from rest_framework.response import Response diff --git a/main/urls.py b/main/urls.py index 1e5355213..ef9e8c3c7 100644 --- a/main/urls.py +++ b/main/urls.py @@ -35,12 +35,13 @@ AreaAggregate, AddCronJobLog, DummyHttpStatusError, - DummyExceptionError + DummyExceptionError, + ResendValidation ) from registrations.views import ( NewRegistration, VerifyEmail, - ValidateUser, + ValidateUser ) from per.views import ( DraftSent, @@ -145,6 +146,7 @@ url(r'^change_password', ChangePassword.as_view()), url(r'^recover_password', RecoverPassword.as_view()), url(r'^show_username', ShowUsername.as_view()), + url(r'^resend_validation', ResendValidation.as_view()), url(r'^api/v2/', include(router.urls)), url(r'^api/v2/event/(?P\d+)', api_views.EventViewset.as_view({'get': 'retrieve'})), url(r'^api/v2/event/(?P[-\w]+)', api_views.EventViewset.as_view({'get': 'retrieve'}, lookup_field='slug')), diff --git a/per/test_views.py b/per/test_views.py index 83119a8d7..a2c05fe91 100644 --- a/per/test_views.py +++ b/per/test_views.py @@ -17,6 +17,7 @@ def test_simple_form(self): 'code': 'A1', 'name': 'Nemo', 'language': 1, + 'user_id': 1, 'unique_id': '1aad9295-ceb9-4ad5-9b10-84cc423e93f4', 'started_at': '2019-04-11 11:42:22.278796+00', 'submitted_at': '2019-04-11 09:42:52.278796+00', diff --git a/per/views.py b/per/views.py index 5783ed208..c5ec6e812 100644 --- a/per/views.py +++ b/per/views.py @@ -1,80 +1,37 @@ -import json -import datetime -import pytz import logging +from django.db import transaction from django.http import JsonResponse -from api.views import ( - bad_request, - PublicJsonPostView, -) -from rest_framework.authtoken.models import Token -from django.contrib.auth.models import User - +from django.utils import timezone +from rest_framework import authentication, permissions +from rest_framework.views import APIView from .models import ( Draft, Form, FormData, WorkPlan, Overview ) +from api.views import bad_request logger = logging.getLogger(__name__) -def create_draft(raw): - Draft.objects.filter( - code=raw['code'], country_id=raw['country_id'], user_id=raw['user_id'] - ).delete() # If exists (a previous draft), delete it. - draft = Draft.objects.create( - code=raw['code'], - country_id=raw['country_id'], - user_id=raw['user_id'], - data=raw['data'], - ) - draft.save() - return draft - - -def create_form(raw): - form = Form.objects.create( - code=raw['code'], - name=raw['name'], - language=raw['language'], - user_id=raw['user_id'], - country_id=raw['country_id'], - ns=raw['ns'], - ip_address=raw['ip_address'], - started_at=raw['started_at'], - ended_at=raw['ended_at'], - submitted_at=raw['submitted_at'], - comment=raw['comment'], - validated=raw['validated'], - finalized=raw['finalized'], - # unique_id = raw['unique_id'], # only KoBo form provided - ) - form.save() - return form - - -def create_form_data(raw, form): - form_data = FormData.objects.create( - form=form, - question_id=raw['id'], - selected_option=raw['op'], - notes=raw['nt'], - ) - form_data.save() - return form_data - - -class FormSent(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - # if not request.user.is_authenticated: - # return bad_request('Could not insert PER data due to not logged in user.') - - body = json.loads(request.body.decode('utf-8')) - - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - body['ip_address'] = x_forwarded_for.split(',')[-1].strip() - else: - body['ip_address'] = request.META.get('REMOTE_ADDR') +def get_client_ip(request): + """ https://stackoverflow.com/questions/4581789/how-do-i-get-user-ip-address-in-django """ + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[-1].strip() + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + +def get_now_str(): + return str(timezone.now()) + + +class FormSent(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) + + def post(self, request): + ip = get_client_ip(request) required_fields = [ 'user_id', @@ -83,415 +40,312 @@ def handle_post(self, request, *args, **kwargs): 'language', ] - currentDT = datetime.datetime.now(pytz.timezone('UTC')) - if 'started_at' not in body: - body['started_at'] = str(currentDT) - if 'ended_at' not in body: - body['ended_at'] = str(currentDT) - if 'submitted_at' not in body: - body['submitted_at'] = str(currentDT) - if 'comment' not in body: - body['comment'] = None - if 'validated' not in body: - body['validated'] = False - if 'finalized' not in body: - body['finalized'] = False - if 'user_id' not in body: - body['user_id'] = None - if 'country_id' not in body: - body['country_id'] = None - if 'ns' not in body: - body['ns'] = None - - missing_fields = [field for field in required_fields if field not in body] + missing_fields = [field for field in required_fields if field not in request.data] if len(missing_fields): return bad_request('Could not complete request. Please submit %s' % ', '.join(missing_fields)) - if ' ' in body['code']: + name = request.data.get('name', None) + code = request.data.get('code', None) + language = request.data.get('language', None) + started_at = request.data.get('started_at', get_now_str()) + ended_at = request.data.get('ended_at', get_now_str()) + submitted_at = request.data.get('submitted_at', get_now_str()) + comment = request.data.get('comment', None) + validated = request.data.get('validated', False) + finalized = request.data.get('finalized', False) + user_id = comment = request.data.get('user_id', None) + country_id = request.data.get('country_id', None) + ns = request.data.get('ns', None) + data = request.data.get('data', None) + + if ' ' in code: return bad_request('Code can not contain spaces, please choose a different one.') + # Create the Form object try: - form = create_form(body) + form = Form.objects.create( + code=code, + name=name, + language=language, + user_id=user_id, + country_id=country_id, + ns=ns, + ip_address=ip, + started_at=started_at, + ended_at=ended_at, + submitted_at=submitted_at, + comment=comment, + # unique_id = raw['unique_id'], # only KoBo form provided + validated=validated, + finalized=finalized + ) except Exception: logger.error('Could not insert PER form record.', exc_info=True) return bad_request('Could not insert PER form record.') - if hasattr(form, 'status_code') and form.status_code == 400: - return bad_request('Could not insert PER form record due to inner failure.') - - if 'data' in body: - for rubr in body['data']: - try: - form_data = create_form_data(rubr, form) - except Exception: - logger.error('Could not insert PER formdata record.', exc_info=True) - return bad_request('Could not insert PER formdata record.') - - if hasattr(form_data, 'status_code') and form_data.status_code == 400: - return bad_request('Could not insert PER form data record due to inner failure.') - - return JsonResponse({'status': 'ok'}) - - -def change_form(raw): - if 'id' not in raw: - return bad_request('Id sending is mandatory') - - form = Form.objects.get(pk=int(raw['id'])) # If exists, get it - - if not form: - return bad_request('Could not find PER form record.') - - # form.code = raw['code'] # we do not change it - # form.user_id = raw['user_id'] # we do not change it - form.name = raw.get('name', form.name) - form.language = raw.get('language', form.language) - form.country_id = raw.get('country_id', form.country_id) - form.ns = raw.get('ns', form.ns) - form.ip_address = raw.get('ip_address', form.ip_address) - form.started_at = raw.get('started_at', form.started_at) - form.ended_at = raw.get('ended_at', form.ended_at) - form.submitted_at = raw.get('submitted_at', form.submitted_at) - form.comment = raw.get('comment', form.comment) - form.validated = raw.get('validated', form.validated) - form.finalized = raw.get('finalized', form.finalized) - form.save() - return form - - -def change_form_data(raw, form): - if 'form_id' not in raw: - return bad_request('Form_id sending is mandatory') - if 'id' not in raw: - return bad_request('Question_id sending is mandatory') - form_data = FormData.objects.filter( - form_id=raw['form_id'], question_id=raw['id'] - )[0] # If exists data item, get the first. We keep only 1 answer per question_id - if not form_data: - return bad_request('Could not find PER form data record.') - - # form_data.question_id = raw['id'] # we do not change it - form_data.selected_option = raw.get('op', form_data.selected_option) - form_data.notes = raw.get('nt', form_data.notes) - form_data.save() - - return form_data - - -class FormEdit(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: + # Create FormData of the Form + if data: try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) + with transaction.atomic(): # all or nothing + for rubr in data: + FormData.objects.create(form=form, + question_id=rubr['id'], + selected_option=rubr['op'], + notes=rubr['nt']) except Exception: - logger.error('User token is not correct.', exc_info=True) - return bad_request('User token is not correct.') - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - else: - return bad_request('User token is not given.') - if not u: - return bad_request('User is not logged in or inactive.') - - body = json.loads(request.body.decode('utf-8')) + logger.error('Could not insert PER formdata record.', exc_info=True) + return bad_request('Could not insert PER formdata record.') - required_fields = [ - 'id', - ] + return JsonResponse({'status': 'ok'}) - currentDT = datetime.datetime.now(pytz.timezone('UTC')) - if 'ended_at' not in body: - body['ended_at'] = str(currentDT) - if 'submitted_at' not in body: - body['submitted_at'] = str(currentDT) - - missing_fields = [field for field in required_fields if field not in body] - if len(missing_fields): - return bad_request('Could not complete request. Please submit %s' % ', '.join(missing_fields)) +class FormEdit(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) + + def post(self, request): + form_id = request.data.get('id', None) + if form_id is None: + return bad_request('Could not complete request. Please submit %s' % form_id) + + form = Form.objects.filter(pk=form_id).first() + if form is None: + return bad_request('Could not find PER form record.') + + started_at = request.data.get('started_at', form.started_at) + ended_at = request.data.get('ended_at', get_now_str()) + submitted_at = request.data.get('submitted_at', get_now_str()) + name = request.data.get('name', form.name) + language = request.data.get('language', form.language) + country_id = request.data.get('country_id', form.country_id) + ns = request.data.get('ns', form.ns) + ip = request.data.get('ip_address', form.ip_address) + comment = request.data.get('comment', form.comment) + validated = request.data.get('validated', form.validated) + finalized = request.data.get('finalized', form.finalized) + data = request.data.get('data', None) + + # Update the Form properties and try to save try: - form = change_form(body) + form.name = name + form.language = language + form.country_id = country_id + form.ns = ns + form.ip_address = ip + form.started_at = started_at + form.ended_at = ended_at + form.submitted_at = submitted_at + form.comment = comment + form.validated = validated + form.finalized = finalized + form.save() except Exception: logger.error('Could not change PER form record.', exc_info=True) return bad_request('Could not change PER form record.') - if hasattr(form, 'status_code') and form.status_code == 400: - return bad_request('Could not change PER form record due to inner failure.') - - if 'data' in body: - for rubr in body['data']: - rubr['form_id'] = body['id'] - try: - form_data = change_form_data(rubr, form) - except Exception: - logger.error('Could not change PER formdata record.', exc_info=True) - return bad_request('Could not change PER formdata record.') - - if hasattr(form_data, 'status_code') and form_data.status_code == 400: - return bad_request('Could not change PER form data record due to inner failure.') + # Update Form Data of the Form + if data: + try: + with transaction.atomic(): # all or nothing + for rubr in data: + if rubr['id'] is None: + raise Exception('PER Form Data ID was missing. Form ID: {}'.format(form_id)) - return JsonResponse({'status': 'ok'}) + form_data = FormData.objects.filter(form_id=form_id, question_id=rubr['id']).first() + if not form_data: + raise Exception('Could not find PER Form Data record. \ + Form ID: {} Form Data ID: {}'.format(form_id, rubr['id'])) + form_data.selected_option = rubr['op'] if rubr['op'] else form_data.selected_option + form_data.notes = rubr['nt'] if rubr['nt'] else form_data.notes -class DraftSent(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: - try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) - except Exception: - logger.error('User token is not correct.', exc_info=True) - return bad_request('User token is not correct.') + form_data.save() + except Exception as err: + logger.error('Could not change PER formdata record.', exc_info=True) + return bad_request('Could not change PER formdata record. {}'.format(err)) - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - else: - return bad_request('User token is not given.') - if not u: - return bad_request('User is not logged in or inactive.') + return JsonResponse({'status': 'ok'}) - body = json.loads(request.body.decode('utf-8')) - required_fields = [ - 'code', 'user_id', - ] +class DraftSent(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) - missing_fields = [field for field in required_fields if field not in body] - if len(missing_fields): + def post(self, request): + required_fields = ('code', 'user_id') + missing_fields = [field for field in required_fields if field not in request.data] + if missing_fields: return bad_request('Could not complete request. Please submit %s' % ', '.join(missing_fields)) - if ' ' in body['code']: - return bad_request('Code can not contain spaces, please choose a different one.') + code = request.data.get('code', None) + if ' ' in code: + return bad_request('Draft "code" can not contain spaces.') + + country_id = request.data.get('country_id', None) + user_id = request.data.get('user_id', None) + data = request.data.get('data', None) try: - create_draft(body) + # If exists (a previous draft), delete it. + Draft.objects.filter(code=code, country_id=country_id, user_id=user_id).delete() + Draft.objects.create( + code=code, + user_id=user_id, + data=data, + country_id=country_id + ) except Exception: - logger.error('User token is not correct.', exc_info=True) - return bad_request('Could not insert PER draft.') + return bad_request('Could not create PER draft.') return JsonResponse({'status': 'ok'}) -def create_workplan(raw): - # WorkPlan.objects.filter(code=raw['code'], user_id=raw['user_id']).delete() # If exists (a previous WorkPlan), delete it. - workplan = WorkPlan.objects.create(prioritization = raw['prioritization'], - components = raw['components'], - benchmark = raw['benchmark'], - actions = raw['actions'], - comments = raw['comments'], - timeline = raw['timeline'] \ - if 'timeline' in raw else str(datetime.datetime.now(pytz.timezone('UTC'))), - status = raw['status'], - support_required = raw['support_required'], - focal_point = raw['focal_point'], - country_id = raw['country_id'], - code = raw['code'], - question_id = raw['question_id'], - user_id = raw['user_id'], - ) - workplan.save() - return workplan - -class WorkPlanSent(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: - try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) - except: - return bad_request('User token is not correct.') - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - else: - return bad_request('User token is not given.') - if not u: - return bad_request('User is not logged in or inactive.') +class WorkPlanSent(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) - body = json.loads(request.body.decode('utf-8')) - - required_fields = [ - 'country_id', 'user_id', - ] - missing_fields = [field for field in required_fields if field not in body] - if len(missing_fields): + def post(self, request): + required_fields = ('country_id', 'user_id') + missing_fields = [field for field in required_fields if field not in request.data] + if missing_fields: return bad_request('Could not complete request. Please submit %s' % ', '.join(missing_fields)) - if ' ' in body['question_id']: - return bad_request('Question_id can not contain spaces, please choose a different one.') + question_id = request.data.get('question_id', None) + if ' ' in question_id: + return bad_request('Question ID can not contain spaces.') + + prioritization = request.data.get('prioritization', None) + components = request.data.get('components', None) + benchmark = request.data.get('benchmark', None) + actions = request.data.get('actions', None) + comments = request.data.get('comments', None) + timeline = request.data.get('timeline', get_now_str()) + status = request.data.get('status', None) + support_required = request.data.get('support_required', None) + focal_point = request.data.get('focal_point', None) + country_id = request.data.get('country_id', None) + code = request.data.get('code', None) + user_id = request.data.get('user_id', None) try: - workplan = create_workplan(body) - except: + WorkPlan.objects.create( + prioritization=prioritization, + components=components, + benchmark=benchmark, + actions=actions, + comments=comments, + timeline=timeline, + status=status, + support_required=support_required, + focal_point=focal_point, + country_id=country_id, + code=code, + question_id=question_id, + user_id=user_id + ) + except Exception: return bad_request('Could not insert PER WorkPlan.') return JsonResponse({'status': 'ok'}) -def create_overview(raw): - # Overview.objects.filter(code=raw['code'], user_id=raw['user_id']).delete() # If exists (a previous Overview), delete it. - overview = Overview.objects.create(country_id = raw['country_id'], - user_id = raw['user_id'], - date_of_current_capacity_assessment = raw['date_of_current_capacity_assessment'] \ - if 'date_of_current_capacity_assessment' in raw else str(datetime.datetime.now(pytz.timezone('UTC'))), - type_of_capacity_assessment = raw['type_of_capacity_assessment'], - branch_involved = raw['branch_involved'], - focal_point_name = raw['focal_point_name'], - focal_point_email = raw['focal_point_email'], - had_previous_assessment = raw['had_previous_assessment'], - focus = raw['focus'], - facilitated_by = raw['facilitated_by'], - facilitator_email = raw['facilitator_email'], - phone_number = raw['phone_number'], - skype_address = raw['skype_address'], - date_of_mid_term_review = raw['date_of_mid_term_review'] \ - if 'date_of_mid_term_review' in raw else str(datetime.datetime.now(pytz.timezone('UTC'))), - approximate_date_next_capacity_assmt = raw['approximate_date_next_capacity_assmt'] \ - if 'approximate_date_next_capacity_assmt' in raw else str(datetime.datetime.now(pytz.timezone('UTC'))), - ) - - overview.save() - return overview - -class OverviewSent(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: - try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) - except: - return bad_request('User token is not correct.') - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - else: - return bad_request('User token is not given.') - if not u: - return bad_request('User is not logged in or inactive.') - - body = json.loads(request.body.decode('utf-8')) - - required_fields = [ - 'country_id', 'user_id', - ] +class OverviewSent(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) - missing_fields = [field for field in required_fields if field not in body] - if len(missing_fields): + def post(self, request): + required_fields = ('country_id', 'user_id') + missing_fields = [field for field in required_fields if field not in request.data] + if missing_fields: return bad_request('Could not complete request. Please submit %s' % ', '.join(missing_fields)) + country_id = request.data.get('country_id', None) + user_id = request.data.get('user_id', None) + country_id = request.data.get('country_id', None) + date_of_current_capacity_assessment = request.data.get('date_of_current_capacity_assessment', get_now_str()) + type_of_capacity_assessment = request.data.get('type_of_capacity_assessment', None) + branch_involved = request.data.get('branch_involved', None) + focal_point_name = request.data.get('focal_point_name', None) + focal_point_email = request.data.get('focal_point_email', None) + had_previous_assessment = request.data.get('had_previous_assessment', None) + focus = request.data.get('focus', None) + facilitated_by = request.data.get('facilitated_by', None) + facilitator_email = request.data.get('facilitator_email', None) + phone_number = request.data.get('phone_number', None) + skype_address = request.data.get('skype_address', None) + date_of_mid_term_review = request.data.get('date_of_mid_term_review', get_now_str()) + approximate_date_next_capacity_assmt = request.data.get('approximate_date_next_capacity_assmt', get_now_str()) + try: - overview = create_overview(body) - except: + Overview.objects.create( + country_id=country_id, + user_id=user_id, + date_of_current_capacity_assessment=date_of_current_capacity_assessment, + type_of_capacity_assessment=type_of_capacity_assessment, + branch_involved=branch_involved, + focal_point_name=focal_point_name, + focal_point_email=focal_point_email, + had_previous_assessment=had_previous_assessment, + focus=focus, + facilitated_by=facilitated_by, + facilitator_email=facilitator_email, + phone_number=phone_number, + skype_address=skype_address, + date_of_mid_term_review=date_of_mid_term_review, + approximate_date_next_capacity_assmt=approximate_date_next_capacity_assmt + ) + except Exception: return bad_request('Could not insert PER Overview.') return JsonResponse({'status': 'ok'}) -def delete_workplan(raw): - WorkPlan.objects.filter(id=raw['id']).delete() - return -class DelWorkPlan(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: - try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) - except: - return bad_request('User token is not correct.') - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - #origtoken = Token.objects.get_or_create(user=u) - else: - return bad_request('User token is not given.') - if not u: # or receivedtoken.key != origtoken: - return bad_request('User is not logged in or inactive.') - - # Did not work any of these... - # if not request.user.is_authenticated: - # return bad_request('Could not insert PER data due to not logged in user.') - # a = self.get_authenticated_user(self, request) - # auth_header = request.META.get('HTTP_AUTHORIZATION') - # parts = auth_header[6:].split(':') - - body = json.loads(request.body.decode('utf-8')) +class DelWorkPlan(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) + + def post(self, request): + workplan_id = request.data.get('id', None) + if workplan_id is None: + return bad_request('Need to provide WorkPlan ID.') + try: - workplan = delete_workplan(body) - except: + WorkPlan.objects.filter(id=workplan_id).delete() + except Exception: return bad_request('Could not delete PER WorkPlan.') return JsonResponse({'status': 'ok'}) -def delete_overview(raw): - Overview.objects.filter(id=raw['id']).delete() - return -class DelOverview(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: - try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) - except: - return bad_request('User token is not correct.') - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - else: - return bad_request('User token is not given.') - if not u: - return bad_request('User is not logged in or inactive.') +class DelOverview(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) - body = json.loads(request.body.decode('utf-8')) + def post(self, request): + overview_id = request.data.get('id', None) + if overview_id is None: + return bad_request('Need to provide Overview ID.') try: - workplan = delete_overview(body) - except: + Overview.objects.filter(id=overview_id).delete() + except Exception: return bad_request('Could not delete PER Overview.') return JsonResponse({'status': 'ok'}) -def delete_draft(draftId): - if draftId: - Draft.objects.filter(id=draftId).delete() - return -class DelDraft(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - u = None - richtokenstring = request.META.get('HTTP_AUTHORIZATION') - if richtokenstring: - try: - receivedtoken = Token.objects.get(key=richtokenstring[6:]) - except: - return bad_request('User token is not correct.') - u = User.objects.filter(is_active=True, pk=receivedtoken.user_id) - else: - return bad_request('User token is not given.') - if not u: - return bad_request('User is not logged in or inactive.') - - body = json.loads(request.body.decode('utf-8')) +class DelDraft(APIView): + authentication_classes = (authentication.TokenAuthentication,) + permissions_classes = (permissions.IsAuthenticated,) + + def post(self, request): + draft_id = request.data.get('id', None) + if draft_id is None: + return bad_request('Need to provide Draft ID.') try: - workplan = delete_draft(body['id']) - except: + Draft.objects.filter(id=draft_id).delete() + except Exception: return bad_request('Could not delete PER Draft.') - - return JsonResponse({'status': 'ok'}) -# *** Test script from bash (similar to test_views.py, but remains in db): -# curl --header "Content-Type: application/json" \ -# --request POST \ -# --data '{ -# "code": "A1", -# "name": "Nemo", -# "language": 1, -# "unique_id": "1aad9295-ceb9-4ad5-9b10-84cc423e93f4", -# "started_at": "2019-04-11 11:42:22.278796+00", -# "submitted_at": "2019-04-11 09:42:52.278796+00", -# "user_id": 1111, -# "country_id": 47, -# "ns": "Huhhhu vorikeri", -# "data": [{"id": "1.1", "op": 3, "nt": "notes here"}, {"id": "1.2", "op": 0, "nt": "notes here also"}] -# }' \ -# http://localhost:8000/sendperform -# *** Afterpurger: -# delete from per_formdata where notes like 'notes here%'; delete from per_form where name='Nemo'; + return JsonResponse({'status': 'ok'}) diff --git a/registrations/test_views.py b/registrations/test_views.py index 0e6a18737..eb5b623fc 100644 --- a/registrations/test_views.py +++ b/registrations/test_views.py @@ -6,21 +6,11 @@ # 6. Use the admin token and new user username to query views.ValidateUser # 7. Confirm that a user without an official email is activated. -from django.test import TestCase from rest_framework.test import APITestCase from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist -from django.utils.crypto import get_random_string from .models import Pending from api.models import Country -from api.views import ( - bad_request, - bad_http_request, - PublicJsonPostView, - PublicJsonRequestView, - GetAuthToken, -) class TwoGatekeepersTest(APITestCase): def setUp(self): @@ -30,7 +20,7 @@ def setUp(self): user2 = User.objects.create(username='ke', email='ke@arcs.org.af') user2.set_password('12345678') user2.save() - country = Country.objects.create(name='country') + Country.objects.create(name='country') def test_two_gatekeepers(self): # 1. Created two users to function as gatekeepers (with checkable email) @@ -41,12 +31,12 @@ def test_two_gatekeepers(self): 'email': 'pe@doesnotexist.hu', 'username': newusr, 'password': '87654321', - 'country': country.pk, + 'country': country.pk, 'organizationType': 'OTHR', 'organization': 'Zoo', 'firstname': 'Peter', 'lastname': 'Falk', - 'contact': [{'email':'jo@arcs.org.af'}, {'email':'ke@arcs.org.af'}] + 'contact': [{'email': 'jo@arcs.org.af'}, {'email': 'ke@arcs.org.af'}] } headers = {'CONTENT_TYPE': 'application/json'} resp = self.client.post('/register', body, format='json', headers=headers) @@ -78,7 +68,7 @@ def test_two_gatekeepers(self): # 6a_1repeat. The first token should be unusable now to query views.ValidateUser again resp = self.client.get('/validate_user', body2, format='json', headers=headers) - #resp.content: You, as an administrator has already confirmed the registration of pe user + # resp.content: You, as an administrator has already confirmed the registration of pe user self.assertEqual(resp.status_code, 400) # 7a_1. Confirming that a user without an official email is STILL NOT activated diff --git a/registrations/views.py b/registrations/views.py index 4de9bee75..130ee9766 100644 --- a/registrations/views.py +++ b/registrations/views.py @@ -1,4 +1,3 @@ -import json import pytz from django.db.models import Q from django.db.models.functions import Lower @@ -9,11 +8,10 @@ from django.contrib.auth.models import User from django.http import JsonResponse, HttpResponse from django.template.loader import render_to_string +from rest_framework.views import APIView from api.views import ( bad_request, bad_http_request, - PublicJsonPostView, - PublicJsonRequestView, ) from api.models import Country from .models import Pending, DomainWhitelist @@ -25,10 +23,10 @@ def is_valid_domain(email): domain = email.lower().split('@')[1] is_allowed = ( DomainWhitelist.objects - .filter(is_active=True) - .annotate(domain_name_lower=Lower('domain_name')) - .filter(domain_name_lower=domain) - .exists() + .filter(is_active=True) + .annotate(domain_name_lower=Lower('domain_name')) + .filter(domain_name_lower=domain) + .exists() ) if is_allowed or domain in ['ifrc.org', 'arcs.org.af', 'voroskereszt.hu']: @@ -48,33 +46,33 @@ def get_valid_admins(contacts): return emails -def create_active_user(raw): - user = User.objects.create_user(username=raw['username'], - first_name=raw['firstname'], - last_name=raw['lastname'], - email=raw['email'], - password=raw['password']) +def create_inactive_user(username, firstname, lastname, email, password): + user = User.objects.create_user(username=username, + first_name=firstname, + last_name=lastname, + email=email, + password=password, + is_active=False) return user -def set_user_profile_inactive(user, raw): - user.is_active = False - user.profile.country = Country.objects.get(pk=raw['country']) - user.profile.org_type = raw['organizationType'] - user.profile.org = raw['organization'] - user.profile.city = raw['city'] if 'city' in raw else None - user.profile.department = raw['department'] if 'department' in raw else None - user.profile.position = raw['position'] if 'position' in raw else None - user.profile.phone_number = raw['phoneNumber'] if 'phoneNumber' in raw else None +def set_user_profile(user, country, organization_type, organization, city, department, position, phone_number): + user.profile.country = Country.objects.get(pk=country) + user.profile.org_type = organization_type + user.profile.org = organization + user.profile.city = city + user.profile.department = department + user.profile.position = position + user.profile.phone_number = phone_number user.save() return user -class NewRegistration(PublicJsonPostView): - def handle_post(self, request, *args, **kwargs): - body = json.loads(request.body.decode('utf-8')) +class NewRegistration(APIView): + permission_classes = [] - required_fields = [ + def post(self, request): + required_fields = ( 'email', 'username', 'password', @@ -83,33 +81,48 @@ def handle_post(self, request, *args, **kwargs): 'organization', 'firstname', 'lastname', - ] + ) - missing_fields = [field for field in required_fields if field not in body] - if len(missing_fields): + missing_fields = [field for field in required_fields if field not in request.data] + if missing_fields: return bad_request('Could not complete request. Please submit %s' % ', '.join(missing_fields)) - is_staff = is_valid_domain(body['email']) - admins = True if is_staff else get_valid_admins(body['contact']) + email = request.data.get('email', None) + username = request.data.get('username', None) + password = request.data.get('password', None) + firstname = request.data.get('firstname', None) + lastname = request.data.get('lastname', None) + country = request.data.get('country', None) + city = request.data.get('city', None) + organization_type = request.data.get('organizationType', None) + organization = request.data.get('organization', None) + department = request.data.get('department', None) + position = request.data.get('position', None) + phone_number = request.data.get('phoneNumber', None) + contact = request.data.get('contact', None) + + is_staff = is_valid_domain(email) + admins = True if is_staff else get_valid_admins(contact) if not admins: return bad_request('Non-IFRC users must submit two valid admin emails.') - if User.objects.filter(email__iexact=body['email']).count() > 0: + if User.objects.filter(email__iexact=email).exists(): return bad_request('A user with that email address already exists.') - if User.objects.filter(username__iexact=body['username']).count() > 0: + if User.objects.filter(username__iexact=username).exists(): return bad_request('That username is taken, please choose a different one.') - if ' ' in body['username']: + if ' ' in username: return bad_request('Username can not contain spaces, please choose a different one.') + # Create the User object try: - user = create_active_user(body) - except: + user = create_inactive_user(username, firstname, lastname, email, password) + except Exception: return bad_request('Could not create user.') + # Set the User Profile properties try: - # Note, this also sets the user's active status to False - set_user_profile_inactive(user, body) - except: - User.objects.filter(username=body['username']).delete() + set_user_profile(user, country, organization_type, organization, city, department, position, phone_number) + except Exception: + User.objects.filter(username=username).delete() return bad_request('Could not create user profile.') pending = Pending.objects.create(user=user, @@ -128,7 +141,7 @@ def handle_post(self, request, *args, **kwargs): 'confirmation_link': 'https://%s/verify_email/?token=%s&user=%s' % ( settings.BASE_URL, # on PROD it should point to goadmin... pending.token, - body['username'], + username, ) } @@ -139,15 +152,15 @@ def handle_post(self, request, *args, **kwargs): template = 'email/registration/verify-outside-email.html' send_notification('Validate your account', - [body['email']], + [email], render_to_string(template, email_context), - 'Validate account - ' + body['username']) + 'Validate account - ' + username) return JsonResponse({'status': 'ok'}) -class VerifyEmail(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class VerifyEmail(APIView): + def get(self, request): token = request.GET.get('token', None) user = request.GET.get('user', None) if not token or not user: @@ -189,7 +202,7 @@ def handle_get(self, request, *args, **kwargs): token = pending_user.admin_token_1 if idx == 0 else pending_user.admin_token_2 email_context = { 'validation_link': 'https://%s/validate_user/?token=%s&user=%s' % ( - settings.BASE_URL, # on PROD it should point to goadmin... + settings.BASE_URL, # on PROD it should point to goadmin... token, user, ), @@ -206,8 +219,8 @@ def handle_get(self, request, *args, **kwargs): return HttpResponse(render_to_string('registration/validation-sent.html')) -class ValidateUser(PublicJsonRequestView): - def handle_get(self, request, *args, **kwargs): +class ValidateUser(APIView): + def get(self, request): token = request.GET.get('token', None) user = request.GET.get('user', None) if not token or not user: