From 2e8be04328a5bf99462a62dc36e132d0b133e620 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 23 Jan 2020 15:29:06 +0100 Subject: [PATCH 001/106] Add basic rest calls --- server/core/hep_client.py | 96 ++++++++++++++++++++++++++++ server/core/tests/test_hep_client.py | 17 +++++ 2 files changed, 113 insertions(+) create mode 100644 server/core/hep_client.py create mode 100644 server/core/tests/test_hep_client.py diff --git a/server/core/hep_client.py b/server/core/hep_client.py new file mode 100644 index 00000000..14942d2b --- /dev/null +++ b/server/core/hep_client.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 23.01.20 +# @author: chrigu +from django.conf import settings +import logging +import requests + +logger = logging.getLogger(__name__) + + +class MagentoException(Exception): + pass + + +class MagentoClient: + URL = 'https://stage.hep-verlag.ch' + # URL = 'https://www.hep-verlag.ch' + WEBSITE_ID = 1 + HEADERS = { + 'accept': 'application/json', + 'content-type': 'application/json' + } + + def _call(self, url, method='get', data=None, additional_headers=None): + + request_url = '{}{}'.format(self.URL, url) + + if additional_headers: + headers = {**additional_headers, **self.HEADERS} + else: + headers = self.HEADERS + + if method == 'post': + response = requests.post(request_url, json=data, headers=headers) + elif method == 'get': + if data: + response = requests.get(request_url, headers=headers, data=data) + else: + response = requests.get(request_url, headers=headers) + + if response.status_code != 200: + raise MagentoException(response.status_code, response.json()) + + logger.info(response.json()) + return response + + def is_email_available(self, email): + response = self._call('/rest/deutsch/V1/customers/isEmailAvailable', method='post', + data={'customerEmail': email, 'websiteId': self.WEBSITE_ID}) + return response.json() + + def customer_verify_email(self, confirmation_key): + response = self._call('/rest/V1/customers/me', method='put', data={'confirmationKey': confirmation_key}) + return response.json() + + def customer_create(self, customer_data, address): + + if customer_data['prefix'] == 'Herr': + customer_data['gender'] = 1 + else: + customer_data['gender'] = 2 + + address['country_id'] = 'CH' + address['default_billing'] = True + address['default_shipping'] = True + + customer_data['addresses'] = [address] + + response = self._call('/rest/deutsch/V1/customers', method='post', data=customer_data) + return response.json() + + def customer_token(self, username, password): + response = self._call('/rest/deutsch/V1/integration/customer/token', 'post', + data={'username': username, 'password': password}) + return response.json() + + def customer_me(self, token): + response = self._call('/rest/V1/customers/me', additional_headers={'authorization': 'Bearer {}'.format(token)}) + return response.json() + + def customer_orders(self, admin_token, customer_id): + url = ("/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0][" + "field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={}".format(customer_id)) + + response = self._call(url, additional_headers={'authorization': 'Bearer {}'.format(admin_token)}) + return response.json() + + def coupon_redeem(self, coupon, customer_id): + response = self._call('/rest/deutsch/V1/coupon/{}/customer/{}'.format(coupon, customer_id), method='put') + return response diff --git a/server/core/tests/test_hep_client.py b/server/core/tests/test_hep_client.py new file mode 100644 index 00000000..775f6a76 --- /dev/null +++ b/server/core/tests/test_hep_client.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 23.01.20 +# @author: chrigu +import json + +from django.test import TestCase, Client +from core.factories import UserFactory + + +class HepClientTestCases(TestCase): + pass From 87ceb5fc0ef57672c61477edb8d621aceee3c12f Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 23 Jan 2020 16:46:55 +0100 Subject: [PATCH 002/106] Setup views and tests --- server/core/hep_client.py | 9 +- server/core/settings.py | 5 + server/registration/mutations_public.py | 67 ++- server/registration/serializers.py | 6 + server/users/managers.py | 4 +- server/users/mutations_public.py | 33 +- server/users/tests/orders.json | 526 ++++++++++++++++++++++++ server/users/tests/test_login.py | 20 +- 8 files changed, 637 insertions(+), 33 deletions(-) create mode 100644 server/users/tests/orders.json diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 14942d2b..884bab11 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -14,11 +14,11 @@ import requests logger = logging.getLogger(__name__) -class MagentoException(Exception): +class HepClientException(Exception): pass -class MagentoClient: +class HepClient: URL = 'https://stage.hep-verlag.ch' # URL = 'https://www.hep-verlag.ch' WEBSITE_ID = 1 @@ -45,7 +45,7 @@ class MagentoClient: response = requests.get(request_url, headers=headers) if response.status_code != 200: - raise MagentoException(response.status_code, response.json()) + raise HepClientException(response.status_code, response.json()) logger.info(response.json()) return response @@ -55,6 +55,9 @@ class MagentoClient: data={'customerEmail': email, 'websiteId': self.WEBSITE_ID}) return response.json() + def is_email_verified(self, email): + return True + def customer_verify_email(self, confirmation_key): response = self._call('/rest/V1/customers/me', method='put', data={'confirmationKey': confirmation_key}) return response.json() diff --git a/server/core/settings.py b/server/core/settings.py index 6d20ef89..99395817 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -361,8 +361,13 @@ SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") SENDGRID_SANDBOX_MODE_IN_DEBUG = False DEFAULT_FROM_EMAIL = 'myskillbox ' +# Taskbase TASKBASE_USER = os.environ.get("TASKBASE_USER") TASKBASE_PASSWORD = os.environ.get("TASKBASE_PASSWORD") TASKBASE_SUPERUSER = os.environ.get("TASKBASE_SUPERUSER") TASKBASE_SUPERPASSWORD = os.environ.get("TASKBASE_SUPERPASSWORD") TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL") + + +USE_LOCAL_REGISTRATION = False + diff --git a/server/registration/mutations_public.py b/server/registration/mutations_public.py index 181fdec5..39224a6b 100644 --- a/server/registration/mutations_public.py +++ b/server/registration/mutations_public.py @@ -8,8 +8,10 @@ # Created on 2019-10-08 # @author: chrigu import graphene +from django.conf import settings from graphene import relay +from core.hep_client import HepClient, HepClientException from core.views import SetPasswordView from registration.models import License from registration.serializers import RegistrationSerializer @@ -51,31 +53,31 @@ class Registration(relay.ClientIDMutation): serializer = RegistrationSerializer(data=registration_data) if serializer.is_valid(): - user = User.objects.create_user_with_random_password(serializer.data['first_name'], - serializer.data['last_name'], - serializer.data['email']) - sb_license = License.objects.create(licensee=user, license_type=serializer.context['license_type']) - if sb_license.license_type.is_teacher_license(): - teacher_role = Role.objects.get(key=Role.objects.TEACHER_KEY) - UserRole.objects.get_or_create(user=user, role=teacher_role) - default_class_name = SchoolClass.generate_default_group_name() - default_class = SchoolClass.objects.create(name=default_class_name) - user.school_classes.add(default_class) + if settings.USE_LOCAL_REGISTRATION: + return cls.create_local_user(serializer, info) else: - student_role = Role.objects.get(key=Role.objects.STUDENT_KEY) - UserRole.objects.get_or_create(user=user, role=student_role) + hep_client = HepClient() - password_reset_view = SetPasswordView() - password_reset_view.request = info.context - form = password_reset_view.form_class({'email': user.email}) + try: + email_available = hep_client.is_email_available(serializer['email']) + except HepClientException: + # Todo: handle error from exception (set on object, code & message) + return cls(success=False, errors=None) - if not form.is_valid(): - return cls(success=False, errors=form.errors) + if not email_available: + errors = [MutationError(field='email', errors=['already_exists'])] + return cls(success=False, errors=errors) - password_reset_view.form_valid(form) + try: + response = hep_client.customer_create(serializer.data, None) + except HepClientException: + # Todo: handle error from exception (set on object, code & message) + return cls(success=False, errors=None) - return cls(success=True) + # create or update local user + + # show verfiy page errors = [] for key, value in serializer.errors.items(): @@ -87,6 +89,33 @@ class Registration(relay.ClientIDMutation): return cls(success=False, errors=errors) + @classmethod + def create_local_user(cls, serializer, info): + user = User.objects.create_user_with_random_password(serializer.data['first_name'], + serializer.data['last_name'], + serializer.data['email']) + sb_license = License.objects.create(licensee=user, license_type=serializer.context['license_type']) + + if sb_license.license_type.is_teacher_license(): + teacher_role = Role.objects.get(key=Role.objects.TEACHER_KEY) + UserRole.objects.get_or_create(user=user, role=teacher_role) + default_class_name = SchoolClass.generate_default_group_name() + default_class = SchoolClass.objects.create(name=default_class_name) + user.school_classes.add(default_class) + else: + student_role = Role.objects.get(key=Role.objects.STUDENT_KEY) + UserRole.objects.get_or_create(user=user, role=student_role) + + password_reset_view = SetPasswordView() + password_reset_view.request = info.context + form = password_reset_view.form_class({'email': user.email}) + + if not form.is_valid(): + return cls(success=False, errors=form.errors) + + password_reset_view.form_valid(form) + + return cls(success=True) class RegistrationMutations: registration = Registration.Field() diff --git a/server/registration/serializers.py b/server/registration/serializers.py index 887fbbe1..ab2e95dd 100644 --- a/server/registration/serializers.py +++ b/server/registration/serializers.py @@ -7,6 +7,7 @@ # # Created on 2019-10-08 # @author: chrigu +from django.conf import settings from django.contrib.auth import get_user_model from rest_framework import serializers from rest_framework.fields import CharField, EmailField @@ -22,7 +23,12 @@ class RegistrationSerializer(serializers.Serializer): skillbox_license = None def validate_email(self, value): + lower_email = value.lower() + + if not settings.USE_LOCAL_REGISTRATION: + return lower_email + # the email is used as username if len(get_user_model().objects.filter(username=lower_email)) > 0: raise serializers.ValidationError(_(u'Diese E-Mail ist bereits registriert')) diff --git a/server/users/managers.py b/server/users/managers.py index e20312cf..cca165b3 100644 --- a/server/users/managers.py +++ b/server/users/managers.py @@ -86,6 +86,8 @@ class UserManager(DjangoUserManager): user, created = self.model.objects.get_or_create(email=email, username=email) user.first_name = first_name user.last_name = last_name - user.set_password(self.model.objects.make_random_password()) + # Todo: remove if not used + # user.set_password(self.model.objects.make_random_password()) + user.set_unusable_password() user.save() return user diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 565b9d84..a5638983 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -9,9 +9,11 @@ # @author: chrigu import graphene +from django.conf import settings from django.contrib.auth import authenticate, login from graphene import relay +from core.hep_client import HepClient from registration.models import License @@ -30,10 +32,33 @@ class Login(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input')) - if user is None: - error = LoginError(field='invalid_credentials') - return cls(success=False, errors=[error]) + # get token + # wrong password + # + # is email verified + # -> show screen + # local license + # -> show coupon + # get hep orders + # -> show coupon + # save role information + # login + + if not settings.USE_LOCAL_REGISTRATION: + user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input')) + if user is None: + error = LoginError(field='invalid_credentials') + return cls(success=False, errors=[error]) + + else: + hep_client = HepClient() + + # Todo catch error + token = hep_client.customer_token(kwargs.get('username_input'), kwargs.get('password_input')) + # Todo save token + #verify email + + user_license = None diff --git a/server/users/tests/orders.json b/server/users/tests/orders.json new file mode 100644 index 00000000..9b1db3a2 --- /dev/null +++ b/server/users/tests/orders.json @@ -0,0 +1,526 @@ +{ + "items": [ + { + "base_currency_code": "CHF", + "base_discount_amount": 0, + "base_grand_total": 46, + "base_discount_tax_compensation_amount": 0, + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "base_subtotal": 44.88, + "base_subtotal_incl_tax": 46, + "base_tax_amount": 1.12, + "base_total_due": 46, + "base_to_global_rate": 1, + "base_to_order_rate": 1, + "billing_address_id": 83693, + "created_at": "2018-07-19 15:05:33", + "customer_email": "1heptest19072018@mailinator.com", + "customer_firstname": "Test", + "customer_gender": 2, + "customer_group_id": 4, + "customer_id": 49124, + "customer_is_guest": 0, + "customer_lastname": "Test", + "customer_note": "coupon", + "customer_note_notify": 1, + "customer_prefix": "Frau", + "discount_amount": 0, + "email_sent": 1, + "entity_id": 57612, + "global_currency_code": "CHF", + "grand_total": 46, + "discount_tax_compensation_amount": 0, + "increment_id": "1004614768", + "is_virtual": 1, + "order_currency_code": "CHF", + "protect_code": "71aedb", + "quote_id": 104401, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0, + "state": "complete", + "status": "complete", + "store_currency_code": "CHF", + "store_id": 1, + "store_name": "hep verlag\nhep verlag\nhep verlag", + "store_to_base_rate": 0, + "store_to_order_rate": 0, + "subtotal": 44.88, + "subtotal_incl_tax": 46, + "tax_amount": 1.12, + "total_due": 46, + "total_item_count": 1, + "total_qty_ordered": 1, + "updated_at": "2018-07-19 15:05:33", + "weight": 0, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 46, + "base_price": 44.88, + "base_price_incl_tax": 46, + "base_row_invoiced": 0, + "base_row_total": 44.88, + "base_row_total_incl_tax": 46, + "base_tax_amount": 1.12, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:05:33", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80317, + "name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57612, + "original_price": 46, + "price": 44.88, + "price_incl_tax": 46, + "product_id": 8652, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135166, + "row_invoiced": 0, + "row_total": 44.88, + "row_total_incl_tax": 46, + "sku": "978-3-0355-1082-9", + "store_id": 1, + "tax_amount": 1.12, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:05:33", + "weight": 0.01 + } + ], + "billing_address": { + "address_type": "billing", + "city": "Test", + "country_id": "CH", + "customer_address_id": 47579, + "email": "1heptest19072018@mailinator.com", + "entity_id": 83693, + "firstname": "Test", + "lastname": "Test", + "parent_id": 57612, + "postcode": "0000", + "prefix": "Frau", + "street": [ + "Test" + ], + "telephone": null + }, + "payment": { + "account_status": null, + "additional_information": [ + "Rechnung", + null, + null + ], + "amount_ordered": 46, + "base_amount_ordered": 46, + "base_shipping_amount": 0, + "cc_last4": null, + "entity_id": 57612, + "method": "checkmo", + "parent_id": 57612, + "shipping_amount": 0, + "extension_attributes": [] + }, + "status_histories": [ + { + "comment": "payed by couponcode", + "created_at": "2018-07-19 15:05:33", + "entity_id": 244885, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + }, + { + "comment": "licence-coupon \"ebf81a59b968\"", + "created_at": "2018-07-19 15:05:33", + "entity_id": 244884, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + }, + { + "comment": null, + "created_at": "2018-07-19 15:05:33", + "entity_id": 244883, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + }, + { + "comment": "Exported to ERP", + "created_at": "2018-07-19 15:05:33", + "entity_id": 244882, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + } + ], + "extension_attributes": { + "shipping_assignments": [ + { + "shipping": { + "total": { + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0 + } + }, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 46, + "base_price": 44.88, + "base_price_incl_tax": 46, + "base_row_invoiced": 0, + "base_row_total": 44.88, + "base_row_total_incl_tax": 46, + "base_tax_amount": 1.12, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:05:33", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80317, + "name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57612, + "original_price": 46, + "price": 44.88, + "price_incl_tax": 46, + "product_id": 8652, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135166, + "row_invoiced": 0, + "row_total": 44.88, + "row_total_incl_tax": 46, + "sku": "978-3-0355-1082-9", + "store_id": 1, + "tax_amount": 1.12, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:05:33", + "weight": 0.01 + } + ] + } + ] + } + }, + { + "base_currency_code": "CHF", + "base_discount_amount": 0, + "base_grand_total": 24, + "base_discount_tax_compensation_amount": 0, + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "base_subtotal": 23.41, + "base_subtotal_incl_tax": 24, + "base_tax_amount": 0.59, + "base_total_due": 24, + "base_to_global_rate": 1, + "base_to_order_rate": 1, + "billing_address_id": 83696, + "created_at": "2018-07-19 15:19:00", + "customer_email": "1heptest19072018@mailinator.com", + "customer_firstname": "Test", + "customer_gender": 2, + "customer_group_id": 4, + "customer_id": 49124, + "customer_is_guest": 0, + "customer_lastname": "Test", + "customer_note": "coupon", + "customer_note_notify": 1, + "customer_prefix": "Frau", + "discount_amount": 0, + "email_sent": 1, + "entity_id": 57614, + "global_currency_code": "CHF", + "grand_total": 24, + "discount_tax_compensation_amount": 0, + "increment_id": "1004614770", + "is_virtual": 1, + "order_currency_code": "CHF", + "protect_code": "1a88e9", + "quote_id": 104403, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0, + "state": "complete", + "status": "complete", + "store_currency_code": "CHF", + "store_id": 1, + "store_name": "hep verlag\nhep verlag\nhep verlag", + "store_to_base_rate": 0, + "store_to_order_rate": 0, + "subtotal": 23.41, + "subtotal_incl_tax": 24, + "tax_amount": 0.59, + "total_due": 24, + "total_item_count": 1, + "total_qty_ordered": 1, + "updated_at": "2018-07-19 15:19:00", + "weight": 0, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 24, + "base_price": 23.41, + "base_price_incl_tax": 24, + "base_row_invoiced": 0, + "base_row_total": 23.41, + "base_row_total_incl_tax": 24, + "base_tax_amount": 0.59, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:19:00", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80320, + "name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57614, + "original_price": 24, + "price": 23.41, + "price_incl_tax": 24, + "product_id": 8654, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135169, + "row_invoiced": 0, + "row_total": 23.41, + "row_total_incl_tax": 24, + "sku": "978-3-0355-1185-7", + "store_id": 1, + "tax_amount": 0.59, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:19:00", + "weight": 0.01 + } + ], + "billing_address": { + "address_type": "billing", + "city": "Test", + "country_id": "CH", + "customer_address_id": 47579, + "email": "1heptest19072018@mailinator.com", + "entity_id": 83696, + "firstname": "Test", + "lastname": "Test", + "parent_id": 57614, + "postcode": "0000", + "prefix": "Frau", + "street": [ + "Test" + ], + "telephone": null + }, + "payment": { + "account_status": null, + "additional_information": [ + "Rechnung", + null, + null + ], + "amount_ordered": 24, + "base_amount_ordered": 24, + "base_shipping_amount": 0, + "cc_last4": null, + "entity_id": 57614, + "method": "checkmo", + "parent_id": 57614, + "shipping_amount": 0, + "extension_attributes": [] + }, + "status_histories": [ + { + "comment": "payed by couponcode", + "created_at": "2018-07-19 15:19:00", + "entity_id": 244890, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + }, + { + "comment": "licence-coupon \"ece5e74a2b36\"", + "created_at": "2018-07-19 15:19:00", + "entity_id": 244889, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + }, + { + "comment": null, + "created_at": "2018-07-19 15:19:00", + "entity_id": 244888, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + }, + { + "comment": "Exported to ERP", + "created_at": "2018-07-19 15:19:00", + "entity_id": 244887, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + } + ], + "extension_attributes": { + "shipping_assignments": [ + { + "shipping": { + "total": { + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0 + } + }, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 24, + "base_price": 23.41, + "base_price_incl_tax": 24, + "base_row_invoiced": 0, + "base_row_total": 23.41, + "base_row_total_incl_tax": 24, + "base_tax_amount": 0.59, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:19:00", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80320, + "name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57614, + "original_price": 24, + "price": 23.41, + "price_incl_tax": 24, + "product_id": 8654, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135169, + "row_invoiced": 0, + "row_total": 23.41, + "row_total_incl_tax": 24, + "sku": "978-3-0355-1185-7", + "store_id": 1, + "tax_amount": 0.59, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:19:00", + "weight": 0.01 + } + ] + } + ] + } + } + ], + "search_criteria": { + "filter_groups": [ + { + "filters": [ + { + "field": "customer_id", + "value": "49124", + "condition_type": "eq" + } + ] + } + ] + }, + "total_count": 2 +} diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 68433116..34ab1852 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -7,15 +7,25 @@ # # Created on 2019-10-02 # @author: chrigu +import json +from unittest.mock import patch from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, RequestFactory from graphene.test import Client from api.schema_public import schema from core.factories import UserFactory +from core.hep_client import HepClient from registration.factories import LicenseTypeFactory, LicenseFactory from users.models import Role +FAKE_TOKEN = 'abcd12345!' + +with open('orders.json', 'r') as file: + order_data = file.read() + +ORDERS = json.loads(order_data) + class PasswordResetTests(TestCase): def setUp(self): @@ -50,12 +60,10 @@ class PasswordResetTests(TestCase): } }) - def test_user_can_login(self): - password = 'test123' - self.user.set_password(password) - self.user.save() - - result = self.make_login_mutation(self.user.email, password) + @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) + @patch.object(HepClient, 'customer_orders', return_value=ORDERS) + def test_user_can_login(self, orders_mock, token_mock): + result = self.make_login_mutation(self.user.email, 'test123') self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) From bc997bbeeae0278dac9b48fe48c24d5f285453cc Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 28 Jan 2020 08:35:45 +0100 Subject: [PATCH 003/106] Add login happy paths --- server/core/factories.py | 1 + server/core/hep_client.py | 38 ++++++- server/core/settings.py | 3 + server/core/settings_test.py | 2 + server/registration/admin.py | 12 +- server/registration/factories.py | 12 +- server/registration/managers.py | 32 ++++++ .../migrations/0003_auto_20200127_0932.py | 41 +++++++ server/registration/models.py | 25 ++-- server/registration/serializers.py | 7 +- server/users/managers.py | 19 +++- server/users/migrations/0010_magentotoken.py | 24 ++++ server/users/migrations/0011_user_hep_id.py | 18 +++ server/users/models.py | 6 + server/users/mutations_public.py | 83 +++++++++++--- server/users/tests/me_data.json | 41 +++++++ server/users/tests/test_login.py | 107 +++++++++++++----- ...{orders.json => valid_teacher_orders.json} | 4 +- 18 files changed, 390 insertions(+), 85 deletions(-) create mode 100644 server/registration/managers.py create mode 100644 server/registration/migrations/0003_auto_20200127_0932.py create mode 100644 server/users/migrations/0010_magentotoken.py create mode 100644 server/users/migrations/0011_user_hep_id.py create mode 100644 server/users/tests/me_data.json rename server/users/tests/{orders.json => valid_teacher_orders.json} (99%) diff --git a/server/core/factories.py b/server/core/factories.py index 72665bf5..fb0f9990 100644 --- a/server/core/factories.py +++ b/server/core/factories.py @@ -53,6 +53,7 @@ class UserFactory(factory.django.DjangoModelFactory): first_name = factory.LazyAttribute(lambda x: fake.first_name()) last_name = factory.LazyAttribute(lambda x: fake.last_name()) email = factory.LazyAttribute(lambda x: fake.ascii_safe_email()) + hep_id = factory.Sequence(lambda n: n) @factory.post_generation def post(self, create, extracted, **kwargs): diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 884bab11..a937e3c7 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -7,12 +7,16 @@ # # Created on 23.01.20 # @author: chrigu +from datetime import datetime + from django.conf import settings import logging import requests logger = logging.getLogger(__name__) +MYSKILLBOX_TEACHER_EDITION_ISBN = "000-4-5678-9012-3" +MYSKILLBOX_STUDENT_EDITION_ISBN = "123-4-5678-9012-3" class HepClientException(Exception): pass @@ -44,6 +48,7 @@ class HepClient: else: response = requests.get(request_url, headers=headers) + # Todo handle 401 and most important network errors if response.status_code != 200: raise HepClientException(response.status_code, response.json()) @@ -87,7 +92,7 @@ class HepClient: response = self._call('/rest/V1/customers/me', additional_headers={'authorization': 'Bearer {}'.format(token)}) return response.json() - def customer_orders(self, admin_token, customer_id): + def _customer_orders(self, admin_token, customer_id): url = ("/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0][" "field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={}".format(customer_id)) @@ -97,3 +102,34 @@ class HepClient: def coupon_redeem(self, coupon, customer_id): response = self._call('/rest/deutsch/V1/coupon/{}/customer/{}'.format(coupon, customer_id), method='put') return response + + def myskillbox_product_for_customer(self, admin_token, customer_id): + orders = self._customer_orders(admin_token, customer_id) + # Todo return only relevant product + return self._extract_myskillbox_products(orders) + + def _extract_myskillbox_products(self, orders): + # Todo retun all products + product = None + + for order_item in orders['items']: + for item in order_item['items']: + if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN: + product = { + 'edition': 'teacher', + 'raw': item, + 'activated': self._get_item_activation(order_item) + } + + elif not product and item['sku'] == MYSKILLBOX_STUDENT_EDITION_ISBN: + product = { + 'edition': 'student', + 'raw': item, + 'activated': self._get_item_activation(order_item) + } + + return product + def _get_item_activation(self, item): + for history in item['status_histories']: + if history['comment'] == 'payed by couponcode': + return datetime.strptime(history['created_at'], '%Y-%m-%d %H:%M:%S') diff --git a/server/core/settings.py b/server/core/settings.py index 99395817..a662e88c 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -371,3 +371,6 @@ TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL") USE_LOCAL_REGISTRATION = False +# HEP +HEP_ADMIN_TOKEN = "asdf" + diff --git a/server/core/settings_test.py b/server/core/settings_test.py index d7fef8b6..904154df 100644 --- a/server/core/settings_test.py +++ b/server/core/settings_test.py @@ -15,3 +15,5 @@ MIGRATION_MODULES = DisableMigrations() SENDGRID_API_KEY = "" EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' LOGIN_REDIRECT_URL = '/accounts/login/' + +USE_LOCAL_REGISTRATION = False diff --git a/server/registration/admin.py b/server/registration/admin.py index 64370f36..f42dd923 100644 --- a/server/registration/admin.py +++ b/server/registration/admin.py @@ -9,17 +9,11 @@ # @author: chrigu from django.contrib import admin -from registration.models import LicenseType, License - - -@admin.register(LicenseType) -class LicenseTypeAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'key', 'for_role', 'active') - list_filter = ('for_role', 'active') +from registration.models import License @admin.register(License) class LicenseAdmin(admin.ModelAdmin): - list_display = ('license_type', 'licensee') - list_filter = ('license_type', 'licensee') + list_display = ('licensee',) + list_filter = ('licensee',) raw_id_fields = ('licensee',) diff --git a/server/registration/factories.py b/server/registration/factories.py index 50c493de..16e92678 100644 --- a/server/registration/factories.py +++ b/server/registration/factories.py @@ -11,17 +11,7 @@ import random import factory -from registration.models import LicenseType, License - - -class LicenseTypeFactory(factory.django.DjangoModelFactory): - class Meta: - model = LicenseType - - name = factory.Sequence(lambda n: 'license-{}'.format(n)) - active = True - key = factory.Sequence(lambda n: "license-key-%03d" % n) - description = factory.Sequence(lambda n: "Some description %03d" % n) +from registration.models import License class LicenseFactory(factory.django.DjangoModelFactory): diff --git a/server/registration/managers.py b/server/registration/managers.py new file mode 100644 index 00000000..3e35002b --- /dev/null +++ b/server/registration/managers.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 27.01.20 +# @author: chrigu +from datetime import timedelta + +from django.db import models +from users.models import Role + +TEACHER_EDITION_DURATION = 365 +STUDENT_EDITION_DURATION = 4*365 + + +class LicenseManager(models.Manager): + + def create_license_for_role(self, licensee, activation_date, raw, role): + if role == 'teacher': + user_role = Role.objects.get_default_teacher_role() + expiry_date = activation_date + timedelta(TEACHER_EDITION_DURATION) + else: + user_role = Role.objects.get_default_student_role() + expiry_date = activation_date + timedelta(STUDENT_EDITION_DURATION) + + return self._create_license_for_role(licensee, expiry_date, raw, user_role) + + def _create_license_for_role(self, licensee, expiry_date, raw, role): + return self.create(licensee=licensee, expire_date=expiry_date, raw=raw, for_role=role) diff --git a/server/registration/migrations/0003_auto_20200127_0932.py b/server/registration/migrations/0003_auto_20200127_0932.py new file mode 100644 index 00000000..521374f9 --- /dev/null +++ b/server/registration/migrations/0003_auto_20200127_0932.py @@ -0,0 +1,41 @@ +# Generated by Django 2.0.6 on 2020-01-27 09:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_magentotoken'), + ('registration', '0002_auto_20191010_0905'), + ] + + operations = [ + migrations.RemoveField( + model_name='licensetype', + name='for_role', + ), + migrations.RemoveField( + model_name='license', + name='license_type', + ), + migrations.AddField( + model_name='license', + name='expire_date', + field=models.DateField(null=True), + ), + migrations.AddField( + model_name='license', + name='for_role', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.Role'), + ), + migrations.AddField( + model_name='license', + name='raw', + field=models.TextField(default=''), + ), + migrations.DeleteModel( + name='LicenseType', + ), + ] diff --git a/server/registration/models.py b/server/registration/models.py index fb3e60a3..78de8ee8 100644 --- a/server/registration/models.py +++ b/server/registration/models.py @@ -7,28 +7,29 @@ # # Created on 2019-10-08 # @author: chrigu +from datetime import datetime + from django.utils.translation import ugettext_lazy as _ from django.db import models +from registration.managers import LicenseManager from users.managers import RoleManager from users.models import Role, User -class LicenseType(models.Model): +class License(models.Model): + for_role = models.ForeignKey(Role, blank=False, null=True, on_delete=models.CASCADE) + licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE) + expire_date = models.DateField(blank=False, null=True,) + raw = models.TextField(default="") - name = models.CharField(_('License name'), max_length=255, blank=False, null=False) - for_role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE) - key = models.CharField(max_length=128, blank=False, null=False, unique=True) - active = models.BooleanField(_('License active'), default=False) - description = models.TextField(_('Description'), default="") + objects = LicenseManager() def is_teacher_license(self): return self.for_role.key == RoleManager.TEACHER_KEY + def is_valid(self): + return datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day) <= datetime.now() + def __str__(self): - return '%s - role: %s' % (self.name, self.for_role) - - -class License(models.Model): - license_type = models.ForeignKey(LicenseType, blank=False, null=False, on_delete=models.CASCADE) - licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE) + return 'License for role: %s' % self.for_role diff --git a/server/registration/serializers.py b/server/registration/serializers.py index ab2e95dd..2e1aa355 100644 --- a/server/registration/serializers.py +++ b/server/registration/serializers.py @@ -12,7 +12,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from rest_framework.fields import CharField, EmailField from django.utils.translation import ugettext_lazy as _ -from registration.models import License, LicenseType +from registration.models import License class RegistrationSerializer(serializers.Serializer): @@ -38,9 +38,4 @@ class RegistrationSerializer(serializers.Serializer): return lower_email def validate_license_key(self, value): - license_types = LicenseType.objects.filter(key=value, active=True) - if len(license_types) == 0: - raise serializers.ValidationError(_(u'Die Lizenznummer ist ungültig')) - - self.context['license_type'] = license_types[0] # Assuming there is just ONE license per key return value diff --git a/server/users/managers.py b/server/users/managers.py index cca165b3..7bc24278 100644 --- a/server/users/managers.py +++ b/server/users/managers.py @@ -4,6 +4,8 @@ from django.utils.translation import ugettext_lazy as _ from django.db import models from django.contrib.auth.models import UserManager as DjangoUserManager +from core.hep_client import HepClient + class RoleManager(models.Manager): use_in_migrations = True @@ -82,12 +84,27 @@ class UserRoleManager(models.Manager): class UserManager(DjangoUserManager): - def create_user_with_random_password(self, first_name, last_name, email): + + def _create_user_with_random_password_no_save(self, first_name, last_name, email): user, created = self.model.objects.get_or_create(email=email, username=email) user.first_name = first_name user.last_name = last_name # Todo: remove if not used # user.set_password(self.model.objects.make_random_password()) user.set_unusable_password() + return user + + def create_user_with_random_password(self, first_name, last_name, email): + user = self._create_user_with_random_password_no_save() + user.save() + return user + + def create_user_from_hep(self, token): + hep_client = HepClient() + me_data = hep_client.customer_me(token) + user = self.user = self._create_user_with_random_password_no_save( + me_data['firstname'], me_data['lastname'], me_data['email']) + + user.hep_id = me_data['id'] user.save() return user diff --git a/server/users/migrations/0010_magentotoken.py b/server/users/migrations/0010_magentotoken.py new file mode 100644 index 00000000..32fd768f --- /dev/null +++ b/server/users/migrations/0010_magentotoken.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.6 on 2020-01-27 09:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_auto_20191009_0905'), + ] + + operations = [ + migrations.CreateModel( + name='MagentoToken', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(blank=True, max_length=64, null=True)), + ('created_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_token', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/server/users/migrations/0011_user_hep_id.py b/server/users/migrations/0011_user_hep_id.py new file mode 100644 index 00000000..e1b4ef4d --- /dev/null +++ b/server/users/migrations/0011_user_hep_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2020-01-27 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_magentotoken'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='hep_id', + field=models.PositiveIntegerField(null=True), + ), + ] diff --git a/server/users/models.py b/server/users/models.py index 321c9f18..22a99b28 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -15,6 +15,7 @@ class User(AbstractUser): last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True) avatar_url = models.CharField(max_length=254, blank=True, default='') email = models.EmailField(_('email address'), unique=True) + hep_id = models.PositiveIntegerField(null=True, blank=False) objects = UserManager() @@ -166,3 +167,8 @@ class UserSetting(models.Model): user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting') selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE) + +class MagentoToken(models.Model): + token = models.CharField(blank=True, null=True, max_length=64) + created_at = models.DateTimeField(auto_now=True) + user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE, related_name='user_token') diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index a5638983..c1e95087 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -15,6 +15,8 @@ from graphene import relay from core.hep_client import HepClient from registration.models import License +from users.managers import UserRoleManager +from users.models import MagentoToken, User, Role, UserRole, SchoolClass class LoginError(graphene.ObjectType): @@ -32,6 +34,9 @@ class Login(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): + username = kwargs.get('username_input') + password = kwargs.get('password_input') + # get token # wrong password # @@ -44,8 +49,8 @@ class Login(relay.ClientIDMutation): # save role information # login - if not settings.USE_LOCAL_REGISTRATION: - user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input')) + if settings.USE_LOCAL_REGISTRATION: + user = authenticate(username=username, password=password) if user is None: error = LoginError(field='invalid_credentials') return cls(success=False, errors=[error]) @@ -53,24 +58,72 @@ class Login(relay.ClientIDMutation): else: hep_client = HepClient() - # Todo catch error - token = hep_client.customer_token(kwargs.get('username_input'), kwargs.get('password_input')) - # Todo save token - #verify email + # Todo network error catch error + token = hep_client.customer_token(username, password) + try: + user = User.objects.get(email=username) + except User.DoesNotExist: + user = User.objects.create_user_from_hep(token) + # ISBN "123-4-5678-9012-3" + + magento_token, created = MagentoToken.objects.get_or_create(user=user) + magento_token.token = token['token'] + magento_token.save() + + if not hep_client.is_email_verified(username): + # Todo handle unverifed emails + pass + + try: + license = License.objects.get(licensee=user) + except License.DoesNotExist: + product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) + + if product: + license = License.objects.create_license_for_role(user, product['activated'], + product['raw'], product['edition']) + else: + # todo go to shop + pass + + UserRole.objects.create_role_for_user(user, license.for_role.key) + default_class_name = SchoolClass.generate_default_group_name() + default_class = SchoolClass.objects.create(name=default_class_name) + user.school_classes.add(default_class) + + # if teacher create class + # if student add to class if exists??? + + # no orders + # network errors + # Todo get orders from magento + # check items + # show buy or create license - user_license = None + # check items + # show buy page - try: - user_license = License.objects.get(licensee=user) - except License.DoesNotExist: - # current users have no license, allow them to login - pass + if not license.is_valid(): + pass - if user_license is not None and not user_license.license_type.active: - error = LoginError(field='license_inactive') - return cls(success=False, errors=[error]) + + # show page + + # + + # user_license = None + # + # try: + # user_license = License.objects.get(licensee=user) + # except License.DoesNotExist: + # # current users have no license, allow them to login + # pass + # + # if user_license is not None and not user_license.license_type.active: + # error = LoginError(field='license_inactive') + # return cls(success=False, errors=[error]) login(info.context, user) return cls(success=True, errors=[]) diff --git a/server/users/tests/me_data.json b/server/users/tests/me_data.json new file mode 100644 index 00000000..d9590f6b --- /dev/null +++ b/server/users/tests/me_data.json @@ -0,0 +1,41 @@ +{ + "id": 49124, + "group_id": 1, + "default_billing": "47579", + "default_shipping": "47579", + "confirmation": "41b58ba6598a618095e8c70625d7f052", + "created_at": "2018-07-19 15:05:27", + "updated_at": "2019-11-26 17:04:29", + "created_in": "hep verlag", + "email": "1heptest19072018@mailinator.com", + "firstname": "Test", + "lastname": "Test", + "prefix": "Frau", + "gender": 2, + "store_id": 1, + "website_id": 1, + "addresses": [ + { + "id": 47579, + "customer_id": 49124, + "region": { + "region_code": null, + "region": null, + "region_id": 0 + }, + "region_id": 0, + "country_id": "CH", + "street": [ + "Test" + ], + "telephone": "", + "postcode": "0000", + "city": "Test", + "firstname": "Test", + "lastname": "Test", + "prefix": "Frau", + "default_shipping": true, + "default_billing": true + } + ] +} diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 34ab1852..9a0b2523 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -8,30 +8,42 @@ # Created on 2019-10-02 # @author: chrigu import json +import os +from datetime import timedelta from unittest.mock import patch from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, RequestFactory +from django.utils import timezone from graphene.test import Client from api.schema_public import schema from core.factories import UserFactory from core.hep_client import HepClient -from registration.factories import LicenseTypeFactory, LicenseFactory -from users.models import Role +from registration.factories import LicenseFactory +from registration.models import License +from users.models import Role, MagentoToken, User FAKE_TOKEN = 'abcd12345!' -with open('orders.json', 'r') as file: + +dir_path = os.path.dirname(os.path.realpath(__file__)) + +with open('{}/valid_teacher_orders.json'.format(dir_path), 'r') as file: order_data = file.read() ORDERS = json.loads(order_data) +with open('{}/me_data.json'.format(dir_path), 'r') as file: + me_data = file.read() + +ME_DATA = json.loads(me_data) + class PasswordResetTests(TestCase): def setUp(self): self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch') - self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role") - self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role) + Role.objects.create_default_roles() + self.teacher_role = Role.objects.get_default_teacher_role() request = RequestFactory().post('/') @@ -61,38 +73,77 @@ class PasswordResetTests(TestCase): }) @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) - @patch.object(HepClient, 'customer_orders', return_value=ORDERS) - def test_user_can_login(self, orders_mock, token_mock): + def test_user_can_login_with_local_user_and_valid_local_license(self, token_mock): + + now = timezone.now() + expiry_date = now + timedelta(365) + LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() + result = self.make_login_mutation(self.user.email, 'test123') + + token = MagentoToken.objects.get(user=self.user) + self.assertEqual(token.token, FAKE_TOKEN) + self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) - def test_user_cannot_login_with_invalid_password(self): - password = 'test123' - self.user.set_password(password) - self.user.save() + @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) + @patch.object(HepClient, '_customer_orders', return_value=ORDERS) + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token): + result = self.make_login_mutation(ME_DATA['email'], 'test123') - result = self.make_login_mutation(self.user.email, 'test1234') - self.assertFalse(result.get('data').get('login').get('success')) + user = User.objects.get(email=ME_DATA['email']) + token = MagentoToken.objects.get(user=user) + self.assertEqual(token.token, FAKE_TOKEN) - def test_user_with_active_license_can_login(self): - password = 'test123' - self.user.set_password(password) - self.user.save() + user_role_key = user.user_roles.get(user=user).role.key + self.assertEqual(user_role_key, Role.objects.TEACHER_KEY) - LicenseFactory(license_type=self.teacher_license_type, licensee=self.user) + license = License.objects.get(licensee=user) + self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY) - result = self.make_login_mutation(self.user.email, password) self.assertTrue(result.get('data').get('login').get('success')) + self.assertTrue(self.user.is_authenticated) - def test_user_with_inactive_license_cannot_login(self): - password = 'test123' - self.user.set_password(password) - self.user.save() + ## can login with license and user + # can login with no user and license + # can login with no user and local license + # cannot login without user + # cannot login with user and not verfied + # cannot login with user and no license + # cannot login with user and expired license + # non 200 error + # if more than one valid license take correct + # if mulitple licenses and one correct take one test in own class - self.teacher_license_type.active = False - self.teacher_license_type.save() - LicenseFactory(license_type=self.teacher_license_type, licensee=self.user) - result = self.make_login_mutation(self.user.email, password) - self.assertFalse(result.get('data').get('login').get('success')) + # def test_user_cannot_login_with_invalid_password(self): + # password = 'test123' + # self.user.set_password(password) + # self.user.save() + # + # result = self.make_login_mutation(self.user.email, 'test1234') + # self.assertFalse(result.get('data').get('login').get('success')) + # + # def test_user_with_active_license_can_login(self): + # password = 'test123' + # self.user.set_password(password) + # self.user.save() + # + # LicenseFactory(license_type=self.teacher_license_type, licensee=self.user) + # + # result = self.make_login_mutation(self.user.email, password) + # self.assertTrue(result.get('data').get('login').get('success')) + # + # def test_user_with_inactive_license_cannot_login(self): + # password = 'test123' + # self.user.set_password(password) + # self.user.save() + # + # self.teacher_license_type.active = False + # self.teacher_license_type.save() + # LicenseFactory(license_type=self.teacher_license_type, licensee=self.user) + # + # result = self.make_login_mutation(self.user.email, password) + # self.assertFalse(result.get('data').get('login').get('success')) diff --git a/server/users/tests/orders.json b/server/users/tests/valid_teacher_orders.json similarity index 99% rename from server/users/tests/orders.json rename to server/users/tests/valid_teacher_orders.json index 9b1db3a2..3faf2b66 100644 --- a/server/users/tests/orders.json +++ b/server/users/tests/valid_teacher_orders.json @@ -82,7 +82,7 @@ "is_qty_decimal": 0, "is_virtual": 1, "item_id": 80317, - "name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)", + "name": "Myskillbox Lehreredition", "no_discount": 0, "order_id": 57612, "original_price": 46, @@ -99,7 +99,7 @@ "row_invoiced": 0, "row_total": 44.88, "row_total_incl_tax": 46, - "sku": "978-3-0355-1082-9", + "sku": "000-4-5678-9012-3", "store_id": 1, "tax_amount": 1.12, "tax_invoiced": 0, From 9c4e2de2966b3daa2c2b5c05558f5abf9b77bf61 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 28 Jan 2020 15:28:32 +0100 Subject: [PATCH 004/106] Select correct product (basic version) --- server/core/hep_client.py | 65 ++++++++++++++++----- server/core/settings.py | 1 + server/core/tests/test_hep_client.py | 87 +++++++++++++++++++++++++++- server/registration/managers.py | 5 +- server/registration/models.py | 4 +- server/users/mutations_public.py | 9 +-- server/users/tests/test_login.py | 23 ++++++-- 7 files changed, 163 insertions(+), 31 deletions(-) diff --git a/server/core/hep_client.py b/server/core/hep_client.py index a937e3c7..35931d21 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -7,7 +7,7 @@ # # Created on 23.01.20 # @author: chrigu -from datetime import datetime +from datetime import datetime, timedelta from django.conf import settings import logging @@ -18,6 +18,10 @@ logger = logging.getLogger(__name__) MYSKILLBOX_TEACHER_EDITION_ISBN = "000-4-5678-9012-3" MYSKILLBOX_STUDENT_EDITION_ISBN = "123-4-5678-9012-3" +TEACHER_EDITION_DURATION = 365 +STUDENT_EDITION_DURATION = 4*365 + + class HepClientException(Exception): pass @@ -105,31 +109,64 @@ class HepClient: def myskillbox_product_for_customer(self, admin_token, customer_id): orders = self._customer_orders(admin_token, customer_id) - # Todo return only relevant product - return self._extract_myskillbox_products(orders) + products = self._extract_myskillbox_products(orders) + + if len(products) == 0: + return None + else: + return self._get_relevant_product(products) def _extract_myskillbox_products(self, orders): - # Todo retun all products - product = None + products = [] for order_item in orders['items']: for item in order_item['items']: - if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN: + if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN or item['sku'] == MYSKILLBOX_STUDENT_EDITION_ISBN: product = { - 'edition': 'teacher', 'raw': item, 'activated': self._get_item_activation(order_item) } - elif not product and item['sku'] == MYSKILLBOX_STUDENT_EDITION_ISBN: - product = { - 'edition': 'student', - 'raw': item, - 'activated': self._get_item_activation(order_item) - } + if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN: + product['edition'] = 'teacher' + + else: + product['edition'] = 'student' + + products.append(product) + + return products - return product def _get_item_activation(self, item): for history in item['status_histories']: + # todo can there be no date? if history['comment'] == 'payed by couponcode': return datetime.strptime(history['created_at'], '%Y-%m-%d %H:%M:%S') + + def _get_relevant_product(self, products): + + def filter_inactive_products(product): + if product['edition'] == 'teacher': + expiry_delta = product['activated'] + timedelta(TEACHER_EDITION_DURATION) + else: + expiry_delta = product['activated'] + timedelta(STUDENT_EDITION_DURATION) + + if HepClient.is_product_valid(expiry_delta): + return True + else: + return False + + active_products = list(filter(filter_inactive_products, products)) + print(active_products) + + # todo can a teacher have multiple licenses? + # clarify with hep + if len(active_products) == 0: + return None + elif len(active_products) == 1: + return active_products[0] + + + @staticmethod + def is_product_valid(expiry_date): + return expiry_date >= datetime.now() diff --git a/server/core/settings.py b/server/core/settings.py index a662e88c..c34a47e1 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -374,3 +374,4 @@ USE_LOCAL_REGISTRATION = False # HEP HEP_ADMIN_TOKEN = "asdf" + diff --git a/server/core/tests/test_hep_client.py b/server/core/tests/test_hep_client.py index 775f6a76..ecfddf15 100644 --- a/server/core/tests/test_hep_client.py +++ b/server/core/tests/test_hep_client.py @@ -8,10 +8,91 @@ # Created on 23.01.20 # @author: chrigu import json +from datetime import datetime, timedelta -from django.test import TestCase, Client -from core.factories import UserFactory +from django.test import TestCase +from core.hep_client import HepClient, TEACHER_EDITION_DURATION class HepClientTestCases(TestCase): - pass + def setUp(self): + self.hep_client = HepClient() + self.now = datetime.now() + + def test_has_no_valid_product(self): + products = [ + { + 'edition': 'teacher', + 'raw': {}, + 'activated': self.now - timedelta(2*TEACHER_EDITION_DURATION) + }, + { + 'edition': 'teacher', + 'raw': {}, + 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION) + }, + { + 'edition': 'teacher', + 'raw': {}, + 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION) + } + ] + + relevant_product = self.hep_client._get_relevant_product(products) + self.assertIsNone(relevant_product) + + def test_has_valid_product(self): + products = [ + { + 'edition': 'teacher', + 'raw': { + 'id': 0 + }, + 'activated': self.now - timedelta(7) + }, + { + 'edition': 'teacher', + 'raw': { + 'id': 1 + }, + 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION) + }, + { + 'edition': 'teacher', + 'raw': { + 'id': 2 + }, + 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION) + } + ] + + relevant_product = self.hep_client._get_relevant_product(products) + self.assertEqual(relevant_product['raw']['id'], 0) + + def test_has_multiple_valid_products(self): + products = [ + { + 'edition': 'teacher', + 'raw': { + 'id': 0 + }, + 'activated': self.now - timedelta(7) + }, + { + 'edition': 'teacher', + 'raw': { + 'id': 1 + }, + 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION) + }, + { + 'edition': 'teacher', + 'raw': { + 'id': 2 + }, + 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION) + } + ] + + relevant_product = self.hep_client._get_relevant_product(products) + self.assertEqual(relevant_product['raw']['id'], 0) diff --git a/server/registration/managers.py b/server/registration/managers.py index 3e35002b..b54d267d 100644 --- a/server/registration/managers.py +++ b/server/registration/managers.py @@ -10,10 +10,9 @@ from datetime import timedelta from django.db import models -from users.models import Role -TEACHER_EDITION_DURATION = 365 -STUDENT_EDITION_DURATION = 4*365 +from core.hep_client import TEACHER_EDITION_DURATION, STUDENT_EDITION_DURATION +from users.models import Role class LicenseManager(models.Manager): diff --git a/server/registration/models.py b/server/registration/models.py index 78de8ee8..2c79542c 100644 --- a/server/registration/models.py +++ b/server/registration/models.py @@ -9,9 +9,9 @@ # @author: chrigu from datetime import datetime -from django.utils.translation import ugettext_lazy as _ from django.db import models +from core.hep_client import HepClient from registration.managers import LicenseManager from users.managers import RoleManager from users.models import Role, User @@ -29,7 +29,7 @@ class License(models.Model): return self.for_role.key == RoleManager.TEACHER_KEY def is_valid(self): - return datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day) <= datetime.now() + return HepClient.is_product_valid(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day)) def __str__(self): return 'License for role: %s' % self.for_role diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index c1e95087..3641b8f2 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -86,10 +86,11 @@ class Login(relay.ClientIDMutation): # todo go to shop pass - UserRole.objects.create_role_for_user(user, license.for_role.key) - default_class_name = SchoolClass.generate_default_group_name() - default_class = SchoolClass.objects.create(name=default_class_name) - user.school_classes.add(default_class) + if license.for_role.key == Role.objects.TEACHER_KEY: + UserRole.objects.create_role_for_user(user, license.for_role.key) + default_class_name = SchoolClass.generate_default_group_name() + default_class = SchoolClass.objects.create(name=default_class_name) + user.school_classes.add(default_class) # if teacher create class # if student add to class if exists??? diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 9a0b2523..e434a1a2 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -9,7 +9,7 @@ # @author: chrigu import json import os -from datetime import timedelta +from datetime import timedelta, datetime from unittest.mock import patch from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, RequestFactory @@ -21,23 +21,33 @@ from core.factories import UserFactory from core.hep_client import HepClient from registration.factories import LicenseFactory from registration.models import License -from users.models import Role, MagentoToken, User +from users.models import Role, MagentoToken, User, SchoolClass FAKE_TOKEN = 'abcd12345!' +## Setup json data dir_path = os.path.dirname(os.path.realpath(__file__)) with open('{}/valid_teacher_orders.json'.format(dir_path), 'r') as file: order_data = file.read() -ORDERS = json.loads(order_data) - with open('{}/me_data.json'.format(dir_path), 'r') as file: me_data = file.read() ME_DATA = json.loads(me_data) +order_items = json.loads(order_data) + +for order_item in order_items['items']: + for status in order_item['status_histories']: + if status['comment'] == 'payed by couponcode': + yesterday = datetime.now() - timedelta(1) + status['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') + + +ORDERS = order_items + class PasswordResetTests(TestCase): def setUp(self): @@ -103,11 +113,14 @@ class PasswordResetTests(TestCase): license = License.objects.get(licensee=user) self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY) + school_class = SchoolClass.objects.get(users__in=[user]) + self.assertIsNotNone(school_class) + self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) ## can login with license and user - # can login with no user and license + ## can login with no user and license # can login with no user and local license # cannot login without user # cannot login with user and not verfied From e98d2774fddb57d472da0af80f0646e33836c979 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 28 Jan 2020 16:36:06 +0100 Subject: [PATCH 005/106] Test user with no login --- server/core/hep_client.py | 8 +- server/users/mutations_public.py | 28 +- .../users/tests/{ => test_data}/me_data.json | 0 .../tests/test_data/valid_student_orders.json | 526 ++++++++++++++++++ .../{ => test_data}/valid_teacher_orders.json | 0 server/users/tests/test_login.py | 73 ++- 6 files changed, 602 insertions(+), 33 deletions(-) rename server/users/tests/{ => test_data}/me_data.json (100%) create mode 100644 server/users/tests/test_data/valid_student_orders.json rename server/users/tests/{ => test_data}/valid_teacher_orders.json (100%) diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 35931d21..1499ab5f 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -26,6 +26,10 @@ class HepClientException(Exception): pass +class HepClientUnauthorizedException(Exception): + pass + + class HepClient: URL = 'https://stage.hep-verlag.ch' # URL = 'https://www.hep-verlag.ch' @@ -53,7 +57,9 @@ class HepClient: response = requests.get(request_url, headers=headers) # Todo handle 401 and most important network errors - if response.status_code != 200: + if response.status_code == 401: + raise HepClientUnauthorizedException(response.status_code, response.json()) + elif response.status_code != 200: raise HepClientException(response.status_code, response.json()) logger.info(response.json()) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 3641b8f2..88e897c7 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -13,9 +13,8 @@ from django.conf import settings from django.contrib.auth import authenticate, login from graphene import relay -from core.hep_client import HepClient +from core.hep_client import HepClient, HepClientUnauthorizedException from registration.models import License -from users.managers import UserRoleManager from users.models import MagentoToken, User, Role, UserRole, SchoolClass @@ -37,18 +36,6 @@ class Login(relay.ClientIDMutation): username = kwargs.get('username_input') password = kwargs.get('password_input') - # get token - # wrong password - # - # is email verified - # -> show screen - # local license - # -> show coupon - # get hep orders - # -> show coupon - # save role information - # login - if settings.USE_LOCAL_REGISTRATION: user = authenticate(username=username, password=password) if user is None: @@ -58,13 +45,16 @@ class Login(relay.ClientIDMutation): else: hep_client = HepClient() - # Todo network error catch error - token = hep_client.customer_token(username, password) + try: + token = hep_client.customer_token(username, password) + except HepClientUnauthorizedException: + error = LoginError(field='invalid_credentials') + return cls(success=False, errors=[error]) + try: user = User.objects.get(email=username) except User.DoesNotExist: user = User.objects.create_user_from_hep(token) - # ISBN "123-4-5678-9012-3" magento_token, created = MagentoToken.objects.get_or_create(user=user) magento_token.token = token['token'] @@ -82,12 +72,14 @@ class Login(relay.ClientIDMutation): if product: license = License.objects.create_license_for_role(user, product['activated'], product['raw'], product['edition']) + # todo handle no license case else: # todo go to shop pass + UserRole.objects.create_role_for_user(user, license.for_role.key) + if license.for_role.key == Role.objects.TEACHER_KEY: - UserRole.objects.create_role_for_user(user, license.for_role.key) default_class_name = SchoolClass.generate_default_group_name() default_class = SchoolClass.objects.create(name=default_class_name) user.school_classes.add(default_class) diff --git a/server/users/tests/me_data.json b/server/users/tests/test_data/me_data.json similarity index 100% rename from server/users/tests/me_data.json rename to server/users/tests/test_data/me_data.json diff --git a/server/users/tests/test_data/valid_student_orders.json b/server/users/tests/test_data/valid_student_orders.json new file mode 100644 index 00000000..3c3d7c78 --- /dev/null +++ b/server/users/tests/test_data/valid_student_orders.json @@ -0,0 +1,526 @@ +{ + "items": [ + { + "base_currency_code": "CHF", + "base_discount_amount": 0, + "base_grand_total": 46, + "base_discount_tax_compensation_amount": 0, + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "base_subtotal": 44.88, + "base_subtotal_incl_tax": 46, + "base_tax_amount": 1.12, + "base_total_due": 46, + "base_to_global_rate": 1, + "base_to_order_rate": 1, + "billing_address_id": 83693, + "created_at": "2018-07-19 15:05:33", + "customer_email": "1heptest19072018@mailinator.com", + "customer_firstname": "Test", + "customer_gender": 2, + "customer_group_id": 4, + "customer_id": 49124, + "customer_is_guest": 0, + "customer_lastname": "Test", + "customer_note": "coupon", + "customer_note_notify": 1, + "customer_prefix": "Frau", + "discount_amount": 0, + "email_sent": 1, + "entity_id": 57612, + "global_currency_code": "CHF", + "grand_total": 46, + "discount_tax_compensation_amount": 0, + "increment_id": "1004614768", + "is_virtual": 1, + "order_currency_code": "CHF", + "protect_code": "71aedb", + "quote_id": 104401, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0, + "state": "complete", + "status": "complete", + "store_currency_code": "CHF", + "store_id": 1, + "store_name": "hep verlag\nhep verlag\nhep verlag", + "store_to_base_rate": 0, + "store_to_order_rate": 0, + "subtotal": 44.88, + "subtotal_incl_tax": 46, + "tax_amount": 1.12, + "total_due": 46, + "total_item_count": 1, + "total_qty_ordered": 1, + "updated_at": "2018-07-19 15:05:33", + "weight": 0, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 46, + "base_price": 44.88, + "base_price_incl_tax": 46, + "base_row_invoiced": 0, + "base_row_total": 44.88, + "base_row_total_incl_tax": 46, + "base_tax_amount": 1.12, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:05:33", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80317, + "name": "Myskillbox Schüler Edition", + "no_discount": 0, + "order_id": 57612, + "original_price": 46, + "price": 44.88, + "price_incl_tax": 46, + "product_id": 8652, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135166, + "row_invoiced": 0, + "row_total": 44.88, + "row_total_incl_tax": 46, + "sku": "123-4-5678-9012-3", + "store_id": 1, + "tax_amount": 1.12, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:05:33", + "weight": 0.01 + } + ], + "billing_address": { + "address_type": "billing", + "city": "Test", + "country_id": "CH", + "customer_address_id": 47579, + "email": "1heptest19072018@mailinator.com", + "entity_id": 83693, + "firstname": "Test", + "lastname": "Test", + "parent_id": 57612, + "postcode": "0000", + "prefix": "Frau", + "street": [ + "Test" + ], + "telephone": null + }, + "payment": { + "account_status": null, + "additional_information": [ + "Rechnung", + null, + null + ], + "amount_ordered": 46, + "base_amount_ordered": 46, + "base_shipping_amount": 0, + "cc_last4": null, + "entity_id": 57612, + "method": "checkmo", + "parent_id": 57612, + "shipping_amount": 0, + "extension_attributes": [] + }, + "status_histories": [ + { + "comment": "payed by couponcode", + "created_at": "2018-07-19 15:05:33", + "entity_id": 244885, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + }, + { + "comment": "licence-coupon \"ebf81a59b968\"", + "created_at": "2018-07-19 15:05:33", + "entity_id": 244884, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + }, + { + "comment": null, + "created_at": "2018-07-19 15:05:33", + "entity_id": 244883, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + }, + { + "comment": "Exported to ERP", + "created_at": "2018-07-19 15:05:33", + "entity_id": 244882, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57612, + "status": "complete" + } + ], + "extension_attributes": { + "shipping_assignments": [ + { + "shipping": { + "total": { + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0 + } + }, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 46, + "base_price": 44.88, + "base_price_incl_tax": 46, + "base_row_invoiced": 0, + "base_row_total": 44.88, + "base_row_total_incl_tax": 46, + "base_tax_amount": 1.12, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:05:33", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80317, + "name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57612, + "original_price": 46, + "price": 44.88, + "price_incl_tax": 46, + "product_id": 8652, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135166, + "row_invoiced": 0, + "row_total": 44.88, + "row_total_incl_tax": 46, + "sku": "978-3-0355-1082-9", + "store_id": 1, + "tax_amount": 1.12, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:05:33", + "weight": 0.01 + } + ] + } + ] + } + }, + { + "base_currency_code": "CHF", + "base_discount_amount": 0, + "base_grand_total": 24, + "base_discount_tax_compensation_amount": 0, + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "base_subtotal": 23.41, + "base_subtotal_incl_tax": 24, + "base_tax_amount": 0.59, + "base_total_due": 24, + "base_to_global_rate": 1, + "base_to_order_rate": 1, + "billing_address_id": 83696, + "created_at": "2018-07-19 15:19:00", + "customer_email": "1heptest19072018@mailinator.com", + "customer_firstname": "Test", + "customer_gender": 2, + "customer_group_id": 4, + "customer_id": 49124, + "customer_is_guest": 0, + "customer_lastname": "Test", + "customer_note": "coupon", + "customer_note_notify": 1, + "customer_prefix": "Frau", + "discount_amount": 0, + "email_sent": 1, + "entity_id": 57614, + "global_currency_code": "CHF", + "grand_total": 24, + "discount_tax_compensation_amount": 0, + "increment_id": "1004614770", + "is_virtual": 1, + "order_currency_code": "CHF", + "protect_code": "1a88e9", + "quote_id": 104403, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0, + "state": "complete", + "status": "complete", + "store_currency_code": "CHF", + "store_id": 1, + "store_name": "hep verlag\nhep verlag\nhep verlag", + "store_to_base_rate": 0, + "store_to_order_rate": 0, + "subtotal": 23.41, + "subtotal_incl_tax": 24, + "tax_amount": 0.59, + "total_due": 24, + "total_item_count": 1, + "total_qty_ordered": 1, + "updated_at": "2018-07-19 15:19:00", + "weight": 0, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 24, + "base_price": 23.41, + "base_price_incl_tax": 24, + "base_row_invoiced": 0, + "base_row_total": 23.41, + "base_row_total_incl_tax": 24, + "base_tax_amount": 0.59, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:19:00", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80320, + "name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57614, + "original_price": 24, + "price": 23.41, + "price_incl_tax": 24, + "product_id": 8654, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135169, + "row_invoiced": 0, + "row_total": 23.41, + "row_total_incl_tax": 24, + "sku": "978-3-0355-1185-7", + "store_id": 1, + "tax_amount": 0.59, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:19:00", + "weight": 0.01 + } + ], + "billing_address": { + "address_type": "billing", + "city": "Test", + "country_id": "CH", + "customer_address_id": 47579, + "email": "1heptest19072018@mailinator.com", + "entity_id": 83696, + "firstname": "Test", + "lastname": "Test", + "parent_id": 57614, + "postcode": "0000", + "prefix": "Frau", + "street": [ + "Test" + ], + "telephone": null + }, + "payment": { + "account_status": null, + "additional_information": [ + "Rechnung", + null, + null + ], + "amount_ordered": 24, + "base_amount_ordered": 24, + "base_shipping_amount": 0, + "cc_last4": null, + "entity_id": 57614, + "method": "checkmo", + "parent_id": 57614, + "shipping_amount": 0, + "extension_attributes": [] + }, + "status_histories": [ + { + "comment": "payed by couponcode", + "created_at": "2018-07-19 15:19:00", + "entity_id": 244890, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + }, + { + "comment": "licence-coupon \"ece5e74a2b36\"", + "created_at": "2018-07-19 15:19:00", + "entity_id": 244889, + "entity_name": "order", + "is_customer_notified": null, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + }, + { + "comment": null, + "created_at": "2018-07-19 15:19:00", + "entity_id": 244888, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + }, + { + "comment": "Exported to ERP", + "created_at": "2018-07-19 15:19:00", + "entity_id": 244887, + "entity_name": "order", + "is_customer_notified": 0, + "is_visible_on_front": 0, + "parent_id": 57614, + "status": "complete" + } + ], + "extension_attributes": { + "shipping_assignments": [ + { + "shipping": { + "total": { + "base_shipping_amount": 0, + "base_shipping_discount_amount": 0, + "base_shipping_incl_tax": 0, + "base_shipping_tax_amount": 0, + "shipping_amount": 0, + "shipping_discount_amount": 0, + "shipping_discount_tax_compensation_amount": 0, + "shipping_incl_tax": 0, + "shipping_tax_amount": 0 + } + }, + "items": [ + { + "amount_refunded": 0, + "base_amount_refunded": 0, + "base_discount_amount": 0, + "base_discount_invoiced": 0, + "base_discount_tax_compensation_amount": 0, + "base_original_price": 24, + "base_price": 23.41, + "base_price_incl_tax": 24, + "base_row_invoiced": 0, + "base_row_total": 23.41, + "base_row_total_incl_tax": 24, + "base_tax_amount": 0.59, + "base_tax_invoiced": 0, + "created_at": "2018-07-19 15:19:00", + "discount_amount": 0, + "discount_invoiced": 0, + "discount_percent": 0, + "free_shipping": 0, + "discount_tax_compensation_amount": 0, + "is_qty_decimal": 0, + "is_virtual": 1, + "item_id": 80320, + "name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)", + "no_discount": 0, + "order_id": 57614, + "original_price": 24, + "price": 23.41, + "price_incl_tax": 24, + "product_id": 8654, + "product_type": "virtual", + "qty_canceled": 0, + "qty_invoiced": 0, + "qty_ordered": 1, + "qty_refunded": 0, + "qty_shipped": 0, + "quote_item_id": 135169, + "row_invoiced": 0, + "row_total": 23.41, + "row_total_incl_tax": 24, + "sku": "978-3-0355-1185-7", + "store_id": 1, + "tax_amount": 0.59, + "tax_invoiced": 0, + "tax_percent": 2.5, + "updated_at": "2018-07-19 15:19:00", + "weight": 0.01 + } + ] + } + ] + } + } + ], + "search_criteria": { + "filter_groups": [ + { + "filters": [ + { + "field": "customer_id", + "value": "49124", + "condition_type": "eq" + } + ] + } + ] + }, + "total_count": 2 +} diff --git a/server/users/tests/valid_teacher_orders.json b/server/users/tests/test_data/valid_teacher_orders.json similarity index 100% rename from server/users/tests/valid_teacher_orders.json rename to server/users/tests/test_data/valid_teacher_orders.json diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index e434a1a2..adfa4a30 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -11,6 +11,8 @@ import json import os from datetime import timedelta, datetime from unittest.mock import patch + +import requests from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, RequestFactory from django.utils import timezone @@ -27,26 +29,44 @@ FAKE_TOKEN = 'abcd12345!' ## Setup json data +def make_orders_valid(order_items): + for order_item in order_items['items']: + for status in order_item['status_histories']: + if status['comment'] == 'payed by couponcode': + yesterday = datetime.now() - timedelta(1) + status['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') + + return order_items + +# Load data + dir_path = os.path.dirname(os.path.realpath(__file__)) -with open('{}/valid_teacher_orders.json'.format(dir_path), 'r') as file: - order_data = file.read() +with open('{}/test_data/valid_teacher_orders.json'.format(dir_path), 'r') as file: + valid_teacher_order_data = file.read() -with open('{}/me_data.json'.format(dir_path), 'r') as file: +with open('{}/test_data/valid_student_orders.json'.format(dir_path), 'r') as file: + valid_student_order_data = file.read() + +with open('{}/test_data/me_data.json'.format(dir_path), 'r') as file: me_data = file.read() ME_DATA = json.loads(me_data) -order_items = json.loads(order_data) +valid_teacher_order_items = json.loads(valid_teacher_order_data) +VALID_TEACHERS_ORDERS = make_orders_valid(valid_teacher_order_items) -for order_item in order_items['items']: - for status in order_item['status_histories']: - if status['comment'] == 'payed by couponcode': - yesterday = datetime.now() - timedelta(1) - status['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') +valid_student_order_items = json.loads(valid_student_order_data) +VALID_STUDENT_ORDERS = make_orders_valid(valid_student_order_items) -ORDERS = order_items +## Mocks +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + + def json(self): + return {} class PasswordResetTests(TestCase): @@ -98,9 +118,9 @@ class PasswordResetTests(TestCase): self.assertTrue(self.user.is_authenticated) @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) - @patch.object(HepClient, '_customer_orders', return_value=ORDERS) + @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token): + def test_teacher_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token): result = self.make_login_mutation(ME_DATA['email'], 'test123') user = User.objects.get(email=ME_DATA['email']) @@ -119,10 +139,35 @@ class PasswordResetTests(TestCase): self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) + @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) + @patch.object(HepClient, '_customer_orders', return_value=VALID_STUDENT_ORDERS) + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_student_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token): + result = self.make_login_mutation(ME_DATA['email'], 'test123') + + user = User.objects.get(email=ME_DATA['email']) + token = MagentoToken.objects.get(user=user) + self.assertEqual(token.token, FAKE_TOKEN) + + user_role_key = user.user_roles.get(user=user).role.key + self.assertEqual(user_role_key, Role.objects.STUDENT_KEY) + + license = License.objects.get(licensee=user) + self.assertEqual(license.for_role.key, Role.objects.STUDENT_KEY) + + self.assertTrue(result.get('data').get('login').get('success')) + self.assertTrue(self.user.is_authenticated) + + @patch.object(requests, 'post', return_value=MockResponse(401)) + def test_user_with_no_login_cannot_login(self, post_fn): + result = self.make_login_mutation(ME_DATA['email'], 'test123') + + self.assertFalse(result.get('data').get('login').get('success')) + ## can login with license and user ## can login with no user and license - # can login with no user and local license - # cannot login without user + # ?can login with no user and local license + ## cannot login without user # cannot login with user and not verfied # cannot login with user and no license # cannot login with user and expired license From 9856693ea6c495a58ecfebd43d0bc548ec129100 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 28 Jan 2020 16:40:51 +0100 Subject: [PATCH 006/106] Test no validated email case --- server/users/tests/test_login.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index adfa4a30..02158f3c 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -163,6 +163,14 @@ class PasswordResetTests(TestCase): result = self.make_login_mutation(ME_DATA['email'], 'test123') self.assertFalse(result.get('data').get('login').get('success')) + # todo check message + + @patch.object(HepClient, 'is_email_verified', return_value=False) + def test_user_with_unconfirmed_email_cannot_login(self, post_fn): + result = self.make_login_mutation(ME_DATA['email'], 'test123') + + self.assertFalse(result.get('data').get('login').get('success')) + # todo check message ## can login with license and user ## can login with no user and license From 8c9d40cbc73be8c230ef67cc90c406aee6d2d44e Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 29 Jan 2020 08:29:32 +0100 Subject: [PATCH 007/106] Verify error message --- server/users/mutations_public.py | 4 ++-- server/users/tests/test_login.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 88e897c7..98d81519 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -61,8 +61,8 @@ class Login(relay.ClientIDMutation): magento_token.save() if not hep_client.is_email_verified(username): - # Todo handle unverifed emails - pass + error = LoginError(field='email_not_verified') + return cls(success=False, errors=[error]) try: license = License.objects.get(licensee=user) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 02158f3c..9b25dcdb 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -163,20 +163,26 @@ class PasswordResetTests(TestCase): result = self.make_login_mutation(ME_DATA['email'], 'test123') self.assertFalse(result.get('data').get('login').get('success')) - # todo check message + self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'invalid_credentials') + @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, 'is_email_verified', return_value=False) - def test_user_with_unconfirmed_email_cannot_login(self, post_fn): + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_with_unconfirmed_email_cannot_login(self, me_fn, post_fn, token_fn): result = self.make_login_mutation(ME_DATA['email'], 'test123') + user = User.objects.get(email=ME_DATA['email']) + token = MagentoToken.objects.get(user=user) + self.assertEqual(token.token, FAKE_TOKEN) + self.assertFalse(result.get('data').get('login').get('success')) - # todo check message + self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'email_not_verified') ## can login with license and user ## can login with no user and license # ?can login with no user and local license ## cannot login without user - # cannot login with user and not verfied + ## cannot login with user and not verfied # cannot login with user and no license # cannot login with user and expired license # non 200 error From 0e95e872c8d5f24cd3ac8d89078bee2930487079 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 29 Jan 2020 08:36:55 +0100 Subject: [PATCH 008/106] Verify valid product --- server/users/mutations_public.py | 4 ++-- server/users/tests/test_login.py | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 98d81519..c22e9146 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -74,8 +74,8 @@ class Login(relay.ClientIDMutation): product['raw'], product['edition']) # todo handle no license case else: - # todo go to shop - pass + error = LoginError(field='no_valid_license') + return cls(success=False, errors=[error]) UserRole.objects.create_role_for_user(user, license.for_role.key) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 9b25dcdb..2f67bbc5 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -159,7 +159,7 @@ class PasswordResetTests(TestCase): self.assertTrue(self.user.is_authenticated) @patch.object(requests, 'post', return_value=MockResponse(401)) - def test_user_with_no_login_cannot_login(self, post_fn): + def test_user_with_no_login_cannot_login(self, post_mock): result = self.make_login_mutation(ME_DATA['email'], 'test123') self.assertFalse(result.get('data').get('login').get('success')) @@ -168,7 +168,7 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, 'is_email_verified', return_value=False) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_with_unconfirmed_email_cannot_login(self, me_fn, post_fn, token_fn): + def test_user_with_unconfirmed_email_cannot_login(self, me_mock, post_mock, token_mock): result = self.make_login_mutation(ME_DATA['email'], 'test123') user = User.objects.get(email=ME_DATA['email']) @@ -178,13 +178,25 @@ class PasswordResetTests(TestCase): self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'email_not_verified') + @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) + @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_cannot_login_without_license(self, me_mock, product_mock, token_mock): + result = self.make_login_mutation(self.user.email, 'test123') + + token = MagentoToken.objects.get(user=self.user) + self.assertEqual(token.token, FAKE_TOKEN) + + self.assertFalse(result.get('data').get('login').get('success')) + self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + ## can login with license and user ## can login with no user and license # ?can login with no user and local license ## cannot login without user ## cannot login with user and not verfied - # cannot login with user and no license - # cannot login with user and expired license + ## cannot login with user and no license + ## cannot login with user and expired license # non 200 error # if more than one valid license take correct # if mulitple licenses and one correct take one test in own class From c13ed2a5b2fcf2abdecf3b5090630f92838246b1 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 29 Jan 2020 09:52:31 +0100 Subject: [PATCH 009/106] Verify valid product, move default school creation --- server/users/models.py | 6 ++++++ server/users/mutations_public.py | 37 ++++---------------------------- server/users/tests/test_login.py | 16 ++++++++++++++ 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/server/users/models.py b/server/users/models.py index 22a99b28..24da0250 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -94,6 +94,12 @@ class SchoolClass(models.Model): return '{} {}'.format(prefix, index + 1) + @classmethod + def create_default_group_for_teacher(cls, user): + default_class_name = cls.generate_default_group_name() + default_class = cls.objects.create(name=default_class_name) + user.school_classes.add(default_class) + def is_user_in_schoolclass(self, user): return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0 diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index c22e9146..16c23eeb 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -66,6 +66,7 @@ class Login(relay.ClientIDMutation): try: license = License.objects.get(licensee=user) + # Todo how handle invalid license? Cron Job? How to select correct license? Save all licenses? History? except License.DoesNotExist: product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) @@ -80,43 +81,13 @@ class Login(relay.ClientIDMutation): UserRole.objects.create_role_for_user(user, license.for_role.key) if license.for_role.key == Role.objects.TEACHER_KEY: - default_class_name = SchoolClass.generate_default_group_name() - default_class = SchoolClass.objects.create(name=default_class_name) - user.school_classes.add(default_class) - - # if teacher create class - # if student add to class if exists??? - - # no orders - # network errors - # Todo get orders from magento - # check items - # show buy or create license - - - - # check items - # show buy page + SchoolClass.create_default_group_for_teacher(user) if not license.is_valid(): - pass + error = LoginError(field='no_valid_license') + return cls(success=False, errors=[error]) - # show page - - # - - # user_license = None - # - # try: - # user_license = License.objects.get(licensee=user) - # except License.DoesNotExist: - # # current users have no license, allow them to login - # pass - # - # if user_license is not None and not user_license.license_type.active: - # error = LoginError(field='license_inactive') - # return cls(success=False, errors=[error]) login(info.context, user) return cls(success=True, errors=[]) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 2f67bbc5..5e18cdb9 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -190,6 +190,22 @@ class PasswordResetTests(TestCase): self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_cannot_login_local_license_invalid(self, me_mock, token_mock): + now = timezone.now() + expiry_date = now - timedelta(1) + LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() + + result = self.make_login_mutation(self.user.email, 'test123') + + token = MagentoToken.objects.get(user=self.user) + self.assertEqual(token.token, FAKE_TOKEN) + + self.assertFalse(result.get('data').get('login').get('success')) + self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + + ## can login with license and user ## can login with no user and license # ?can login with no user and local license From dbc4f6f2eea8a104353f0e6fad7bf90999ae0327 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 29 Jan 2020 10:10:45 +0100 Subject: [PATCH 010/106] Handle network error --- server/users/mutations_public.py | 36 +++++++++++++++++------------ server/users/tests/test_login.py | 39 ++++++-------------------------- 2 files changed, 28 insertions(+), 47 deletions(-) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 16c23eeb..bd2c5238 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -13,7 +13,7 @@ from django.conf import settings from django.contrib.auth import authenticate, login from graphene import relay -from core.hep_client import HepClient, HepClientUnauthorizedException +from core.hep_client import HepClient, HepClientUnauthorizedException, HepClientException from registration.models import License from users.models import MagentoToken, User, Role, UserRole, SchoolClass @@ -39,8 +39,7 @@ class Login(relay.ClientIDMutation): if settings.USE_LOCAL_REGISTRATION: user = authenticate(username=username, password=password) if user is None: - error = LoginError(field='invalid_credentials') - return cls(success=False, errors=[error]) + return cls.return_login_error('invalid_credentials') else: hep_client = HepClient() @@ -48,8 +47,9 @@ class Login(relay.ClientIDMutation): try: token = hep_client.customer_token(username, password) except HepClientUnauthorizedException: - error = LoginError(field='invalid_credentials') - return cls(success=False, errors=[error]) + return cls.return_login_error('invalid_credentials') + except HepClientException: + return cls.return_login_error('unknown_error') try: user = User.objects.get(email=username) @@ -60,23 +60,27 @@ class Login(relay.ClientIDMutation): magento_token.token = token['token'] magento_token.save() - if not hep_client.is_email_verified(username): - error = LoginError(field='email_not_verified') - return cls(success=False, errors=[error]) + try: + if not hep_client.is_email_verified(username): + return cls.return_login_error('email_not_verified') + except HepClientException: + return cls.return_login_error('unknown_error') try: license = License.objects.get(licensee=user) # Todo how handle invalid license? Cron Job? How to select correct license? Save all licenses? History? except License.DoesNotExist: - product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) + try: + product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) + except HepClientException: + return cls.return_login_error('unknown_error') if product: license = License.objects.create_license_for_role(user, product['activated'], product['raw'], product['edition']) # todo handle no license case else: - error = LoginError(field='no_valid_license') - return cls(success=False, errors=[error]) + return cls.return_login_error('no_valid_license') UserRole.objects.create_role_for_user(user, license.for_role.key) @@ -84,14 +88,16 @@ class Login(relay.ClientIDMutation): SchoolClass.create_default_group_for_teacher(user) if not license.is_valid(): - error = LoginError(field='no_valid_license') - return cls(success=False, errors=[error]) - - + return cls.return_login_error('no_valid_license') login(info.context, user) return cls(success=True, errors=[]) + @classmethod + def return_login_error(cls, message): + error = LoginError(field=message) + return cls(success=False, errors=[error]) + class UserMutations: login = Login.Field() diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 5e18cdb9..0d8a46d7 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -205,6 +205,12 @@ class PasswordResetTests(TestCase): self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + @patch.object(requests, 'post', return_value=MockResponse(500)) + def test_user_gets_notified_if_server_error(self, post_mock): + result = self.make_login_mutation(ME_DATA['email'], 'test123') + + self.assertFalse(result.get('data').get('login').get('success')) + self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'unknown_error') ## can login with license and user ## can login with no user and license @@ -213,37 +219,6 @@ class PasswordResetTests(TestCase): ## cannot login with user and not verfied ## cannot login with user and no license ## cannot login with user and expired license - # non 200 error + ## non 200 error # if more than one valid license take correct # if mulitple licenses and one correct take one test in own class - - - # def test_user_cannot_login_with_invalid_password(self): - # password = 'test123' - # self.user.set_password(password) - # self.user.save() - # - # result = self.make_login_mutation(self.user.email, 'test1234') - # self.assertFalse(result.get('data').get('login').get('success')) - # - # def test_user_with_active_license_can_login(self): - # password = 'test123' - # self.user.set_password(password) - # self.user.save() - # - # LicenseFactory(license_type=self.teacher_license_type, licensee=self.user) - # - # result = self.make_login_mutation(self.user.email, password) - # self.assertTrue(result.get('data').get('login').get('success')) - # - # def test_user_with_inactive_license_cannot_login(self): - # password = 'test123' - # self.user.set_password(password) - # self.user.save() - # - # self.teacher_license_type.active = False - # self.teacher_license_type.save() - # LicenseFactory(license_type=self.teacher_license_type, licensee=self.user) - # - # result = self.make_login_mutation(self.user.email, password) - # self.assertFalse(result.get('data').get('login').get('success')) From a2f634a677c357ebdeb13b11004cf99816cd2905 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 30 Jan 2020 06:47:38 +0100 Subject: [PATCH 011/106] Use token --- server/users/mutations_public.py | 9 +++++--- server/users/tests/test_login.py | 38 ++++++++------------------------ 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index bd2c5238..995f7c6e 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -34,9 +34,9 @@ class Login(relay.ClientIDMutation): def mutate_and_get_payload(cls, root, info, **kwargs): username = kwargs.get('username_input') - password = kwargs.get('password_input') if settings.USE_LOCAL_REGISTRATION: + password = kwargs.get('password_input') user = authenticate(username=username, password=password) if user is None: return cls.return_login_error('invalid_credentials') @@ -44,8 +44,10 @@ class Login(relay.ClientIDMutation): else: hep_client = HepClient() + token = kwargs.get('token') + try: - token = hep_client.customer_token(username, password) + hep_client.customer_me(token) except HepClientUnauthorizedException: return cls.return_login_error('invalid_credentials') except HepClientException: @@ -56,8 +58,9 @@ class Login(relay.ClientIDMutation): except User.DoesNotExist: user = User.objects.create_user_from_hep(token) + #is this needed? magento_token, created = MagentoToken.objects.get_or_create(user=user) - magento_token.token = token['token'] + magento_token.token = token magento_token.save() try: diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 0d8a46d7..9453d579 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -25,7 +25,7 @@ from registration.factories import LicenseFactory from registration.models import License from users.models import Role, MagentoToken, User, SchoolClass -FAKE_TOKEN = 'abcd12345!' +TOKEN = 'abcd12345!' ## Setup json data @@ -102,8 +102,8 @@ class PasswordResetTests(TestCase): } }) - @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) - def test_user_can_login_with_local_user_and_valid_local_license(self, token_mock): + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_can_login_with_local_user_and_valid_local_license(self, me_mock): now = timezone.now() expiry_date = now + timedelta(365) @@ -111,21 +111,15 @@ class PasswordResetTests(TestCase): result = self.make_login_mutation(self.user.email, 'test123') - token = MagentoToken.objects.get(user=self.user) - self.assertEqual(token.token, FAKE_TOKEN) - self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) - @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_teacher_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token): + def test_teacher_can_login_with_local_user_and_remote_license(self, order_mock, me_token): result = self.make_login_mutation(ME_DATA['email'], 'test123') user = User.objects.get(email=ME_DATA['email']) - token = MagentoToken.objects.get(user=user) - self.assertEqual(token.token, FAKE_TOKEN) user_role_key = user.user_roles.get(user=user).role.key self.assertEqual(user_role_key, Role.objects.TEACHER_KEY) @@ -139,15 +133,12 @@ class PasswordResetTests(TestCase): self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) - @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, '_customer_orders', return_value=VALID_STUDENT_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_student_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token): + def test_student_can_login_with_local_user_and_remote_license(self, order_mock, me_token): result = self.make_login_mutation(ME_DATA['email'], 'test123') user = User.objects.get(email=ME_DATA['email']) - token = MagentoToken.objects.get(user=user) - self.assertEqual(token.token, FAKE_TOKEN) user_role_key = user.user_roles.get(user=user).role.key self.assertEqual(user_role_key, Role.objects.STUDENT_KEY) @@ -165,47 +156,36 @@ class PasswordResetTests(TestCase): self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'invalid_credentials') - @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, 'is_email_verified', return_value=False) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_with_unconfirmed_email_cannot_login(self, me_mock, post_mock, token_mock): + def test_user_with_unconfirmed_email_cannot_login(self, me_mock, post_mock): result = self.make_login_mutation(ME_DATA['email'], 'test123') user = User.objects.get(email=ME_DATA['email']) - token = MagentoToken.objects.get(user=user) - self.assertEqual(token.token, FAKE_TOKEN) self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'email_not_verified') - @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_cannot_login_without_license(self, me_mock, product_mock, token_mock): + def test_user_cannot_login_without_license(self, me_mock, product_mock): result = self.make_login_mutation(self.user.email, 'test123') - token = MagentoToken.objects.get(user=self.user) - self.assertEqual(token.token, FAKE_TOKEN) - self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') - @patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN}) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_cannot_login_local_license_invalid(self, me_mock, token_mock): + def test_user_cannot_login_local_license_invalid(self, me_mock): now = timezone.now() expiry_date = now - timedelta(1) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() result = self.make_login_mutation(self.user.email, 'test123') - token = MagentoToken.objects.get(user=self.user) - self.assertEqual(token.token, FAKE_TOKEN) - self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') - @patch.object(requests, 'post', return_value=MockResponse(500)) + @patch.object(requests, 'get', return_value=MockResponse(500)) def test_user_gets_notified_if_server_error(self, post_mock): result = self.make_login_mutation(ME_DATA['email'], 'test123') From 45f887287fbd018e7bbf73e405ff839c2b8c629a Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 30 Jan 2020 07:11:34 +0100 Subject: [PATCH 012/106] Save group_id, verify email confirmed --- server/core/hep_client.py | 4 +- server/users/managers.py | 9 ++-- .../migrations/0012_user_hep_group_id.py | 18 ++++++++ server/users/models.py | 1 + server/users/mutations_public.py | 8 ++-- .../test_data/email_not_confirmed_me.json | 41 +++++++++++++++++++ server/users/tests/test_data/me_data.json | 1 - server/users/tests/test_login.py | 27 ++++++++---- 8 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 server/users/migrations/0012_user_hep_group_id.py create mode 100644 server/users/tests/test_data/email_not_confirmed_me.json diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 1499ab5f..1cca39bd 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -70,8 +70,8 @@ class HepClient: data={'customerEmail': email, 'websiteId': self.WEBSITE_ID}) return response.json() - def is_email_verified(self, email): - return True + def is_email_verified(self, user_data): + return 'confirmation' not in user_data def customer_verify_email(self, confirmation_key): response = self._call('/rest/V1/customers/me', method='put', data={'confirmationKey': confirmation_key}) diff --git a/server/users/managers.py b/server/users/managers.py index 7bc24278..5b489747 100644 --- a/server/users/managers.py +++ b/server/users/managers.py @@ -99,12 +99,11 @@ class UserManager(DjangoUserManager): user.save() return user - def create_user_from_hep(self, token): - hep_client = HepClient() - me_data = hep_client.customer_me(token) + def create_user_from_hep(self, user_data): user = self.user = self._create_user_with_random_password_no_save( - me_data['firstname'], me_data['lastname'], me_data['email']) + user_data['firstname'], user_data['lastname'], user_data['email']) - user.hep_id = me_data['id'] + user.hep_id = user_data['id'] + user.hep_gruop_id = user_data['group_id'] user.save() return user diff --git a/server/users/migrations/0012_user_hep_group_id.py b/server/users/migrations/0012_user_hep_group_id.py new file mode 100644 index 00000000..b2e624d7 --- /dev/null +++ b/server/users/migrations/0012_user_hep_group_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2020-01-30 05:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0011_user_hep_id'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='hep_group_id', + field=models.PositiveIntegerField(null=True), + ), + ] diff --git a/server/users/models.py b/server/users/models.py index 24da0250..c70bc8c8 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -16,6 +16,7 @@ class User(AbstractUser): avatar_url = models.CharField(max_length=254, blank=True, default='') email = models.EmailField(_('email address'), unique=True) hep_id = models.PositiveIntegerField(null=True, blank=False) + hep_group_id = models.PositiveIntegerField(null=True, blank=False) objects = UserManager() diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 995f7c6e..05ca4047 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -47,7 +47,7 @@ class Login(relay.ClientIDMutation): token = kwargs.get('token') try: - hep_client.customer_me(token) + user_data = hep_client.customer_me(token) except HepClientUnauthorizedException: return cls.return_login_error('invalid_credentials') except HepClientException: @@ -56,15 +56,15 @@ class Login(relay.ClientIDMutation): try: user = User.objects.get(email=username) except User.DoesNotExist: - user = User.objects.create_user_from_hep(token) + user = User.objects.create_user_from_hep(user_data) - #is this needed? + #todo is this needed? magento_token, created = MagentoToken.objects.get_or_create(user=user) magento_token.token = token magento_token.save() try: - if not hep_client.is_email_verified(username): + if not hep_client.is_email_verified(user_data): return cls.return_login_error('email_not_verified') except HepClientException: return cls.return_login_error('unknown_error') diff --git a/server/users/tests/test_data/email_not_confirmed_me.json b/server/users/tests/test_data/email_not_confirmed_me.json new file mode 100644 index 00000000..d9590f6b --- /dev/null +++ b/server/users/tests/test_data/email_not_confirmed_me.json @@ -0,0 +1,41 @@ +{ + "id": 49124, + "group_id": 1, + "default_billing": "47579", + "default_shipping": "47579", + "confirmation": "41b58ba6598a618095e8c70625d7f052", + "created_at": "2018-07-19 15:05:27", + "updated_at": "2019-11-26 17:04:29", + "created_in": "hep verlag", + "email": "1heptest19072018@mailinator.com", + "firstname": "Test", + "lastname": "Test", + "prefix": "Frau", + "gender": 2, + "store_id": 1, + "website_id": 1, + "addresses": [ + { + "id": 47579, + "customer_id": 49124, + "region": { + "region_code": null, + "region": null, + "region_id": 0 + }, + "region_id": 0, + "country_id": "CH", + "street": [ + "Test" + ], + "telephone": "", + "postcode": "0000", + "city": "Test", + "firstname": "Test", + "lastname": "Test", + "prefix": "Frau", + "default_shipping": true, + "default_billing": true + } + ] +} diff --git a/server/users/tests/test_data/me_data.json b/server/users/tests/test_data/me_data.json index d9590f6b..111c8d94 100644 --- a/server/users/tests/test_data/me_data.json +++ b/server/users/tests/test_data/me_data.json @@ -3,7 +3,6 @@ "group_id": 1, "default_billing": "47579", "default_shipping": "47579", - "confirmation": "41b58ba6598a618095e8c70625d7f052", "created_at": "2018-07-19 15:05:27", "updated_at": "2019-11-26 17:04:29", "created_in": "hep verlag", diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 9453d579..20943c32 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -51,7 +51,11 @@ with open('{}/test_data/valid_student_orders.json'.format(dir_path), 'r') as fil with open('{}/test_data/me_data.json'.format(dir_path), 'r') as file: me_data = file.read() +with open('{}/test_data/email_not_confirmed_me.json'.format(dir_path), 'r') as file: + not_confirmed_email_me_data = file.read() + ME_DATA = json.loads(me_data) +NOT_CONFIRMED_ME = json.loads(not_confirmed_email_me_data) valid_teacher_order_items = json.loads(valid_teacher_order_data) VALID_TEACHERS_ORDERS = make_orders_valid(valid_teacher_order_items) @@ -109,7 +113,7 @@ class PasswordResetTests(TestCase): expiry_date = now + timedelta(365) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() - result = self.make_login_mutation(self.user.email, 'test123') + result = self.make_login_mutation(self.user.email, TOKEN) self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) @@ -117,7 +121,7 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) def test_teacher_can_login_with_local_user_and_remote_license(self, order_mock, me_token): - result = self.make_login_mutation(ME_DATA['email'], 'test123') + result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) @@ -136,7 +140,7 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, '_customer_orders', return_value=VALID_STUDENT_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) def test_student_can_login_with_local_user_and_remote_license(self, order_mock, me_token): - result = self.make_login_mutation(ME_DATA['email'], 'test123') + result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) @@ -151,7 +155,7 @@ class PasswordResetTests(TestCase): @patch.object(requests, 'post', return_value=MockResponse(401)) def test_user_with_no_login_cannot_login(self, post_mock): - result = self.make_login_mutation(ME_DATA['email'], 'test123') + result = self.make_login_mutation(ME_DATA['email'], TOKEN) self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'invalid_credentials') @@ -159,7 +163,7 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'is_email_verified', return_value=False) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) def test_user_with_unconfirmed_email_cannot_login(self, me_mock, post_mock): - result = self.make_login_mutation(ME_DATA['email'], 'test123') + result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) @@ -169,7 +173,7 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) def test_user_cannot_login_without_license(self, me_mock, product_mock): - result = self.make_login_mutation(self.user.email, 'test123') + result = self.make_login_mutation(self.user.email, TOKEN) self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') @@ -180,14 +184,21 @@ class PasswordResetTests(TestCase): expiry_date = now - timedelta(1) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() - result = self.make_login_mutation(self.user.email, 'test123') + result = self.make_login_mutation(self.user.email, TOKEN) self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + @patch.object(HepClient, 'customer_me', return_value=NOT_CONFIRMED_ME) + def test_user_cannot_login_with_unconfirmed_email(self, me_mock): + result = self.make_login_mutation(self.user.email, TOKEN) + + self.assertFalse(result.get('data').get('login').get('success')) + self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'email_not_verified') + @patch.object(requests, 'get', return_value=MockResponse(500)) def test_user_gets_notified_if_server_error(self, post_mock): - result = self.make_login_mutation(ME_DATA['email'], 'test123') + result = self.make_login_mutation(ME_DATA['email'], TOKEN) self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'unknown_error') From 773547c8833b872d1bd1ce56ac12552589b31297 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 30 Jan 2020 14:46:13 +0100 Subject: [PATCH 013/106] Fix client, refactor user handling, fix tests --- server/core/hep_client.py | 29 +++++++--- server/core/tests/test_hep_client.py | 40 ++++++++++---- server/registration/mutations_public.py | 61 +++++++-------------- server/registration/serializers.py | 41 --------------- server/users/mutations_public.py | 45 ++-------------- server/users/tests/test_login.py | 21 ++++---- server/users/user_signup_login_handler.py | 64 +++++++++++++++++++++++ 7 files changed, 152 insertions(+), 149 deletions(-) delete mode 100644 server/registration/serializers.py create mode 100644 server/users/user_signup_login_handler.py diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 1cca39bd..72e98d49 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -102,6 +102,12 @@ class HepClient: response = self._call('/rest/V1/customers/me', additional_headers={'authorization': 'Bearer {}'.format(token)}) return response.json() + def customer_activate(self, confirmation_key): + response = self._call('/rest/V1/customers/me/activate', method='post', data={ + 'confirmationKey': confirmation_key + }) + return response.json() + def _customer_orders(self, admin_token, customer_id): url = ("/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0][" "field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={}".format(customer_id)) @@ -126,11 +132,18 @@ class HepClient: products = [] for order_item in orders['items']: + + status = '' + if 'status' in order_item: + status = order_item['status'] + for item in order_item['items']: if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN or item['sku'] == MYSKILLBOX_STUDENT_EDITION_ISBN: + product = { 'raw': item, - 'activated': self._get_item_activation(order_item) + 'activated': self._get_item_activation(order_item), + 'status': status } if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN: @@ -144,14 +157,16 @@ class HepClient: return products def _get_item_activation(self, item): - for history in item['status_histories']: - # todo can there be no date? - if history['comment'] == 'payed by couponcode': - return datetime.strptime(history['created_at'], '%Y-%m-%d %H:%M:%S') + if 'created_at' in item: + return datetime.strptime(item['created_at'], '%Y-%m-%d %H:%M:%S') def _get_relevant_product(self, products): - def filter_inactive_products(product): + def filter_valid_products(product): + + if product['status'] != 'complete': + return False + if product['edition'] == 'teacher': expiry_delta = product['activated'] + timedelta(TEACHER_EDITION_DURATION) else: @@ -162,7 +177,7 @@ class HepClient: else: return False - active_products = list(filter(filter_inactive_products, products)) + active_products = list(filter(filter_valid_products, products)) print(active_products) # todo can a teacher have multiple licenses? diff --git a/server/core/tests/test_hep_client.py b/server/core/tests/test_hep_client.py index ecfddf15..06f482ad 100644 --- a/server/core/tests/test_hep_client.py +++ b/server/core/tests/test_hep_client.py @@ -24,17 +24,33 @@ class HepClientTestCases(TestCase): { 'edition': 'teacher', 'raw': {}, - 'activated': self.now - timedelta(2*TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(2*TEACHER_EDITION_DURATION), + 'status': 'complete' }, { 'edition': 'teacher', 'raw': {}, - 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION), + 'status': 'complete' }, { 'edition': 'teacher', 'raw': {}, - 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION), + 'status': 'complete' + } + ] + + relevant_product = self.hep_client._get_relevant_product(products) + self.assertIsNone(relevant_product) + + def test_has_no_not_completed_product(self): + products = [ + { + 'edition': 'teacher', + 'raw': {}, + 'activated': self.now - timedelta(7), + 'status': 'not' } ] @@ -48,21 +64,24 @@ class HepClientTestCases(TestCase): 'raw': { 'id': 0 }, - 'activated': self.now - timedelta(7) + 'activated': self.now - timedelta(7), + 'status': 'complete' }, { 'edition': 'teacher', 'raw': { 'id': 1 }, - 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION), + 'status': 'complete' }, { 'edition': 'teacher', 'raw': { 'id': 2 }, - 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION), + 'status': 'complete' } ] @@ -76,21 +95,24 @@ class HepClientTestCases(TestCase): 'raw': { 'id': 0 }, - 'activated': self.now - timedelta(7) + 'activated': self.now - timedelta(7), + 'status': 'complete' }, { 'edition': 'teacher', 'raw': { 'id': 1 }, - 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION), + 'status': 'complete' }, { 'edition': 'teacher', 'raw': { 'id': 2 }, - 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION) + 'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION), + 'status': 'complete' } ] diff --git a/server/registration/mutations_public.py b/server/registration/mutations_public.py index 39224a6b..0cdc7066 100644 --- a/server/registration/mutations_public.py +++ b/server/registration/mutations_public.py @@ -11,10 +11,9 @@ import graphene from django.conf import settings from graphene import relay -from core.hep_client import HepClient, HepClientException +from core.hep_client import HepClient, HepClientException, HepClientUnauthorizedException from core.views import SetPasswordView from registration.models import License -from registration.serializers import RegistrationSerializer from users.models import User, Role, UserRole, SchoolClass @@ -39,51 +38,29 @@ class Registration(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - first_name = kwargs.get('firstname_input') - last_name = kwargs.get('lastname_input') - email = kwargs.get('email_input') - license_key = kwargs.get('license_key_input') - registration_data = { - 'first_name': first_name, - 'last_name': last_name, - 'email': email, - 'license_key': license_key, - } + confirmation_key = kwargs.get('confirmationKey') - serializer = RegistrationSerializer(data=registration_data) + hep_client = HepClient() - if serializer.is_valid(): + try: + user_data = hep_client.customer_activate(confirmation_key) + except HepClientUnauthorizedException: + return cls.return_login_error('invalid_credentials') + except HepClientException: + return cls.return_login_error('unknown_error') - if settings.USE_LOCAL_REGISTRATION: - return cls.create_local_user(serializer, info) - else: - hep_client = HepClient() + try: + user = User.objects.get(hep_id=user_data['id']) + except User.DoesNotExist: + user = User.objects.create_user_from_hep(user_data) - try: - email_available = hep_client.is_email_available(serializer['email']) - except HepClientException: - # Todo: handle error from exception (set on object, code & message) - return cls(success=False, errors=None) + # show verfiy page - if not email_available: - errors = [MutationError(field='email', errors=['already_exists'])] - return cls(success=False, errors=errors) - - try: - response = hep_client.customer_create(serializer.data, None) - except HepClientException: - # Todo: handle error from exception (set on object, code & message) - return cls(success=False, errors=None) - - # create or update local user - - # show verfiy page - - errors = [] - for key, value in serializer.errors.items(): - error = MutationError(field=key, errors=[]) - for field_error in serializer.errors[key]: - error.errors.append(PublicFieldError(code=field_error.code)) + # errors = [] + # for key, value in serializer.errors.items(): + # error = MutationError(field=key, errors=[]) + # for field_error in serializer.errors[key]: + # error.errors.append(PublicFieldError(code=field_error.code)) errors.append(error) diff --git a/server/registration/serializers.py b/server/registration/serializers.py deleted file mode 100644 index 2e1aa355..00000000 --- a/server/registration/serializers.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# -# ITerativ GmbH -# http://www.iterativ.ch/ -# -# Copyright (c) 2019 ITerativ GmbH. All rights reserved. -# -# Created on 2019-10-08 -# @author: chrigu -from django.conf import settings -from django.contrib.auth import get_user_model -from rest_framework import serializers -from rest_framework.fields import CharField, EmailField -from django.utils.translation import ugettext_lazy as _ -from registration.models import License - - -class RegistrationSerializer(serializers.Serializer): - first_name = CharField(allow_blank=False) - last_name = CharField(allow_blank=False) - email = EmailField(allow_blank=False) - license_key = CharField(allow_blank=False) - skillbox_license = None - - def validate_email(self, value): - - lower_email = value.lower() - - if not settings.USE_LOCAL_REGISTRATION: - return lower_email - - # the email is used as username - if len(get_user_model().objects.filter(username=lower_email)) > 0: - raise serializers.ValidationError(_(u'Diese E-Mail ist bereits registriert')) - elif len(get_user_model().objects.filter(email=lower_email)) > 0: - raise serializers.ValidationError(_(u'Dieser E-Mail ist bereits registriert')) - else: - return lower_email - - def validate_license_key(self, value): - return value diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 05ca4047..c51a452e 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -14,8 +14,7 @@ from django.contrib.auth import authenticate, login from graphene import relay from core.hep_client import HepClient, HepClientUnauthorizedException, HepClientException -from registration.models import License -from users.models import MagentoToken, User, Role, UserRole, SchoolClass +from users.user_signup_login_handler import handle_user_and_verify_products class LoginError(graphene.ObjectType): @@ -43,7 +42,6 @@ class Login(relay.ClientIDMutation): else: hep_client = HepClient() - token = kwargs.get('token') try: @@ -53,45 +51,10 @@ class Login(relay.ClientIDMutation): except HepClientException: return cls.return_login_error('unknown_error') - try: - user = User.objects.get(email=username) - except User.DoesNotExist: - user = User.objects.create_user_from_hep(user_data) + user, error_msg = handle_user_and_verify_products(user_data, token) - #todo is this needed? - magento_token, created = MagentoToken.objects.get_or_create(user=user) - magento_token.token = token - magento_token.save() - - try: - if not hep_client.is_email_verified(user_data): - return cls.return_login_error('email_not_verified') - except HepClientException: - return cls.return_login_error('unknown_error') - - try: - license = License.objects.get(licensee=user) - # Todo how handle invalid license? Cron Job? How to select correct license? Save all licenses? History? - except License.DoesNotExist: - try: - product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) - except HepClientException: - return cls.return_login_error('unknown_error') - - if product: - license = License.objects.create_license_for_role(user, product['activated'], - product['raw'], product['edition']) - # todo handle no license case - else: - return cls.return_login_error('no_valid_license') - - UserRole.objects.create_role_for_user(user, license.for_role.key) - - if license.for_role.key == Role.objects.TEACHER_KEY: - SchoolClass.create_default_group_for_teacher(user) - - if not license.is_valid(): - return cls.return_login_error('no_valid_license') + if error_msg: + return cls.return_login_error(error_msg) login(info.context, user) return cls(success=True, errors=[]) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 20943c32..2c49fcef 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -31,10 +31,9 @@ TOKEN = 'abcd12345!' def make_orders_valid(order_items): for order_item in order_items['items']: - for status in order_item['status_histories']: - if status['comment'] == 'payed by couponcode': - yesterday = datetime.now() - timedelta(1) - status['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') + if 'created_at' in order_item: + yesterday = datetime.now() - timedelta(1) + order_item['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') return order_items @@ -109,6 +108,9 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'customer_me', return_value=ME_DATA) def test_user_can_login_with_local_user_and_valid_local_license(self, me_mock): + self.user.hep_id = ME_DATA['id'] + self.user.save() + now = timezone.now() expiry_date = now + timedelta(365) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() @@ -120,7 +122,7 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_teacher_can_login_with_local_user_and_remote_license(self, order_mock, me_token): + def test_teacher_can_login_with_remote_user_and_remote_license(self, order_mock, me_token): result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) @@ -139,9 +141,9 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, '_customer_orders', return_value=VALID_STUDENT_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_student_can_login_with_local_user_and_remote_license(self, order_mock, me_token): - result = self.make_login_mutation(ME_DATA['email'], TOKEN) + def test_student_can_login_with_remote_user_and_remote_license(self, order_mock, me_token): + result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) user_role_key = user.user_roles.get(user=user).role.key @@ -165,7 +167,7 @@ class PasswordResetTests(TestCase): def test_user_with_unconfirmed_email_cannot_login(self, me_mock, post_mock): result = self.make_login_mutation(ME_DATA['email'], TOKEN) - user = User.objects.get(email=ME_DATA['email']) + User.objects.get(email=ME_DATA['email']) self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'email_not_verified') @@ -178,8 +180,9 @@ class PasswordResetTests(TestCase): self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_cannot_login_local_license_invalid(self, me_mock): + def test_user_cannot_login_local_license_invalid(self, product_mock, me_mock): now = timezone.now() expiry_date = now - timedelta(1) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py new file mode 100644 index 00000000..e276c276 --- /dev/null +++ b/server/users/user_signup_login_handler.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 30.01.20 +# @author: chrigu +from django.conf import settings + +from core.hep_client import HepClient, HepClientException +from registration.models import License +from users.models import User, MagentoToken, UserRole, Role, SchoolClass + + +def handle_user_and_verify_products(user_data, token): + + hep_client = HepClient() + + try: + user = User.objects.get(hep_id=user_data['id']) + except User.DoesNotExist: + user = User.objects.create_user_from_hep(user_data) + + # todo check if email has changed + + # todo is this needed? + magento_token, created = MagentoToken.objects.get_or_create(user=user) + magento_token.token = token + magento_token.save() + + try: + if not hep_client.is_email_verified(user_data): + return user, 'email_not_verified' + except HepClientException: + return user, 'unknown_error' + + try: + license = License.objects.get(licensee=user) + # Todo how handle invalid license? Cron Job? How to select correct license? Save all licenses? History? + except License.DoesNotExist: + try: + # todo is admin token valid, save it? do we need it? + product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) + except HepClientException: + return user, 'unknown_error' + + if product: + license = License.objects.create_license_for_role(user, product['activated'], + product['raw'], product['edition']) + # todo handle no license case + else: + return user, 'no_valid_license' + + UserRole.objects.create_role_for_user(user, license.for_role.key) + + if license.for_role.key == Role.objects.TEACHER_KEY: + SchoolClass.create_default_group_for_teacher(user) + + if not license.is_valid(): + return user, 'no_valid_license' + + return user, None From cd7f79072ecc86c1a910e796638eb00713a027cd Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 3 Feb 2020 08:56:08 +0100 Subject: [PATCH 014/106] Remove user token, use confirmation key --- server/core/hep_client.py | 9 ++ server/registration/mutations_public.py | 62 ++------- .../registration/tests/test_registration.py | 129 ++++++++++-------- server/users/mutations_public.py | 2 +- server/users/user_signup_login_handler.py | 7 +- 5 files changed, 94 insertions(+), 115 deletions(-) diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 72e98d49..b5546a43 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -108,6 +108,15 @@ class HepClient: }) return response.json() + def customers_search(self, email): + response = self._call("/rest/V1/customers/search?searchCriteria[filterGroups][0][filters][0][field]" + "=email&searchCriteria[filterGroups][0][filters][0][value]={}".format(email)) + + json_data = response.json() + if len(json_data) > 0: + return json_data[0] + return None + def _customer_orders(self, admin_token, customer_id): url = ("/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0][" "field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={}".format(customer_id)) diff --git a/server/registration/mutations_public.py b/server/registration/mutations_public.py index 0cdc7066..32e600fb 100644 --- a/server/registration/mutations_public.py +++ b/server/registration/mutations_public.py @@ -12,33 +12,27 @@ from django.conf import settings from graphene import relay from core.hep_client import HepClient, HepClientException, HepClientUnauthorizedException -from core.views import SetPasswordView -from registration.models import License -from users.models import User, Role, UserRole, SchoolClass +from users.user_signup_login_handler import handle_user_and_verify_products class PublicFieldError(graphene.ObjectType): code = graphene.String() -class MutationError(graphene.ObjectType): +class RegistrationError(graphene.ObjectType): field = graphene.String() - errors = graphene.List(PublicFieldError) class Registration(relay.ClientIDMutation): class Input: - firstname_input = graphene.String() - lastname_input = graphene.String() - email_input = graphene.String() - license_key_input = graphene.String() + confirmation_key = graphene.String() success = graphene.Boolean() - errors = graphene.List(MutationError) # todo: change for consistency + errors = graphene.List(RegistrationError) # todo: change for consistency @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - confirmation_key = kwargs.get('confirmationKey') + confirmation_key = kwargs.get('confirmation_key') hep_client = HepClient() @@ -49,50 +43,18 @@ class Registration(relay.ClientIDMutation): except HepClientException: return cls.return_login_error('unknown_error') - try: - user = User.objects.get(hep_id=user_data['id']) - except User.DoesNotExist: - user = User.objects.create_user_from_hep(user_data) + user, error_msg = handle_user_and_verify_products(user_data) - # show verfiy page + if error_msg: + return cls.return_login_error(error_msg) - # errors = [] - # for key, value in serializer.errors.items(): - # error = MutationError(field=key, errors=[]) - # for field_error in serializer.errors[key]: - # error.errors.append(PublicFieldError(code=field_error.code)) - - errors.append(error) - - return cls(success=False, errors=errors) + return cls(success=False, errors=[]) @classmethod - def create_local_user(cls, serializer, info): - user = User.objects.create_user_with_random_password(serializer.data['first_name'], - serializer.data['last_name'], - serializer.data['email']) - sb_license = License.objects.create(licensee=user, license_type=serializer.context['license_type']) + def return_login_error(cls, message): + error = RegistrationError(field=message) + return cls(success=False, errors=[error]) - if sb_license.license_type.is_teacher_license(): - teacher_role = Role.objects.get(key=Role.objects.TEACHER_KEY) - UserRole.objects.get_or_create(user=user, role=teacher_role) - default_class_name = SchoolClass.generate_default_group_name() - default_class = SchoolClass.objects.create(name=default_class_name) - user.school_classes.add(default_class) - else: - student_role = Role.objects.get(key=Role.objects.STUDENT_KEY) - UserRole.objects.get_or_create(user=user, role=student_role) - - password_reset_view = SetPasswordView() - password_reset_view.request = info.context - form = password_reset_view.form_class({'email': user.email}) - - if not form.is_valid(): - return cls(success=False, errors=form.errors) - - password_reset_view.form_valid(form) - - return cls(success=True) class RegistrationMutations: registration = Registration.Field() diff --git a/server/registration/tests/test_registration.py b/server/registration/tests/test_registration.py index 15779a98..77a5be5c 100644 --- a/server/registration/tests/test_registration.py +++ b/server/registration/tests/test_registration.py @@ -13,6 +13,7 @@ from django.test import TestCase, RequestFactory from graphene.test import Client from api.schema import schema +from core.hep_client import HepClient from registration.factories import LicenseTypeFactory, LicenseFactory from registration.models import License from users.managers import RoleManager @@ -43,7 +44,7 @@ class RegistrationTests(TestCase): request.session.save() self.client = Client(schema=schema, context_value=request) - def make_register_mutation(self, first_name, last_name, email, license_key): + def make_register_mutation(self, confirmation_key): mutation = ''' mutation Registration($input: RegistrationInput!){ registration(input: $input) { @@ -57,64 +58,76 @@ class RegistrationTests(TestCase): return self.client.execute(mutation, variables={ 'input': { - 'firstnameInput': first_name, - 'lastnameInput': last_name, - 'emailInput': email, - 'licenseKeyInput': license_key, + 'confirmationKeyInput': confirmation_key } }) - def _assert_user_registration(self, count, email, role_key): - users = User.objects.filter(username=self.email) - self.assertEqual(len(users), count) - user_roles = UserRole.objects.filter(user__email=email, role__key=role_key) - self.assertEqual(len(user_roles), count) - licenses = License.objects.filter(licensee__email=email, license_type__for_role__key=role_key) - self.assertEqual(len(licenses), count) + # @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + # def test_user_can_register_with_valid_confirmation_key(self, me_mock): + # + # self.user.hep_id = ME_DATA['id'] + # self.user.save() + # + # now = timezone.now() + # expiry_date = now + timedelta(365) + # LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() + # + # result = self.make_login_mutation(self.user.email, TOKEN) + # + # self.assertTrue(result.get('data').get('login').get('success')) + # self.assertTrue(self.user.is_authenticated) - def test_user_can_register_as_teacher(self): - self._assert_user_registration(0, self.email, RoleManager.TEACHER_KEY) - school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse') - self.assertEqual(len(school_classes), 0) - result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.teacher_license_type.key) - self.assertTrue(result.get('data').get('registration').get('success')) - self._assert_user_registration(1, self.email, RoleManager.TEACHER_KEY) - school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse') - self.assertEqual(len(school_classes), 1) - user = User.objects.get(email=self.email) - self.assertTrue(school_classes[0].is_user_in_schoolclass(user)) - self.assertEqual(len(mail.outbox), 1) - self.assertEqual(mail.outbox[0].subject, 'Myskillbox: E-Mail bestätigen und Passwort setzen') - - def test_user_can_register_as_student(self): - self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) - result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - self.assertTrue(result.get('data').get('registration').get('success')) - self._assert_user_registration(1, self.email, RoleManager.STUDENT_KEY) - - def test_existing_user_cannot_register(self): - self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) - self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') - - def test_existing_user_cannot_register_with_uppercase_email(self): - self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) - self.make_register_mutation(self.first_name, self.last_name, self.email.upper(), self.student_license_type.key) - result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') - - def test_user_cannot_register_if_firstname_is_missing(self): - result = self.make_register_mutation('', self.last_name, self.email, self.teacher_license_type.key) - self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'first_name') - self.assertFalse(result.get('data').get('registration').get('success')) - - def test_user_cannot_register_if_lastname_is_missing(self): - result = self.make_register_mutation(self.first_name, '', self.email, self.teacher_license_type.key) - self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'last_name') - self.assertFalse(result.get('data').get('registration').get('success')) - - def test_user_cannot_register_if_email_is_missing(self): - result = self.make_register_mutation(self.first_name, self.last_name, '', self.teacher_license_type.key) - self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') - self.assertFalse(result.get('data').get('registration').get('success')) + # def _assert_user_registration(self, count, email, role_key): + # users = User.objects.filter(username=self.email) + # self.assertEqual(len(users), count) + # user_roles = UserRole.objects.filter(user__email=email, role__key=role_key) + # self.assertEqual(len(user_roles), count) + # licenses = License.objects.filter(licensee__email=email, license_type__for_role__key=role_key) + # self.assertEqual(len(licenses), count) + # + # def test_user_can_register_as_teacher(self): + # self._assert_user_registration(0, self.email, RoleManager.TEACHER_KEY) + # school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse') + # self.assertEqual(len(school_classes), 0) + # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.teacher_license_type.key) + # self.assertTrue(result.get('data').get('registration').get('success')) + # self._assert_user_registration(1, self.email, RoleManager.TEACHER_KEY) + # school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse') + # self.assertEqual(len(school_classes), 1) + # user = User.objects.get(email=self.email) + # self.assertTrue(school_classes[0].is_user_in_schoolclass(user)) + # self.assertEqual(len(mail.outbox), 1) + # self.assertEqual(mail.outbox[0].subject, 'Myskillbox: E-Mail bestätigen und Passwort setzen') + # + # def test_user_can_register_as_student(self): + # self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) + # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + # self.assertTrue(result.get('data').get('registration').get('success')) + # self._assert_user_registration(1, self.email, RoleManager.STUDENT_KEY) + # + # def test_existing_user_cannot_register(self): + # self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) + # self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') + # + # def test_existing_user_cannot_register_with_uppercase_email(self): + # self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) + # self.make_register_mutation(self.first_name, self.last_name, self.email.upper(), self.student_license_type.key) + # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') + # + # def test_user_cannot_register_if_firstname_is_missing(self): + # result = self.make_register_mutation('', self.last_name, self.email, self.teacher_license_type.key) + # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'first_name') + # self.assertFalse(result.get('data').get('registration').get('success')) + # + # def test_user_cannot_register_if_lastname_is_missing(self): + # result = self.make_register_mutation(self.first_name, '', self.email, self.teacher_license_type.key) + # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'last_name') + # self.assertFalse(result.get('data').get('registration').get('success')) + # + # def test_user_cannot_register_if_email_is_missing(self): + # result = self.make_register_mutation(self.first_name, self.last_name, '', self.teacher_license_type.key) + # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') + # self.assertFalse(result.get('data').get('registration').get('success')) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index c51a452e..811ae089 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -51,7 +51,7 @@ class Login(relay.ClientIDMutation): except HepClientException: return cls.return_login_error('unknown_error') - user, error_msg = handle_user_and_verify_products(user_data, token) + user, error_msg = handle_user_and_verify_products(user_data) if error_msg: return cls.return_login_error(error_msg) diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py index e276c276..f085d8c5 100644 --- a/server/users/user_signup_login_handler.py +++ b/server/users/user_signup_login_handler.py @@ -14,7 +14,7 @@ from registration.models import License from users.models import User, MagentoToken, UserRole, Role, SchoolClass -def handle_user_and_verify_products(user_data, token): +def handle_user_and_verify_products(user_data): hep_client = HepClient() @@ -25,11 +25,6 @@ def handle_user_and_verify_products(user_data, token): # todo check if email has changed - # todo is this needed? - magento_token, created = MagentoToken.objects.get_or_create(user=user) - magento_token.token = token - magento_token.save() - try: if not hep_client.is_email_verified(user_data): return user, 'email_not_verified' From 6beb4296f7e449e920b3b4db24e8fe0e849111f7 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 3 Feb 2020 10:34:36 +0100 Subject: [PATCH 015/106] Store admin token in db --- server/core/hep_client.py | 5 +++ server/core/managers.py | 39 +++++++++++++++++++++++ server/core/models.py | 21 ++++++++++++ server/core/settings.py | 3 +- server/users/tests/test_login.py | 12 ++++--- server/users/user_signup_login_handler.py | 7 ++-- 6 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 server/core/managers.py create mode 100644 server/core/models.py diff --git a/server/core/hep_client.py b/server/core/hep_client.py index b5546a43..8ae9cc68 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -65,6 +65,11 @@ class HepClient: logger.info(response.json()) return response + def fetch_admin_token(self, admin_user, password): + response = self._call('/rest/deutsch/V1/integration/admin/token', 'post', + data={'username': admin_user, 'password': password}) + return response.json()['token'] + def is_email_available(self, email): response = self._call('/rest/deutsch/V1/customers/isEmailAvailable', method='post', data={'customerEmail': email, 'websiteId': self.WEBSITE_ID}) diff --git a/server/core/managers.py b/server/core/managers.py new file mode 100644 index 00000000..059130a4 --- /dev/null +++ b/server/core/managers.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 03.02.20 +# @author: chrigu +from django.conf import settings +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from core.hep_client import HepClient + +DEFAULT_PK = 1 + + +class AdminDataManager(models.Manager): + hep_client = HepClient() + + def update_admin_token(self): + + admin_token = self.hep_client.fetch_admin_token(settings.HEP_ADMIN_USER, settings.HEP_ADMIN_PASSWORD) + + admin_data, created = self.get_or_create(pk=DEFAULT_PK) + admin_data.hep_admin_token = admin_token + admin_data.save() + return admin_data.hep_admin_token + + def get_admin_token(self): + + try: + admin_token = self.get(pk=DEFAULT_PK) + except self.model.DoesNotExist: + admin_token = self.update_admin_token() + + return admin_token diff --git a/server/core/models.py b/server/core/models.py new file mode 100644 index 00000000..0fba1a3f --- /dev/null +++ b/server/core/models.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 03.02.20 +# @author: chrigu +from datetime import datetime + +from django.db import models + +from core.managers import AdminDataManager + + +class AdminData(models.Model): + hep_admin_token = models.CharField(max_length=100, blank=False, null=False) + updated_at = models.DateTimeField(blank=False, null=True, auto_now=True) + + objects = AdminDataManager() diff --git a/server/core/settings.py b/server/core/settings.py index c34a47e1..86f66bb7 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -372,6 +372,7 @@ TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL") USE_LOCAL_REGISTRATION = False # HEP -HEP_ADMIN_TOKEN = "asdf" +HEP_ADMIN_USER = "adminuser" +HEP_ADMIN_PASSWORD = "password" diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 2c49fcef..912178de 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -122,7 +122,8 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_teacher_can_login_with_remote_user_and_remote_license(self, order_mock, me_token): + @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) + def test_teacher_can_login_with_remote_user_and_remote_license(self, order_mock, me_mock, admin_token_mock): result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) @@ -141,7 +142,8 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, '_customer_orders', return_value=VALID_STUDENT_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_student_can_login_with_remote_user_and_remote_license(self, order_mock, me_token): + @patch.object(HepClient, 'fetch_admin_token', return_value={'token':'AABBCCDDEE**44566'}) + def test_student_can_login_with_remote_user_and_remote_license(self, order_mock, me_mock, admin_token_mock): result = self.make_login_mutation(ME_DATA['email'], TOKEN) user = User.objects.get(email=ME_DATA['email']) @@ -174,7 +176,8 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_cannot_login_without_license(self, me_mock, product_mock): + @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) + def test_user_cannot_login_without_license(self, me_mock, product_mock, admin_token_mock): result = self.make_login_mutation(self.user.email, TOKEN) self.assertFalse(result.get('data').get('login').get('success')) @@ -182,7 +185,8 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - def test_user_cannot_login_local_license_invalid(self, product_mock, me_mock): + @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) + def test_user_cannot_login_local_license_invalid(self, product_mock, me_mock, admin_token_mock): now = timezone.now() expiry_date = now - timedelta(1) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py index f085d8c5..8fc1cf1d 100644 --- a/server/users/user_signup_login_handler.py +++ b/server/users/user_signup_login_handler.py @@ -10,8 +10,9 @@ from django.conf import settings from core.hep_client import HepClient, HepClientException +from core.models import AdminData from registration.models import License -from users.models import User, MagentoToken, UserRole, Role, SchoolClass +from users.models import User, UserRole, Role, SchoolClass def handle_user_and_verify_products(user_data): @@ -36,8 +37,8 @@ def handle_user_and_verify_products(user_data): # Todo how handle invalid license? Cron Job? How to select correct license? Save all licenses? History? except License.DoesNotExist: try: - # todo is admin token valid, save it? do we need it? - product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id) + admin_token = AdminData.objects.get_admin_token() + product = hep_client.myskillbox_product_for_customer(admin_token, user.hep_id) except HepClientException: return user, 'unknown_error' From f628966ae99b705d3deb07cd07cee5cac898b8d4 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 3 Feb 2020 13:30:19 +0100 Subject: [PATCH 016/106] Add registration, implement tests --- .../mock_data}/email_not_confirmed_me.json | 0 .../tests/mock_data}/me_data.json | 0 .../mock_data}/valid_student_orders.json | 0 .../mock_data}/valid_teacher_orders.json | 0 server/core/tests/mock_hep_data_factory.py | 57 ++++++++ server/registration/mutations_public.py | 34 ++--- .../registration/tests/test_registration.py | 126 ++++++------------ server/users/tests/test_login.py | 47 +------ server/users/user_signup_login_handler.py | 2 +- 9 files changed, 123 insertions(+), 143 deletions(-) rename server/{users/tests/test_data => core/tests/mock_data}/email_not_confirmed_me.json (100%) rename server/{users/tests/test_data => core/tests/mock_data}/me_data.json (100%) rename server/{users/tests/test_data => core/tests/mock_data}/valid_student_orders.json (100%) rename server/{users/tests/test_data => core/tests/mock_data}/valid_teacher_orders.json (100%) create mode 100644 server/core/tests/mock_hep_data_factory.py diff --git a/server/users/tests/test_data/email_not_confirmed_me.json b/server/core/tests/mock_data/email_not_confirmed_me.json similarity index 100% rename from server/users/tests/test_data/email_not_confirmed_me.json rename to server/core/tests/mock_data/email_not_confirmed_me.json diff --git a/server/users/tests/test_data/me_data.json b/server/core/tests/mock_data/me_data.json similarity index 100% rename from server/users/tests/test_data/me_data.json rename to server/core/tests/mock_data/me_data.json diff --git a/server/users/tests/test_data/valid_student_orders.json b/server/core/tests/mock_data/valid_student_orders.json similarity index 100% rename from server/users/tests/test_data/valid_student_orders.json rename to server/core/tests/mock_data/valid_student_orders.json diff --git a/server/users/tests/test_data/valid_teacher_orders.json b/server/core/tests/mock_data/valid_teacher_orders.json similarity index 100% rename from server/users/tests/test_data/valid_teacher_orders.json rename to server/core/tests/mock_data/valid_teacher_orders.json diff --git a/server/core/tests/mock_hep_data_factory.py b/server/core/tests/mock_hep_data_factory.py new file mode 100644 index 00000000..0a02827e --- /dev/null +++ b/server/core/tests/mock_hep_data_factory.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 03.02.20 +# @author: chrigu + +## Mocks +import json +import os +from datetime import datetime, timedelta + + +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + + def json(self): + return {} + + +## Setup json data + +def make_orders_valid(order_items): + for order_item in order_items['items']: + if 'created_at' in order_item: + yesterday = datetime.now() - timedelta(1) + order_item['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') + + return order_items + +# Load data +dir_path = os.path.dirname(os.path.realpath(__file__)) + +with open('{}/mock_data/valid_teacher_orders.json'.format(dir_path), 'r') as file: + valid_teacher_order_data = file.read() + +with open('{}/mock_data/valid_student_orders.json'.format(dir_path), 'r') as file: + valid_student_order_data = file.read() + +with open('{}/mock_data/me_data.json'.format(dir_path), 'r') as file: + me_data = file.read() + +with open('{}/mock_data/email_not_confirmed_me.json'.format(dir_path), 'r') as file: + not_confirmed_email_me_data = file.read() + +ME_DATA = json.loads(me_data) +NOT_CONFIRMED_ME = json.loads(not_confirmed_email_me_data) + +valid_teacher_order_items = json.loads(valid_teacher_order_data) +VALID_TEACHERS_ORDERS = make_orders_valid(valid_teacher_order_items) + +valid_student_order_items = json.loads(valid_student_order_data) +VALID_STUDENT_ORDERS = make_orders_valid(valid_student_order_items) diff --git a/server/registration/mutations_public.py b/server/registration/mutations_public.py index 32e600fb..687812c9 100644 --- a/server/registration/mutations_public.py +++ b/server/registration/mutations_public.py @@ -8,52 +8,54 @@ # Created on 2019-10-08 # @author: chrigu import graphene -from django.conf import settings +from django.contrib.auth import login from graphene import relay from core.hep_client import HepClient, HepClientException, HepClientUnauthorizedException from users.user_signup_login_handler import handle_user_and_verify_products -class PublicFieldError(graphene.ObjectType): - code = graphene.String() - - class RegistrationError(graphene.ObjectType): field = graphene.String() class Registration(relay.ClientIDMutation): class Input: - confirmation_key = graphene.String() + confirmation_key_input = graphene.String() success = graphene.Boolean() + message = graphene.String() errors = graphene.List(RegistrationError) # todo: change for consistency @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - confirmation_key = kwargs.get('confirmation_key') + confirmation_key = kwargs.get('confirmation_key_input') hep_client = HepClient() try: user_data = hep_client.customer_activate(confirmation_key) except HepClientUnauthorizedException: - return cls.return_login_error('invalid_credentials') + return cls.return_registration_msg('invalid_key') except HepClientException: - return cls.return_login_error('unknown_error') + return cls.return_registration_msg('unknown_error') - user, error_msg = handle_user_and_verify_products(user_data) + user, status_msg = handle_user_and_verify_products(user_data) - if error_msg: - return cls.return_login_error(error_msg) + if status_msg: + return cls.return_registration_msg(status_msg) - return cls(success=False, errors=[]) + login(info.context, user) + return cls(success=True, errors=[], message='success') @classmethod - def return_login_error(cls, message): - error = RegistrationError(field=message) - return cls(success=False, errors=[error]) + def return_registration_msg(cls, message): + # even if the user has no valid license treat it like a success + if message == 'unknown_error': + error = RegistrationError(field=message) + return cls(success=False, errors=[error], message='') + + return cls(success=True, errors=[], message=message) class RegistrationMutations: diff --git a/server/registration/tests/test_registration.py b/server/registration/tests/test_registration.py index 77a5be5c..bee2bf9c 100644 --- a/server/registration/tests/test_registration.py +++ b/server/registration/tests/test_registration.py @@ -7,33 +7,26 @@ # # Created on 2019-10-08 # @author: chrigu -from django.core import mail +from unittest.mock import patch + +import requests from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, RequestFactory from graphene.test import Client from api.schema import schema from core.hep_client import HepClient -from registration.factories import LicenseTypeFactory, LicenseFactory +from core.tests.mock_hep_data_factory import MockResponse, ME_DATA, VALID_TEACHERS_ORDERS from registration.models import License -from users.managers import RoleManager -from users.models import Role, User, UserRole, SchoolClass +from users.models import User, Role, SchoolClass class RegistrationTests(TestCase): def setUp(self): - - self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role") - self.student_role = Role.objects.create(key=Role.objects.STUDENT_KEY, name="Student Role") - - self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role) - self.student_license_type = LicenseTypeFactory(for_role=self.student_role) - - self.teacher_license = LicenseFactory(license_type=self.teacher_license_type) - self.student_license = LicenseFactory(license_type=self.student_license_type) - request = RequestFactory().post('/') + Role.objects.create_default_roles() + self.email = 'sepp@skillbox.iterativ.ch' self.first_name = 'Sepp' self.last_name = 'Feuz' @@ -49,6 +42,7 @@ class RegistrationTests(TestCase): mutation Registration($input: RegistrationInput!){ registration(input: $input) { success + message errors { field } @@ -62,72 +56,40 @@ class RegistrationTests(TestCase): } }) - # @patch.object(HepClient, 'customer_me', return_value=ME_DATA) - # def test_user_can_register_with_valid_confirmation_key(self, me_mock): - # - # self.user.hep_id = ME_DATA['id'] - # self.user.save() - # - # now = timezone.now() - # expiry_date = now + timedelta(365) - # LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() - # - # result = self.make_login_mutation(self.user.email, TOKEN) - # - # self.assertTrue(result.get('data').get('login').get('success')) - # self.assertTrue(self.user.is_authenticated) + @patch.object(HepClient, 'customer_activate', return_value=ME_DATA) + @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) + @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) + def test_user_can_register_with_valid_confirmation_key_and_no_license(self, admin_mock, product_mock, customer_mock): - # def _assert_user_registration(self, count, email, role_key): - # users = User.objects.filter(username=self.email) - # self.assertEqual(len(users), count) - # user_roles = UserRole.objects.filter(user__email=email, role__key=role_key) - # self.assertEqual(len(user_roles), count) - # licenses = License.objects.filter(licensee__email=email, license_type__for_role__key=role_key) - # self.assertEqual(len(licenses), count) - # - # def test_user_can_register_as_teacher(self): - # self._assert_user_registration(0, self.email, RoleManager.TEACHER_KEY) - # school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse') - # self.assertEqual(len(school_classes), 0) - # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.teacher_license_type.key) - # self.assertTrue(result.get('data').get('registration').get('success')) - # self._assert_user_registration(1, self.email, RoleManager.TEACHER_KEY) - # school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse') - # self.assertEqual(len(school_classes), 1) - # user = User.objects.get(email=self.email) - # self.assertTrue(school_classes[0].is_user_in_schoolclass(user)) - # self.assertEqual(len(mail.outbox), 1) - # self.assertEqual(mail.outbox[0].subject, 'Myskillbox: E-Mail bestätigen und Passwort setzen') - # - # def test_user_can_register_as_student(self): - # self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) - # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - # self.assertTrue(result.get('data').get('registration').get('success')) - # self._assert_user_registration(1, self.email, RoleManager.STUDENT_KEY) - # - # def test_existing_user_cannot_register(self): - # self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) - # self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') - # - # def test_existing_user_cannot_register_with_uppercase_email(self): - # self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) - # self.make_register_mutation(self.first_name, self.last_name, self.email.upper(), self.student_license_type.key) - # result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) - # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') - # - # def test_user_cannot_register_if_firstname_is_missing(self): - # result = self.make_register_mutation('', self.last_name, self.email, self.teacher_license_type.key) - # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'first_name') - # self.assertFalse(result.get('data').get('registration').get('success')) - # - # def test_user_cannot_register_if_lastname_is_missing(self): - # result = self.make_register_mutation(self.first_name, '', self.email, self.teacher_license_type.key) - # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'last_name') - # self.assertFalse(result.get('data').get('registration').get('success')) - # - # def test_user_cannot_register_if_email_is_missing(self): - # result = self.make_register_mutation(self.first_name, self.last_name, '', self.teacher_license_type.key) - # self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') - # self.assertFalse(result.get('data').get('registration').get('success')) + result = self.make_register_mutation('CONFIRMATION_KEY') + + self.assertTrue(result.get('data').get('registration').get('success')) + self.assertEqual(result.get('data').get('registration').get('message'), 'no_valid_license') + + @patch.object(requests, 'post', return_value=MockResponse(401)) + def test_user_cannot_register_with_invalid_key(self, confirmation_mock): + + result = self.make_register_mutation('CONFIRMATION_KEY') + + self.assertTrue(result.get('data').get('registration').get('success')) + self.assertEqual(result.get('data').get('registration').get('message'), 'invalid_key') + + @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) + @patch.object(HepClient, 'customer_activate', return_value=ME_DATA) + @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) + def test_teacher_can_register_with_remote_license(self, order_mock, me_mock, admin_token_mock): + result = self.make_register_mutation('CONFIRMATION_KEY') + + user = User.objects.get(email=ME_DATA['email']) + + user_role_key = user.user_roles.get(user=user).role.key + self.assertEqual(user_role_key, Role.objects.TEACHER_KEY) + + license = License.objects.get(licensee=user) + self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY) + + school_class = SchoolClass.objects.get(users__in=[user]) + self.assertIsNotNone(school_class) + + self.assertTrue(result.get('data').get('registration').get('success')) + self.assertTrue(user.is_authenticated) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 912178de..5d02c59d 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -21,55 +21,14 @@ from graphene.test import Client from api.schema_public import schema from core.factories import UserFactory from core.hep_client import HepClient +from core.tests.mock_hep_data_factory import MockResponse, ME_DATA, VALID_STUDENT_ORDERS, VALID_TEACHERS_ORDERS, \ + NOT_CONFIRMED_ME from registration.factories import LicenseFactory from registration.models import License -from users.models import Role, MagentoToken, User, SchoolClass +from users.models import Role, User, SchoolClass TOKEN = 'abcd12345!' -## Setup json data - -def make_orders_valid(order_items): - for order_item in order_items['items']: - if 'created_at' in order_item: - yesterday = datetime.now() - timedelta(1) - order_item['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S') - - return order_items - -# Load data - -dir_path = os.path.dirname(os.path.realpath(__file__)) - -with open('{}/test_data/valid_teacher_orders.json'.format(dir_path), 'r') as file: - valid_teacher_order_data = file.read() - -with open('{}/test_data/valid_student_orders.json'.format(dir_path), 'r') as file: - valid_student_order_data = file.read() - -with open('{}/test_data/me_data.json'.format(dir_path), 'r') as file: - me_data = file.read() - -with open('{}/test_data/email_not_confirmed_me.json'.format(dir_path), 'r') as file: - not_confirmed_email_me_data = file.read() - -ME_DATA = json.loads(me_data) -NOT_CONFIRMED_ME = json.loads(not_confirmed_email_me_data) - -valid_teacher_order_items = json.loads(valid_teacher_order_data) -VALID_TEACHERS_ORDERS = make_orders_valid(valid_teacher_order_items) - -valid_student_order_items = json.loads(valid_student_order_data) -VALID_STUDENT_ORDERS = make_orders_valid(valid_student_order_items) - - -## Mocks -class MockResponse: - def __init__(self, status_code): - self.status_code = status_code - - def json(self): - return {} class PasswordResetTests(TestCase): diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py index 8fc1cf1d..12bcc74e 100644 --- a/server/users/user_signup_login_handler.py +++ b/server/users/user_signup_login_handler.py @@ -24,7 +24,7 @@ def handle_user_and_verify_products(user_data): except User.DoesNotExist: user = User.objects.create_user_from_hep(user_data) - # todo check if email has changed + # todo check if email has changed, any impact on our system? try: if not hep_client.is_email_verified(user_data): From 23028c779b736e34e7b166e595c7b3b75f1605cb Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 3 Feb 2020 14:10:43 +0100 Subject: [PATCH 017/106] Refactor login --- server/registration/mutations_public.py | 8 ++++--- server/users/managers.py | 4 ++-- server/users/mutations_public.py | 29 +++++++++++++---------- server/users/tests/test_login.py | 19 ++++++++------- server/users/user_signup_login_handler.py | 14 +++++++---- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/server/registration/mutations_public.py b/server/registration/mutations_public.py index 687812c9..848f348d 100644 --- a/server/registration/mutations_public.py +++ b/server/registration/mutations_public.py @@ -12,7 +12,7 @@ from django.contrib.auth import login from graphene import relay from core.hep_client import HepClient, HepClientException, HepClientUnauthorizedException -from users.user_signup_login_handler import handle_user_and_verify_products +from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR class RegistrationError(graphene.ObjectType): @@ -42,16 +42,18 @@ class Registration(relay.ClientIDMutation): user, status_msg = handle_user_and_verify_products(user_data) + if user: + login(info.context, user) + if status_msg: return cls.return_registration_msg(status_msg) - login(info.context, user) return cls(success=True, errors=[], message='success') @classmethod def return_registration_msg(cls, message): # even if the user has no valid license treat it like a success - if message == 'unknown_error': + if message == UNKNOWN_ERROR: error = RegistrationError(field=message) return cls(success=False, errors=[error], message='') diff --git a/server/users/managers.py b/server/users/managers.py index 5b489747..c0b176d8 100644 --- a/server/users/managers.py +++ b/server/users/managers.py @@ -100,10 +100,10 @@ class UserManager(DjangoUserManager): return user def create_user_from_hep(self, user_data): - user = self.user = self._create_user_with_random_password_no_save( + user = self._create_user_with_random_password_no_save( user_data['firstname'], user_data['lastname'], user_data['email']) user.hep_id = user_data['id'] - user.hep_gruop_id = user_data['group_id'] + user.hep_group_id = user_data['group_id'] user.save() return user diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 811ae089..50c74d0a 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -14,7 +14,7 @@ from django.contrib.auth import authenticate, login from graphene import relay from core.hep_client import HepClient, HepClientUnauthorizedException, HepClientException -from users.user_signup_login_handler import handle_user_and_verify_products +from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR, EMAIL_NOT_VERIFIED class LoginError(graphene.ObjectType): @@ -27,6 +27,7 @@ class Login(relay.ClientIDMutation): password_input = graphene.String() success = graphene.Boolean() + message = graphene.String() errors = graphene.List(LoginError) # todo: change for consistency @classmethod @@ -38,7 +39,7 @@ class Login(relay.ClientIDMutation): password = kwargs.get('password_input') user = authenticate(username=username, password=password) if user is None: - return cls.return_login_error('invalid_credentials') + return cls.return_login_message('invalid_credentials') else: hep_client = HepClient() @@ -47,22 +48,26 @@ class Login(relay.ClientIDMutation): try: user_data = hep_client.customer_me(token) except HepClientUnauthorizedException: - return cls.return_login_error('invalid_credentials') + return cls.return_login_message('invalid_credentials') except HepClientException: - return cls.return_login_error('unknown_error') + return cls.return_login_message(UNKNOWN_ERROR) - user, error_msg = handle_user_and_verify_products(user_data) + user, status_msg = handle_user_and_verify_products(user_data) - if error_msg: - return cls.return_login_error(error_msg) + if user and status_msg != EMAIL_NOT_VERIFIED: + login(info.context, user) - login(info.context, user) - return cls(success=True, errors=[]) + if status_msg: + return cls.return_login_message(status_msg) + + return cls(success=True, errors=[], message='success') @classmethod - def return_login_error(cls, message): - error = LoginError(field=message) - return cls(success=False, errors=[error]) + def return_login_message(cls, message): + if message == EMAIL_NOT_VERIFIED or message == UNKNOWN_ERROR or message == 'invalid_credentials': + error = LoginError(field=message) + return cls(success=False, errors=[error], message='') + return cls(success=True, errors=[], message=message) class UserMutations: diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 5d02c59d..2ee8c069 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -50,6 +50,7 @@ class PasswordResetTests(TestCase): mutation Login($input: LoginInput!){ login(input: $input) { success + message errors { field } @@ -118,7 +119,7 @@ class PasswordResetTests(TestCase): @patch.object(requests, 'post', return_value=MockResponse(401)) def test_user_with_no_login_cannot_login(self, post_mock): - result = self.make_login_mutation(ME_DATA['email'], TOKEN) + result = self.make_login_mutation('some@some.ch', 'some') self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'invalid_credentials') @@ -136,27 +137,29 @@ class PasswordResetTests(TestCase): @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) - def test_user_cannot_login_without_license(self, me_mock, product_mock, admin_token_mock): + def test_user_can_login_without_license(self, me_mock, product_mock, admin_token_mock): result = self.make_login_mutation(self.user.email, TOKEN) - self.assertFalse(result.get('data').get('login').get('success')) - self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + self.assertTrue(result.get('data').get('login').get('success')) + self.assertEqual(result.get('data').get('login').get('message'), 'no_valid_license') + self.assertTrue(self.user.is_authenticated) @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) - def test_user_cannot_login_local_license_invalid(self, product_mock, me_mock, admin_token_mock): + def test_user_can_login_local_license_invalid(self, product_mock, me_mock, admin_token_mock): now = timezone.now() expiry_date = now - timedelta(1) LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() result = self.make_login_mutation(self.user.email, TOKEN) - self.assertFalse(result.get('data').get('login').get('success')) - self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'no_valid_license') + self.assertTrue(result.get('data').get('login').get('success')) + self.assertEqual(result.get('data').get('login').get('message'), 'no_valid_license') + self.assertTrue(self.user.is_authenticated) @patch.object(HepClient, 'customer_me', return_value=NOT_CONFIRMED_ME) - def test_user_cannot_login_with_unconfirmed_email(self, me_mock): + def test_user_can_login_with_unconfirmed_email(self, me_mock): result = self.make_login_mutation(self.user.email, TOKEN) self.assertFalse(result.get('data').get('login').get('success')) diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py index 12bcc74e..44774244 100644 --- a/server/users/user_signup_login_handler.py +++ b/server/users/user_signup_login_handler.py @@ -15,6 +15,10 @@ from registration.models import License from users.models import User, UserRole, Role, SchoolClass +EMAIL_NOT_VERIFIED = 'email_not_verified' +UNKNOWN_ERROR = 'unknown_error' +NO_VALID_LICENSE = 'no_valid_license' + def handle_user_and_verify_products(user_data): hep_client = HepClient() @@ -28,9 +32,9 @@ def handle_user_and_verify_products(user_data): try: if not hep_client.is_email_verified(user_data): - return user, 'email_not_verified' + return user, EMAIL_NOT_VERIFIED except HepClientException: - return user, 'unknown_error' + return user, UNKNOWN_ERROR try: license = License.objects.get(licensee=user) @@ -40,14 +44,14 @@ def handle_user_and_verify_products(user_data): admin_token = AdminData.objects.get_admin_token() product = hep_client.myskillbox_product_for_customer(admin_token, user.hep_id) except HepClientException: - return user, 'unknown_error' + return user, UNKNOWN_ERROR if product: license = License.objects.create_license_for_role(user, product['activated'], product['raw'], product['edition']) # todo handle no license case else: - return user, 'no_valid_license' + return user, NO_VALID_LICENSE UserRole.objects.create_role_for_user(user, license.for_role.key) @@ -55,6 +59,6 @@ def handle_user_and_verify_products(user_data): SchoolClass.create_default_group_for_teacher(user) if not license.is_valid(): - return user, 'no_valid_license' + return user, NO_VALID_LICENSE return user, None From 321163e54257f7c465f089386246231574bdb77c Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 4 Feb 2020 08:15:48 +0100 Subject: [PATCH 018/106] Add coupons, refactor utility functions --- server/core/hep_client.py | 9 ++- server/core/schema/mutations/coupon.py | 54 +++++++++++++ server/core/schema/mutations/main.py | 2 + server/core/tests/test_coupon.py | 95 +++++++++++++++++++++++ server/users/user_signup_login_handler.py | 43 ++++++---- 5 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 server/core/schema/mutations/coupon.py create mode 100644 server/core/tests/test_coupon.py diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 8ae9cc68..533d669e 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -55,6 +55,8 @@ class HepClient: response = requests.get(request_url, headers=headers, data=data) else: response = requests.get(request_url, headers=headers) + elif method == 'put': + response = requests.put(request_url, data=data) # Todo handle 401 and most important network errors if response.status_code == 401: @@ -130,7 +132,12 @@ class HepClient: return response.json() def coupon_redeem(self, coupon, customer_id): - response = self._call('/rest/deutsch/V1/coupon/{}/customer/{}'.format(coupon, customer_id), method='put') + try: + response = self._call('/rest/deutsch/V1/coupon/{}/customer/{}'.format(coupon, customer_id), method='put') + except HepClientException as e: + if e.args[0] == 201: + return None + return response def myskillbox_product_for_customer(self, admin_token, customer_id): diff --git a/server/core/schema/mutations/coupon.py b/server/core/schema/mutations/coupon.py new file mode 100644 index 00000000..8609cacf --- /dev/null +++ b/server/core/schema/mutations/coupon.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 03.02.20 +# @author: chrigu +import graphene +from graphene import relay + +from core.hep_client import HepClient, HepClientException +from users.user_signup_login_handler import check_and_create_licenses, create_role_for_user + + +class CouponError(graphene.ObjectType): + field = graphene.String() + + +class Coupon(relay.ClientIDMutation): + class Input: + coupon_code_input = graphene.String() + + success = graphene.Boolean() + errors = graphene.List(CouponError) # todo: change for consistency + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + coupon_code = kwargs.get('coupon_code_input') + hep_client = HepClient() + + try: + hep_id = info.context.user.hep_id + except AttributeError: + return cls(success=False, errors=[{'field': 'not_authenticated'}]) + + try: + response = hep_client.coupon_redeem(coupon_code, hep_id) + except HepClientException: + return cls(success=False, errors=[{'field': 'unkown_error'}]) + + if not response: + return cls(success=False, errors=[{'field': 'invalid_code'}]) + + license, error_msg = check_and_create_licenses(hep_client, info.context.user) + + # todo fail if no license + if error_msg: + return info.context.user, error_msg + + create_role_for_user(info.context.user, license.for_role.key) + + return cls(success=True, errors=[]) diff --git a/server/core/schema/mutations/main.py b/server/core/schema/mutations/main.py index b87afabe..ea0140c7 100644 --- a/server/core/schema/mutations/main.py +++ b/server/core/schema/mutations/main.py @@ -7,8 +7,10 @@ # # Created on 22.10.18 # @author: chrigu +from core.schema.mutations.coupon import Coupon from core.schema.mutations.logout import Logout class CoreMutations(object): logout = Logout.Field() + coupon = Coupon.Field() diff --git a/server/core/tests/test_coupon.py b/server/core/tests/test_coupon.py new file mode 100644 index 00000000..1bd57fbe --- /dev/null +++ b/server/core/tests/test_coupon.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2020 ITerativ GmbH. All rights reserved. +# +# Created on 03.02.20 +# @author: chrigu +from unittest.mock import patch + +import requests +from django.contrib.sessions.middleware import SessionMiddleware +from django.test import TestCase, RequestFactory +from graphene.test import Client + +from api.schema import schema +from core.factories import UserFactory +from core.hep_client import HepClient +from core.tests.mock_hep_data_factory import MockResponse, ME_DATA, VALID_TEACHERS_ORDERS +from registration.models import License +from users.models import User, Role, SchoolClass + + +class CouponTests(TestCase): + def setUp(self): + Role.objects.create_default_roles() + + self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch', hep_id=3) + Role.objects.create_default_roles() + self.teacher_role = Role.objects.get_default_teacher_role() + + # adding session + request = RequestFactory().post('/') + middleware = SessionMiddleware() + middleware.process_request(request) + request.user = self.user + request.session.save() + self.client = Client(schema=schema, context_value=request) + + def make_coupon_mutation(self, coupon_code, client): + mutation = ''' + mutation Coupon($input: CouponInput!){ + coupon(input: $input) { + success + errors { + field + } + } + } + ''' + + return client.execute(mutation, variables={ + 'input': { + 'couponCodeInput': coupon_code + } + }) + + @patch.object(requests, 'put', return_value=MockResponse(200)) + @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) + @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) + def test_user_has_valid_coupon(self, admin_mock, orders_mock, response_mock): + result = self.make_coupon_mutation('COUPON--1234', self.client) + + user_role_key = self.user.user_roles.get(user=self.user).role.key + self.assertEqual(user_role_key, Role.objects.TEACHER_KEY) + license = License.objects.get(licensee=self.user) + self.assertIsNotNone(license) + + school_class = SchoolClass.objects.get(users__in=[self.user]) + self.assertIsNotNone(school_class) + + self.assertTrue(result.get('data').get('coupon').get('success')) + self.assertTrue(self.user.is_authenticated) + + @patch.object(requests, 'put', return_value=MockResponse(201)) + def test_user_has_invalid_coupon(self, response_mock): + result = self.make_coupon_mutation('COUPON--1234', self.client) + + self.assertFalse(result.get('data').get('coupon').get('success')) + self.assertEqual(result.get('data').get('coupon').get('errors')[0].get('field'), 'invalid_code') + + @patch.object(requests, 'put', return_value=MockResponse(201)) + def test_unauthenticated_user_cannot_redeem(self, response_mock): + + request = RequestFactory().post('/') + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + client = Client(schema=schema, context_value=request) + + result = self.make_coupon_mutation('COUPON--1234', client) + + self.assertFalse(result.get('data').get('coupon').get('success')) + self.assertEqual(result.get('data').get('coupon').get('errors')[0].get('field'), 'not_authenticated') diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py index 44774244..82fdc8b3 100644 --- a/server/users/user_signup_login_handler.py +++ b/server/users/user_signup_login_handler.py @@ -40,25 +40,38 @@ def handle_user_and_verify_products(user_data): license = License.objects.get(licensee=user) # Todo how handle invalid license? Cron Job? How to select correct license? Save all licenses? History? except License.DoesNotExist: - try: - admin_token = AdminData.objects.get_admin_token() - product = hep_client.myskillbox_product_for_customer(admin_token, user.hep_id) - except HepClientException: - return user, UNKNOWN_ERROR + license, error_msg = check_and_create_licenses(hep_client, user) - if product: - license = License.objects.create_license_for_role(user, product['activated'], - product['raw'], product['edition']) - # todo handle no license case - else: - return user, NO_VALID_LICENSE + if error_msg: + return user, error_msg - UserRole.objects.create_role_for_user(user, license.for_role.key) - - if license.for_role.key == Role.objects.TEACHER_KEY: - SchoolClass.create_default_group_for_teacher(user) + create_role_for_user(user, license.for_role.key) if not license.is_valid(): return user, NO_VALID_LICENSE return user, None + + +def check_and_create_licenses(hep_client, user): + try: + admin_token = AdminData.objects.get_admin_token() + product = hep_client.myskillbox_product_for_customer(admin_token, user.hep_id) + except HepClientException: + return None, UNKNOWN_ERROR + + if product: + license = License.objects.create_license_for_role(user, product['activated'], + product['raw'], product['edition']) + # todo handle no license case + else: + return None, NO_VALID_LICENSE + + return license, None + + +def create_role_for_user(user, role_key): + UserRole.objects.create_role_for_user(user, role_key) + + if role_key == Role.objects.TEACHER_KEY: + SchoolClass.create_default_group_for_teacher(user) From ffb330223c9c4ab97dbbcc09a063c79144438201 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 4 Feb 2020 09:42:34 +0100 Subject: [PATCH 019/106] Check minimal date --- server/core/hep_client.py | 42 ++++++++++++++++++++++------ server/core/tests/test_hep_client.py | 39 +++++++++++++++++++++++++- server/registration/models.py | 3 +- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 533d669e..58a1fafd 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -21,6 +21,9 @@ MYSKILLBOX_STUDENT_EDITION_ISBN = "123-4-5678-9012-3" TEACHER_EDITION_DURATION = 365 STUDENT_EDITION_DURATION = 4*365 +TEACHER_KEY = 'teacher' +STUDENT_KEY = 'student' + class HepClientException(Exception): pass @@ -168,10 +171,10 @@ class HepClient: } if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN: - product['edition'] = 'teacher' + product['edition'] = TEACHER_KEY else: - product['edition'] = 'student' + product['edition'] = STUDENT_KEY products.append(product) @@ -188,27 +191,48 @@ class HepClient: if product['status'] != 'complete': return False - if product['edition'] == 'teacher': + if product['edition'] == TEACHER_KEY: expiry_delta = product['activated'] + timedelta(TEACHER_EDITION_DURATION) else: expiry_delta = product['activated'] + timedelta(STUDENT_EDITION_DURATION) - if HepClient.is_product_valid(expiry_delta): + if HepClient.is_product_active(expiry_delta, product['edition']): return True else: return False active_products = list(filter(filter_valid_products, products)) - print(active_products) - # todo can a teacher have multiple licenses? - # clarify with hep if len(active_products) == 0: return None elif len(active_products) == 1: return active_products[0] + else: + return self._select_from_teacher_products(active_products) + def _select_from_teacher_products(self, active_products): + teacher_edition = None + + # select first teacher product, as they are all valid it does not matter which one + for product in active_products: + if product['edition'] == TEACHER_KEY: + teacher_edition = product + break + + # select a student product, as they are all valid it does not matter which one + if not teacher_edition: + return active_products[0] + + return teacher_edition @staticmethod - def is_product_valid(expiry_date): - return expiry_date >= datetime.now() + def is_product_active(expiry_date, edition): + + if edition == TEACHER_KEY: + duration = TEACHER_EDITION_DURATION + else: + duration = STUDENT_EDITION_DURATION + + now = datetime.now() + + return expiry_date >= now >= expiry_date - timedelta(days=duration) diff --git a/server/core/tests/test_hep_client.py b/server/core/tests/test_hep_client.py index 06f482ad..d6ec3000 100644 --- a/server/core/tests/test_hep_client.py +++ b/server/core/tests/test_hep_client.py @@ -88,7 +88,7 @@ class HepClientTestCases(TestCase): relevant_product = self.hep_client._get_relevant_product(products) self.assertEqual(relevant_product['raw']['id'], 0) - def test_has_multiple_valid_products(self): + def test_has_multiple_valid_teacher_products_but_only_one_active(self): products = [ { 'edition': 'teacher', @@ -118,3 +118,40 @@ class HepClientTestCases(TestCase): relevant_product = self.hep_client._get_relevant_product(products) self.assertEqual(relevant_product['raw']['id'], 0) + + def test_has_valid_student_and_teacher_edition(self): + products = [ + { + 'edition': 'student', + 'raw': { + 'id': 0 + }, + 'activated': self.now - timedelta(7), + 'status': 'complete' + }, + { + 'edition': 'teacher', + 'raw': { + 'id': 1 + }, + 'activated': self.now - timedelta(7), + 'status': 'complete' + } + ] + + relevant_product = self.hep_client._select_from_teacher_products(products) + self.assertEqual(relevant_product['raw']['id'], 1) + + def test_product_is_active(self): + + expiry_date = self.now + timedelta(3) + + is_active = HepClient.is_product_active(expiry_date, 'teacher') + self.assertTrue(is_active) + + def test_product_is_not_active(self): + + expiry_date = self.now - timedelta(3 * TEACHER_EDITION_DURATION) + + is_active = HepClient.is_product_active(expiry_date, 'teacher') + self.assertFalse(is_active) diff --git a/server/registration/models.py b/server/registration/models.py index 2c79542c..0e9e79c1 100644 --- a/server/registration/models.py +++ b/server/registration/models.py @@ -29,7 +29,8 @@ class License(models.Model): return self.for_role.key == RoleManager.TEACHER_KEY def is_valid(self): - return HepClient.is_product_valid(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day)) + return HepClient.is_product_active(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day), + self.for_role.key) def __str__(self): return 'License for role: %s' % self.for_role From 75e1597d6f22a91575c976ea015d8b0cc577d6e3 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 4 Feb 2020 13:15:14 +0100 Subject: [PATCH 020/106] Update email if user changed on hep --- server/users/mutations_public.py | 6 +++ server/users/tests/test_login.py | 52 +++++++++++++++++------ server/users/user_signup_login_handler.py | 1 + 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index 50c74d0a..4dc03bb9 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -54,6 +54,12 @@ class Login(relay.ClientIDMutation): user, status_msg = handle_user_and_verify_products(user_data) + # sync email if user changed it on hep account + if user.email != user_data['email']: + user.email = user_data['email'] + user.username = user_data['email'] + user.save() + if user and status_msg != EMAIL_NOT_VERIFIED: login(info.context, user) diff --git a/server/users/tests/test_login.py b/server/users/tests/test_login.py index 2ee8c069..c6317916 100644 --- a/server/users/tests/test_login.py +++ b/server/users/tests/test_login.py @@ -30,10 +30,9 @@ from users.models import Role, User, SchoolClass TOKEN = 'abcd12345!' - class PasswordResetTests(TestCase): def setUp(self): - self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch') + self.user = UserFactory(username=ME_DATA['id'], email=ME_DATA['id']) Role.objects.create_default_roles() self.teacher_role = Role.objects.get_default_teacher_role() @@ -80,6 +79,44 @@ class PasswordResetTests(TestCase): self.assertTrue(result.get('data').get('login').get('success')) self.assertTrue(self.user.is_authenticated) + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_can_login_with_updated_email(self, me_mock): + + old_mail = 'aschi@iterativ.ch' + + self.user.hep_id = ME_DATA['id'] + self.user.email = old_mail + self.user.username = old_mail + self.user.save() + + now = timezone.now() + expiry_date = now + timedelta(365) + LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() + + result = self.make_login_mutation(self.user.email, TOKEN) + + user = User.objects.get(hep_id=self.user.hep_id) + + self.assertEqual(user.username, ME_DATA['email']) + self.assertEqual(user.email, ME_DATA['email']) + self.assertTrue(result.get('data').get('login').get('success')) + self.assertTrue(self.user.is_authenticated) + + @patch.object(HepClient, 'customer_me', return_value=ME_DATA) + def test_user_can_login_with_local_user_and_valid_local_license(self, me_mock): + + self.user.hep_id = ME_DATA['id'] + self.user.save() + + now = timezone.now() + expiry_date = now + timedelta(365) + LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save() + + result = self.make_login_mutation(self.user.email, TOKEN) + + self.assertTrue(result.get('data').get('login').get('success')) + self.assertTrue(self.user.is_authenticated) + @patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS) @patch.object(HepClient, 'customer_me', return_value=ME_DATA) @patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'}) @@ -171,14 +208,3 @@ class PasswordResetTests(TestCase): self.assertFalse(result.get('data').get('login').get('success')) self.assertEqual(result.get('data').get('login').get('errors')[0].get('field'), 'unknown_error') - - ## can login with license and user - ## can login with no user and license - # ?can login with no user and local license - ## cannot login without user - ## cannot login with user and not verfied - ## cannot login with user and no license - ## cannot login with user and expired license - ## non 200 error - # if more than one valid license take correct - # if mulitple licenses and one correct take one test in own class diff --git a/server/users/user_signup_login_handler.py b/server/users/user_signup_login_handler.py index 82fdc8b3..6408d07a 100644 --- a/server/users/user_signup_login_handler.py +++ b/server/users/user_signup_login_handler.py @@ -19,6 +19,7 @@ EMAIL_NOT_VERIFIED = 'email_not_verified' UNKNOWN_ERROR = 'unknown_error' NO_VALID_LICENSE = 'no_valid_license' + def handle_user_and_verify_products(user_data): hep_client = HepClient() From 68bba26c75c016b508a6bf3590c8e4446f4900e9 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 4 Feb 2020 14:21:45 +0100 Subject: [PATCH 021/106] Add stage confirmation view --- server/api/urls.py | 3 ++- server/core/hep_client.py | 12 ++++++------ server/core/settings.py | 1 + server/core/templates/confirmation_key.html | 10 ++++++++++ server/core/views.py | 21 +++++++++++++++++++++ 5 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 server/core/templates/confirmation_key.html diff --git a/server/api/urls.py b/server/api/urls.py index 69beea15..51835b5f 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -5,7 +5,7 @@ from graphene_django.views import GraphQLView from api.schema_public import schema -from core.views import PrivateGraphQLView +from core.views import PrivateGraphQLView, ConfirmationKeyView app_name = 'api' urlpatterns = [ @@ -17,5 +17,6 @@ if settings.DEBUG: urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True, pretty=True)))] urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))] + urlpatterns += [url(r'^confirmation', ConfirmationKeyDisplayView.as_view(), name='confirmation_key_display')] diff --git a/server/core/hep_client.py b/server/core/hep_client.py index 58a1fafd..6779b193 100644 --- a/server/core/hep_client.py +++ b/server/core/hep_client.py @@ -34,8 +34,7 @@ class HepClientUnauthorizedException(Exception): class HepClient: - URL = 'https://stage.hep-verlag.ch' - # URL = 'https://www.hep-verlag.ch' + URL = settings.HEP_URL WEBSITE_ID = 1 HEADERS = { 'accept': 'application/json', @@ -118,13 +117,14 @@ class HepClient: }) return response.json() - def customers_search(self, email): + def customers_search(self, admin_token, email): response = self._call("/rest/V1/customers/search?searchCriteria[filterGroups][0][filters][0][field]" - "=email&searchCriteria[filterGroups][0][filters][0][value]={}".format(email)) + "=email&searchCriteria[filterGroups][0][filters][0][value]={}".format(email), method='get', + additional_headers={'authorization': 'Bearer {}'.format(admin_token)}) json_data = response.json() - if len(json_data) > 0: - return json_data[0] + if len(json_data['items']) > 0: + return json_data['items'][0] return None def _customer_orders(self, admin_token, customer_id): diff --git a/server/core/settings.py b/server/core/settings.py index 86f66bb7..204cf128 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -374,5 +374,6 @@ USE_LOCAL_REGISTRATION = False # HEP HEP_ADMIN_USER = "adminuser" HEP_ADMIN_PASSWORD = "password" +HEP_URL = 'https://stage.hep-verlag.ch' diff --git a/server/core/templates/confirmation_key.html b/server/core/templates/confirmation_key.html new file mode 100644 index 00000000..876f9568 --- /dev/null +++ b/server/core/templates/confirmation_key.html @@ -0,0 +1,10 @@ + + + + + Confirmation + + + Email bestätitgen + + diff --git a/server/core/views.py b/server/core/views.py index b495cfc1..8ccd7dd2 100644 --- a/server/core/views.py +++ b/server/core/views.py @@ -7,9 +7,13 @@ from django.http.response import HttpResponse from django.shortcuts import render from django.urls import reverse_lazy from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.generic import TemplateView from graphene_django.views import GraphQLView from django.utils.translation import gettext_lazy as _ +from core.hep_client import HepClient +from core.models import AdminData + class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): pass @@ -73,3 +77,20 @@ class LegacySetPasswordConfirmView(PasswordResetConfirmView): class LegacySetPasswordCompleteView(PasswordResetCompleteView): template_name = 'registration/set_password_complete.html' title = _('Passwort setzen erfolgreich') + + +class ConfirmationKeyDisplayView(TemplateView): + + template_name = 'confirmation_key.html' + + def get_context_data(self, request, *args, **kwargs): + + email = request.GET.get('email', '') + + hep_client = HepClient() + admin_token = AdminData.objects.get_admin_token() + hep_user = hep_client.customers_search(admin_token, email) + + context = super().get_context_data(**kwargs) + context['confirmation_key'] = hep_user['confirmation'] + return context From 057e65a82ff792e1fdc1d0b5bb359c6052ff99a4 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 4 Feb 2020 14:32:38 +0100 Subject: [PATCH 022/106] Fix urls, reset new migrations --- server/api/urls.py | 2 +- ...127_0932.py => 0003_auto_20200204_1331.py} | 4 ++-- ...group_id.py => 0010_auto_20200204_1331.py} | 9 +++++-- server/users/migrations/0010_magentotoken.py | 24 ------------------- server/users/migrations/0011_user_hep_id.py | 18 -------------- server/users/models.py | 6 ----- 6 files changed, 10 insertions(+), 53 deletions(-) rename server/registration/migrations/{0003_auto_20200127_0932.py => 0003_auto_20200204_1331.py} (91%) rename server/users/migrations/{0012_user_hep_group_id.py => 0010_auto_20200204_1331.py} (54%) delete mode 100644 server/users/migrations/0010_magentotoken.py delete mode 100644 server/users/migrations/0011_user_hep_id.py diff --git a/server/api/urls.py b/server/api/urls.py index 51835b5f..8667c1fe 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -5,7 +5,7 @@ from graphene_django.views import GraphQLView from api.schema_public import schema -from core.views import PrivateGraphQLView, ConfirmationKeyView +from core.views import PrivateGraphQLView, ConfirmationKeyDisplayView app_name = 'api' urlpatterns = [ diff --git a/server/registration/migrations/0003_auto_20200127_0932.py b/server/registration/migrations/0003_auto_20200204_1331.py similarity index 91% rename from server/registration/migrations/0003_auto_20200127_0932.py rename to server/registration/migrations/0003_auto_20200204_1331.py index 521374f9..f18bb95c 100644 --- a/server/registration/migrations/0003_auto_20200127_0932.py +++ b/server/registration/migrations/0003_auto_20200204_1331.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.6 on 2020-01-27 09:32 +# Generated by Django 2.0.6 on 2020-02-04 13:31 from django.db import migrations, models import django.db.models.deletion @@ -7,7 +7,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('users', '0010_magentotoken'), + ('users', '0010_auto_20200204_1331'), ('registration', '0002_auto_20191010_0905'), ] diff --git a/server/users/migrations/0012_user_hep_group_id.py b/server/users/migrations/0010_auto_20200204_1331.py similarity index 54% rename from server/users/migrations/0012_user_hep_group_id.py rename to server/users/migrations/0010_auto_20200204_1331.py index b2e624d7..9d19221b 100644 --- a/server/users/migrations/0012_user_hep_group_id.py +++ b/server/users/migrations/0010_auto_20200204_1331.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.6 on 2020-01-30 05:50 +# Generated by Django 2.0.6 on 2020-02-04 13:31 from django.db import migrations, models @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('users', '0011_user_hep_id'), + ('users', '0009_auto_20191009_0905'), ] operations = [ @@ -15,4 +15,9 @@ class Migration(migrations.Migration): name='hep_group_id', field=models.PositiveIntegerField(null=True), ), + migrations.AddField( + model_name='user', + name='hep_id', + field=models.PositiveIntegerField(null=True), + ), ] diff --git a/server/users/migrations/0010_magentotoken.py b/server/users/migrations/0010_magentotoken.py deleted file mode 100644 index 32fd768f..00000000 --- a/server/users/migrations/0010_magentotoken.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.6 on 2020-01-27 09:32 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0009_auto_20191009_0905'), - ] - - operations = [ - migrations.CreateModel( - name='MagentoToken', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('token', models.CharField(blank=True, max_length=64, null=True)), - ('created_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_token', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/server/users/migrations/0011_user_hep_id.py b/server/users/migrations/0011_user_hep_id.py deleted file mode 100644 index e1b4ef4d..00000000 --- a/server/users/migrations/0011_user_hep_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.6 on 2020-01-27 13:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0010_magentotoken'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='hep_id', - field=models.PositiveIntegerField(null=True), - ), - ] diff --git a/server/users/models.py b/server/users/models.py index c70bc8c8..f071bed6 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -173,9 +173,3 @@ class UserRole(models.Model): class UserSetting(models.Model): user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting') selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE) - - -class MagentoToken(models.Model): - token = models.CharField(blank=True, null=True, max_length=64) - created_at = models.DateTimeField(auto_now=True) - user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE, related_name='user_token') From 1d50287dbf9cd3bbfd6624f5502dca4070f6ee9e Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 5 Feb 2020 14:45:10 +0100 Subject: [PATCH 023/106] Add basic registration flow --- client/src/hep-client/index.js | 11 ++ client/src/main.js | 4 +- client/src/pages/check-email.vue | 23 ++++ client/src/pages/registration.vue | 185 +++++++++++++++++++++--------- client/src/router/index.js | 10 ++ 5 files changed, 179 insertions(+), 54 deletions(-) create mode 100644 client/src/hep-client/index.js create mode 100644 client/src/pages/check-email.vue diff --git a/client/src/hep-client/index.js b/client/src/hep-client/index.js new file mode 100644 index 00000000..56f4a4eb --- /dev/null +++ b/client/src/hep-client/index.js @@ -0,0 +1,11 @@ +import * as axios from 'axios' + +const hepBaseUrl = 'https://stage.hep-verlag.ch'; + +export function register(registrationData) { + return axios.post(`${hepBaseUrl}/rest/deutsch/V1/customers`, registrationData); +} + +export function login(username, password) { + return axios.post(`${hepBaseUrl}/rest/deutsch/V1/customers`, {username, password}); +} diff --git a/client/src/main.js b/client/src/main.js index 63336b3e..89f42ff4 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -11,7 +11,7 @@ import store from '@/store/index' import VueScrollTo from 'vue-scrollto'; import VueAnalytics from 'vue-analytics'; import {Validator, install as VeeValidate} from 'vee-validate/dist/vee-validate.minimal.esm.js'; -import {required, min} from 'vee-validate/dist/rules.esm.js'; +import {required, min, decimal} from 'vee-validate/dist/rules.esm.js'; import veeDe from 'vee-validate/dist/locale/de'; import {dateFilter} from './filters/date-filter'; import autoGrow from '@/directives/auto-grow' @@ -76,6 +76,7 @@ const apolloProvider = new VueApollo({ Validator.extend('required', required); Validator.extend('min', min); +Validator.extend('decimal', decimal); const dict = { custom: { @@ -91,6 +92,7 @@ const dict = { Validator.localize('de', veeDe) Validator.localize('de', dict) + // https://github.com/baianat/vee-validate/issues/51 Validator.extend('strongPassword', { getMessage: field => 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten', diff --git a/client/src/pages/check-email.vue b/client/src/pages/check-email.vue new file mode 100644 index 00000000..d983c3ec --- /dev/null +++ b/client/src/pages/check-email.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/client/src/pages/registration.vue b/client/src/pages/registration.vue index 7ff3d1b8..08f71c22 100644 --- a/client/src/pages/registration.vue +++ b/client/src/pages/registration.vue @@ -2,6 +2,23 @@

Registrieren Sie ihr persönliches Konto.

+
+ + +
{{ error }}
- + {{ errors.first('licenseKey') }} + data-cy="address-local-errors" + >{{ errors.first('address') }} {{ error }} +
+
+ + + {{ errors.first('zipCode') }} + {{ error }} +
+
+ + + {{ errors.first('city') }} + {{ error }}
@@ -122,7 +191,8 @@ + + diff --git a/client/src/pages/login-local.vue b/client/src/pages/login-local.vue new file mode 100644 index 00000000..94155c82 --- /dev/null +++ b/client/src/pages/login-local.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/client/src/router/index.js b/client/src/router/index.js index 3221518a..d5ea5bc6 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -28,6 +28,8 @@ import surveyPage from '@/pages/survey' import styleGuidePage from '@/pages/styleguide' import moduleRoom from '@/pages/moduleRoom' import login from '@/pages/login' +import loginLocal from '@/pages/login-local' +import hello from '@/pages/hello' import registration from '@/pages/registration' import waitForClass from '@/pages/waitForClass' import checkEmail from '@/pages/check-email' @@ -50,6 +52,24 @@ const routes = [ public: true } }, + { + path: '/hello', + name: 'hello', + component: hello, + meta: { + layout: 'public', + public: true + } + }, + { + path: '/login-local', + name: 'loginLocal', + component: loginLocal, + meta: { + layout: 'public', + public: true + } + }, { path: '/module/:slug', component: moduleBase, diff --git a/server/core/migrations/0001_initial.py b/server/core/migrations/0001_initial.py new file mode 100644 index 00000000..6be51120 --- /dev/null +++ b/server/core/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.6 on 2020-02-05 13:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AdminData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hep_admin_token', models.CharField(max_length=100)), + ('updated_at', models.DateTimeField(auto_now=True, null=True)), + ], + ), + ] diff --git a/server/core/migrations/__init__.py b/server/core/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/core/views.py b/server/core/views.py index 8ccd7dd2..f8a8bba8 100644 --- a/server/core/views.py +++ b/server/core/views.py @@ -83,14 +83,14 @@ class ConfirmationKeyDisplayView(TemplateView): template_name = 'confirmation_key.html' - def get_context_data(self, request, *args, **kwargs): + def get_context_data(self, *args, **kwargs): - email = request.GET.get('email', '') + email = self.request.GET.get('email', '') hep_client = HepClient() admin_token = AdminData.objects.get_admin_token() hep_user = hep_client.customers_search(admin_token, email) - context = super().get_context_data(**kwargs) + context = super().get_context_data(self.request, **kwargs) context['confirmation_key'] = hep_user['confirmation'] return context From a0eeec4e187a5aca75cac8e7b1fe76b303883a02 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 6 Feb 2020 15:14:35 +0100 Subject: [PATCH 025/106] Add extra fields to registration, remove unused fields --- .../gql/local/mutations/helloEmail.gql | 3 + client/src/graphql/resolvers.js | 4 +- client/src/main.js | 3 +- client/src/pages/hello.vue | 4 +- client/src/pages/login.vue | 129 ++++++++------- client/src/pages/registration.vue | 147 +++++++----------- 6 files changed, 128 insertions(+), 162 deletions(-) create mode 100644 client/src/graphql/gql/local/mutations/helloEmail.gql diff --git a/client/src/graphql/gql/local/mutations/helloEmail.gql b/client/src/graphql/gql/local/mutations/helloEmail.gql new file mode 100644 index 00000000..b171cb1b --- /dev/null +++ b/client/src/graphql/gql/local/mutations/helloEmail.gql @@ -0,0 +1,3 @@ +mutation($helloEmail: String!) { + helloEmail(email: $helloEmail) @client +} diff --git a/client/src/graphql/resolvers.js b/client/src/graphql/resolvers.js index ae68098a..27a60133 100644 --- a/client/src/graphql/resolvers.js +++ b/client/src/graphql/resolvers.js @@ -9,9 +9,9 @@ export const resolvers = { cache.writeQuery({query: SCROLL_POSITION, data}); return data.scrollPosition; }, - helloEmail: (_, {helloEmail}, {cache}) => { + helloEmail: (_, {email}, {cache}) => { const data = cache.readQuery({query: HELLO_EMAIL}); - data.helloEmail.helloEmail = helloEmail; + data.helloEmail.email = email; cache.writeQuery({query: HELLO_EMAIL, data}); return data.helloEmail; }, diff --git a/client/src/main.js b/client/src/main.js index 89f42ff4..8cc587f5 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -11,7 +11,7 @@ import store from '@/store/index' import VueScrollTo from 'vue-scrollto'; import VueAnalytics from 'vue-analytics'; import {Validator, install as VeeValidate} from 'vee-validate/dist/vee-validate.minimal.esm.js'; -import {required, min, decimal} from 'vee-validate/dist/rules.esm.js'; +import {required, min, decimal, confirmed} from 'vee-validate/dist/rules.esm.js'; import veeDe from 'vee-validate/dist/locale/de'; import {dateFilter} from './filters/date-filter'; import autoGrow from '@/directives/auto-grow' @@ -77,6 +77,7 @@ const apolloProvider = new VueApollo({ Validator.extend('required', required); Validator.extend('min', min); Validator.extend('decimal', decimal); +Validator.extend('confirmed', confirmed); const dict = { custom: { diff --git a/client/src/pages/hello.vue b/client/src/pages/hello.vue index 29fdabe3..3af0ac67 100644 --- a/client/src/pages/hello.vue +++ b/client/src/pages/hello.vue @@ -39,7 +39,7 @@ diff --git a/client/src/pages/registration.vue b/client/src/pages/registration.vue index 08f71c22..6ce40fb8 100644 --- a/client/src/pages/registration.vue +++ b/client/src/pages/registration.vue @@ -1,8 +1,12 @@ @@ -193,6 +167,7 @@ From b36ba5f9a1e2d04c06ac44f4f57726a3cd9d343e Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 10 Feb 2020 10:59:09 +0100 Subject: [PATCH 026/106] Add email verifcation page --- .../graphql/gql/mutations/registration.gql | 3 +- client/src/pages/activate-license.vue | 0 client/src/pages/email-verification.vue | 56 +++++++++++++++++++ client/src/pages/hello.vue | 2 +- client/src/pages/login.vue | 2 +- client/src/router/index.js | 10 ++++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/activate-license.vue create mode 100644 client/src/pages/email-verification.vue diff --git a/client/src/graphql/gql/mutations/registration.gql b/client/src/graphql/gql/mutations/registration.gql index 4a5f8367..74ac38f2 100644 --- a/client/src/graphql/gql/mutations/registration.gql +++ b/client/src/graphql/gql/mutations/registration.gql @@ -1,6 +1,7 @@ -mutation Registration($input: RegistrationInput!){ +mutation Registration($input: RegistrationInput!) { registration(input: $input) { success + message errors { field } diff --git a/client/src/pages/activate-license.vue b/client/src/pages/activate-license.vue new file mode 100644 index 00000000..e69de29b diff --git a/client/src/pages/email-verification.vue b/client/src/pages/email-verification.vue new file mode 100644 index 00000000..df04d556 --- /dev/null +++ b/client/src/pages/email-verification.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/client/src/pages/hello.vue b/client/src/pages/hello.vue index 3af0ac67..26ef342b 100644 --- a/client/src/pages/hello.vue +++ b/client/src/pages/hello.vue @@ -66,7 +66,7 @@ export default { this.$router.push({name: redirectRouteName}); }) .catch(() => { - this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'; + this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals.'; }); } }); diff --git a/client/src/pages/login.vue b/client/src/pages/login.vue index f12f696d..f09df22c 100644 --- a/client/src/pages/login.vue +++ b/client/src/pages/login.vue @@ -64,7 +64,7 @@ export default { }) .catch((error) => { console.log(error) - this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'; + this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals.'; }); // this.$apollo.mutate({ diff --git a/client/src/router/index.js b/client/src/router/index.js index d5ea5bc6..235db9c4 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -33,6 +33,7 @@ import hello from '@/pages/hello' import registration from '@/pages/registration' import waitForClass from '@/pages/waitForClass' import checkEmail from '@/pages/check-email' +import emailVerification from '@/pages/email-verification' import store from '@/store/index'; @@ -164,6 +165,15 @@ const routes = [ layout: 'public' } }, + { + path: '/verify-email', + component: emailVerification, + name: 'emailVerification', + meta: { + public: true, + layout: 'public' + } + }, {path: '/styleguide', component: styleGuidePage}, {path: '*', component: p404} ]; From 0922c5d1983763f7493ce7134c56872e9b71287d Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 11 Feb 2020 14:36:48 +0100 Subject: [PATCH 027/106] Fix local signup, style header --- client/package-lock.json | 52 +++++--- client/src/pages/activate-license.vue | 0 client/src/pages/hello.vue | 3 +- client/src/pages/license-activation.vue | 126 ++++++++++++++++++ client/src/pages/login.vue | 4 +- client/src/pages/registration.vue | 4 +- client/src/router/index.js | 9 ++ client/src/styles/_info_header.scss | 13 ++ client/src/styles/_password_forms.scss | 1 + client/src/styles/main.scss | 2 + client/vue.config.js | 2 +- server/core/settings.py | 3 +- .../commands/create_dummy_license.py | 12 +- server/users/mutations_public.py | 1 + 14 files changed, 201 insertions(+), 31 deletions(-) delete mode 100644 client/src/pages/activate-license.vue create mode 100644 client/src/pages/license-activation.vue create mode 100644 client/src/styles/_info_header.scss diff --git a/client/package-lock.json b/client/package-lock.json index 69308bb2..b27a9409 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2460,7 +2460,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -2492,7 +2492,7 @@ }, "onetime": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, @@ -11540,7 +11540,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -11582,7 +11583,8 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", @@ -11593,7 +11595,8 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -11710,7 +11713,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -11722,6 +11726,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -11736,6 +11741,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -11743,12 +11749,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -11767,6 +11775,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -11847,7 +11856,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -11859,6 +11869,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -11944,7 +11955,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -11980,6 +11992,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11999,6 +12012,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -12042,12 +12056,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -13198,7 +13214,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -13245,13 +13261,13 @@ }, "onetime": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, "ora": { "version": "0.2.3", - "resolved": "http://registry.npmjs.org/ora/-/ora-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", "dev": true, "requires": { @@ -13309,7 +13325,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -13373,7 +13389,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -13405,7 +13421,7 @@ }, "onetime": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, @@ -13675,7 +13691,7 @@ }, "onetime": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, diff --git a/client/src/pages/activate-license.vue b/client/src/pages/activate-license.vue deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/pages/hello.vue b/client/src/pages/hello.vue index 26ef342b..899573b3 100644 --- a/client/src/pages/hello.vue +++ b/client/src/pages/hello.vue @@ -1,6 +1,6 @@ + + + + diff --git a/client/src/pages/login.vue b/client/src/pages/login.vue index f09df22c..8564b45f 100644 --- a/client/src/pages/login.vue +++ b/client/src/pages/login.vue @@ -1,7 +1,7 @@