From dcbab36ef0c5d13cfe699946f168c150803fb3e0 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 7 Sep 2020 12:14:34 -0700 Subject: [PATCH 01/32] Added osprojects to admin interface. --- project/osprojects/admin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/project/osprojects/admin.py b/project/osprojects/admin.py index 8c38f3f3..4f391a5c 100644 --- a/project/osprojects/admin.py +++ b/project/osprojects/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin +from .models import OSProjects + # Register your models here. +class OSProjectAdmin(admin.ModelAdmin): + + list_display = ['tag_list'] + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('tags') + + def tag_list(self, obj): + return u", ".join(o.name for o in obj.tags.all()) + + +admin.site.register(OSProjects) From 31edf0fb1a1a1bc731042500608554693651e771 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 7 Sep 2020 13:24:44 -0700 Subject: [PATCH 02/32] Added osprojects to admin interface. --- project/osprojects/admin.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/project/osprojects/admin.py b/project/osprojects/admin.py index 4f391a5c..8c38f3f3 100644 --- a/project/osprojects/admin.py +++ b/project/osprojects/admin.py @@ -1,17 +1,3 @@ from django.contrib import admin -from .models import OSProjects - # Register your models here. -class OSProjectAdmin(admin.ModelAdmin): - - list_display = ['tag_list'] - - def get_queryset(self, request): - return super().get_queryset(request).prefetch_related('tags') - - def tag_list(self, obj): - return u", ".join(o.name for o in obj.tags.all()) - - -admin.site.register(OSProjects) From d585cddb58feaf3ced429a04815a67f0fc93a585 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 7 Sep 2020 13:33:24 -0700 Subject: [PATCH 03/32] Added OSProjects to admin interface. --- project/osprojects/admin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/project/osprojects/admin.py b/project/osprojects/admin.py index 8c38f3f3..4f391a5c 100644 --- a/project/osprojects/admin.py +++ b/project/osprojects/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin +from .models import OSProjects + # Register your models here. +class OSProjectAdmin(admin.ModelAdmin): + + list_display = ['tag_list'] + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('tags') + + def tag_list(self, obj): + return u", ".join(o.name for o in obj.tags.all()) + + +admin.site.register(OSProjects) From 606c871b96d3806a907cb0a5ebfa14a7fe1237f6 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 7 Sep 2020 18:22:23 -0700 Subject: [PATCH 04/32] added django-res-auth and associated basic settings to test DRF and allauth together. --- project/config/settings/base.py | 6 +++++- project/config/urls.py | 2 ++ project/requirements/base.txt | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/project/config/settings/base.py b/project/config/settings/base.py index 387b145a..c645303c 100644 --- a/project/config/settings/base.py +++ b/project/config/settings/base.py @@ -77,10 +77,13 @@ "allauth.account", "allauth.socialaccount", "rest_framework", + "rest_framework.authtoken", "corsheaders", "taggit", "django_celery_beat", - "taggit_serializer" + "taggit_serializer", + "rest_auth", + "rest_auth.registration" ] LOCAL_APPS = [ @@ -327,6 +330,7 @@ 'PAGE_SIZE': 10, } +REST_USE_JWT = True JWT_AUTH = { 'JWT_AUTH_HEADER_PREFIX': 'Bearer', diff --git a/project/config/urls.py b/project/config/urls.py index a757dc3c..4391a52d 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -21,6 +21,8 @@ # Your stuff: custom urls includes go here path('api/v1/', include('resources.urls')), path('auth/', include('userauth.urls', namespace="userauth")), + path('rest-auth/', include('rest_auth.urls')), + path('rest-auth/registration/', include('rest_auth.registration.urls')) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/project/requirements/base.txt b/project/requirements/base.txt index bf8c4ab6..3d62143e 100644 --- a/project/requirements/base.txt +++ b/project/requirements/base.txt @@ -33,3 +33,5 @@ djangorestframework==3.10.2 # https://github.com/encode/django-rest-framework coreapi==2.3.3 # https://github.com/core-api/python-client django_taggit_serializer==0.1.7 #https://github.com/glemmaPaul/django-taggit-serializer drf-jwt==1.13.4 # https://github.com/Styria-Digital/django-rest-framework-jwt +django-rest-auth==0.9.5 #https://github.com/Tivix/django-rest-auth +django-rest-authtoken==2.1.3 #https://pypi.org/project/django-rest-authtoken/ From 30936fe475f384a6ddf0204027ccce7e15fcb844 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 14 Sep 2020 05:49:44 -0700 Subject: [PATCH 05/32] Working state Mon Sept 14 - dj-rest-auth and django-allauth --- project/config/settings/base.py | 61 +++++++++++-------- project/config/urls.py | 22 ++++--- .../email/email _confirmation_subject.txt | 1 + .../email/email_confirmation_message.txt | 9 +++ project/requirements/base.txt | 2 + project/resources/serializers.py | 9 +-- project/resources/urls.py | 4 +- project/userauth/adapter.py | 10 +++ project/userauth/serializers.py | 39 +++++++++++- project/userauth/urls.py | 41 +++++++++---- project/userauth/views.py | 38 +++++++++++- project/users/urls.py | 3 +- 12 files changed, 182 insertions(+), 57 deletions(-) create mode 100644 project/core/templates/account/email/email _confirmation_subject.txt create mode 100644 project/core/templates/account/email/email_confirmation_message.txt create mode 100644 project/userauth/adapter.py diff --git a/project/config/settings/base.py b/project/config/settings/base.py index c645303c..6105773a 100644 --- a/project/config/settings/base.py +++ b/project/config/settings/base.py @@ -82,8 +82,8 @@ "taggit", "django_celery_beat", "taggit_serializer", - "rest_auth", - "rest_auth.registration" + "dj_rest_auth", + "dj_rest_auth.registration", ] LOCAL_APPS = [ @@ -290,21 +290,43 @@ # CELERY_TASK_SOFT_TIME_LIMIT = 60 # # http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler # CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" -# # django-allauth -# # ------------------------------------------------------------------------------ -# ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) -# # https://django-allauth.readthedocs.io/en/latest/configuration.html -# ACCOUNT_AUTHENTICATION_METHOD = "username" -# # https://django-allauth.readthedocs.io/en/latest/configuration.html -# ACCOUNT_EMAIL_REQUIRED = True -# # https://django-allauth.readthedocs.io/en/latest/configuration.html -# ACCOUNT_EMAIL_VERIFICATION = "mandatory" -# # https://django-allauth.readthedocs.io/en/latest/configuration.html -# ACCOUNT_ADAPTER = "users.adapters.AccountAdapter" + + +# # django-allauth config # # https://django-allauth.readthedocs.io/en/latest/configuration.html +# # ------------------------------------------------------------------------------ +ACCOUNT_ADAPTER = "userauth.adapter.CustomAccountAdapter" +CUSTOM_ACCOUNT_CONFIRM_EMAIL_URL = "verify-email/?key={0}" +CUSTOM_ACCOUNT_PASSWORD_RESET_CONFIRM_URL = "password/reset/confirm/" +#URL_FRONT = "http://localhost:8000/" +#ACCOUNT_ADAPTER = "users.adapters.AccountAdapter" +ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "mandatory" +ACCOUNT_AUTHENTICATION_METHOD = "username_email" +ACCOUNT_CONFIRM_EMAIL_ON_GET = False +#ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None +#ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = reverse_lazy('account_confirm_complete') +ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 +ACCOUNT_EMAIL_CONFIRMATION_HMAC = True +ACCOUNT_EMAIL_SUBJECT_PREFIX = "Codebuddies: " +ACCOUNT_DEFAULT_HTTP_PROTOCOL = "http" +ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True +ACCOUNT_LOGOUT_ON_GET = False + # SOCIALACCOUNT_ADAPTER = "users.adapters.SocialAccountAdapter" +# #dj-rest-auth config +# #https://dj-rest-auth.readthedocs.io/en/latest/configuration.html +# # --------------------------------------------------------------------------------- +REST_USE_JWT = True +JWT_AUTH_COOKIE = 'cb-auth' +OLD_PASSWORD_FIELD_ENABLED = True +LOGOUT_ON_PASSWORD_CHANGE = True + + REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], @@ -323,23 +345,14 @@ 'rest_framework.renderers.BrowsableAPIRenderer', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', + # 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, } -REST_USE_JWT = True - -JWT_AUTH = { - 'JWT_AUTH_HEADER_PREFIX': 'Bearer', - 'JWT_RESPONSE_PAYLOAD_HANDLER': 'core.utils.my_jwt_response_handler', - 'JWT_ALLOW_REFRESH': True, - 'JWT_EXPIRATION_DELTA': timedelta(hours=1), - 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=3), -} - CORS_ORIGIN_WHITELIST = ( 'https://127.0.0.1:3000', 'http://localhost:3000', diff --git a/project/config/urls.py b/project/config/urls.py index 4391a52d..ad2802a7 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -4,25 +4,31 @@ from django.contrib import admin from django.views.generic import TemplateView from django.views import defaults as default_views -from rest_framework.exceptions import server_error +from rest_framework import routers, serializers, viewsets +from resources.urls import router as resources_router +from userauth.urls import router as userauth_router +router = routers.DefaultRouter() +router.registry.extend(resources_router.registry) +router.registry.extend(userauth_router.registry) urlpatterns = [ path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), - path( - "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" - ), + path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"), + # Django Admin, use {% url 'admin:index' %} path(settings.ADMIN_URL, admin.site.urls), + # User management path("users/", include("users.urls", namespace="users")), path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here - path('api/v1/', include('resources.urls')), - path('auth/', include('userauth.urls', namespace="userauth")), - path('rest-auth/', include('rest_auth.urls')), - path('rest-auth/registration/', include('rest_auth.registration.urls')) + #this is a route for logging into the "browsable api" if not needed for testing, it should be omitted. + path('api/v1/', include('rest_framework.urls', namespace='rest_framework')), + path('api/v1/auth/', include('userauth.urls', namespace="userauth")), + path('api/v1/', include('resources.urls', namespace='resources')), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/project/core/templates/account/email/email _confirmation_subject.txt b/project/core/templates/account/email/email _confirmation_subject.txt new file mode 100644 index 00000000..4c85ebb9 --- /dev/null +++ b/project/core/templates/account/email/email _confirmation_subject.txt @@ -0,0 +1 @@ +{% include "account/email/email_confirmation_subject.txt" %} diff --git a/project/core/templates/account/email/email_confirmation_message.txt b/project/core/templates/account/email/email_confirmation_message.txt new file mode 100644 index 00000000..9714bf4d --- /dev/null +++ b/project/core/templates/account/email/email_confirmation_message.txt @@ -0,0 +1,9 @@ +{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! + +You're receiving this e-mail because user {{ user_display }} has used this e-mail address to register an account on {{ site_domain }}. + +To confirm this is correct, go to {{ activate_url }} +{% endblocktrans %} +{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you from {{ site_name }}! +{{ site_domain }}{% endblocktrans %} +{% endautoescape %} diff --git a/project/requirements/base.txt b/project/requirements/base.txt index 3d62143e..f1c15b54 100644 --- a/project/requirements/base.txt +++ b/project/requirements/base.txt @@ -33,5 +33,7 @@ djangorestframework==3.10.2 # https://github.com/encode/django-rest-framework coreapi==2.3.3 # https://github.com/core-api/python-client django_taggit_serializer==0.1.7 #https://github.com/glemmaPaul/django-taggit-serializer drf-jwt==1.13.4 # https://github.com/Styria-Digital/django-rest-framework-jwt +djangorestframework-simplejwt #https://github.com/SimpleJWT/django-rest-framework-simplejwt django-rest-auth==0.9.5 #https://github.com/Tivix/django-rest-auth +dj-rest-auth==1.1.1 #https://github.com/jazzband/dj-rest-auth django-rest-authtoken==2.1.3 #https://pypi.org/project/django-rest-authtoken/ diff --git a/project/resources/serializers.py b/project/resources/serializers.py index 2d7329bb..7d72cf4a 100644 --- a/project/resources/serializers.py +++ b/project/resources/serializers.py @@ -7,19 +7,14 @@ class MediaTypeSerializerField(serializers.ChoiceField): def to_representation(self, value): - valid_media_types = ', '.join(item for item in self.choices) - if not value: return '' - else: try: media_type = self.choices[value] - - except KeyError as err: - raise KeyError(f'Invalid media type. The media type should be one of the following: {valid_media_types}') from err - + except KeyError: + raise serializers.ValidationError( f'Invalid media type. The media type should be one of the following: {valid_media_types}') return media_type def to_internal_value(self, value): diff --git a/project/resources/urls.py b/project/resources/urls.py index 37d35615..5440e3c7 100644 --- a/project/resources/urls.py +++ b/project/resources/urls.py @@ -2,10 +2,10 @@ from rest_framework import routers from . import views -router = routers.DefaultRouter() +app_name = 'resources' +router = routers.SimpleRouter() router.register(r'resources', views.ResourceView, basename='resources') urlpatterns = [ path('', include(router.urls)), - path('resource/', include('rest_framework.urls', namespace='rest_framework')), ] diff --git a/project/userauth/adapter.py b/project/userauth/adapter.py new file mode 100644 index 00000000..0f6c97df --- /dev/null +++ b/project/userauth/adapter.py @@ -0,0 +1,10 @@ +from allauth.account.adapter import DefaultAccountAdapter +from allauth.utils import build_absolute_uri +from django.conf import settings + +class CustomAccountAdapter(DefaultAccountAdapter): + + def get_email_confirmation_url(self, request, emailconfirmation): + url = settings.CUSTOM_ACCOUNT_CONFIRM_EMAIL_URL.format(emailconfirmation.key) + result = build_absolute_uri(request, url) + return result diff --git a/project/userauth/serializers.py b/project/userauth/serializers.py index 25a7e069..a41e3511 100644 --- a/project/userauth/serializers.py +++ b/project/userauth/serializers.py @@ -1,5 +1,8 @@ from rest_framework import serializers from rest_framework_jwt.settings import api_settings +from dj_rest_auth.serializers import LoginSerializer +from django.conf import settings +from allauth.account.models import EmailAddress from django.contrib.auth import get_user_model @@ -11,7 +14,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ('id', 'username', 'first_name', 'last_name', 'is_superuser',) + fields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_superuser') lookup_field = 'username' class UserSerializerWithToken(serializers.ModelSerializer): @@ -43,3 +46,37 @@ def create(self, validated_data): instance.save() return instance + +class VerifyEmailSerializer(serializers.ModelSerializer): + + def post(self, request): + serializer = self.get_serializer(data=request.DATA) + + if serializer.is_valid(): + user = UserSerializer(read_only=True) + + # check if settings swith is on / then check validity + if settings.ACCOUNT_EMAIL_VERIFICATION == settings.ACCOUNT_EMAIL_VERIFICATION_MANDATORY: + email_address = user.emailaddress_set.get(email=user.email) + + if not email_address.verified: + raise serializers.ValidationError(f'Your email is not verified. Please verify your email before continuing.') + + token = serializer.object.get('token') + response_data = jwt_response_payload_handler(token, user, request) + + return response_data + + +class UserLoginSerializer(LoginSerializer): + + user = get_user_model() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def post(self): + + if settings.ACCOUNT_EMAIL_VERIFICATION == 'mandatory': + if not EmailAddress.objects.get(email=user.get('email').verified()): + raise serializers.ValidationError(f'Your email is not verified. Please verify your email before continuing.') diff --git a/project/userauth/urls.py b/project/userauth/urls.py index fc27d1f9..5993f906 100644 --- a/project/userauth/urls.py +++ b/project/userauth/urls.py @@ -1,15 +1,32 @@ -from django.urls import path -from rest_framework_jwt.views import obtain_jwt_token -from rest_framework_jwt.views import refresh_jwt_token -from rest_framework_jwt.views import verify_jwt_token -from .views import current_user, UserList +from django.urls import include, path, re_path +from django.views.generic import RedirectView, TemplateView +from rest_framework import routers +from dj_rest_auth.views import PasswordResetView, PasswordResetConfirmView +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView, +) + +from . import views app_name = "userauth" +router = routers.SimpleRouter(r'userauth') + +urlpatterns = ( + path('', include(router.urls)), + path('', include('dj_rest_auth.urls')), + path('registration/', include('dj_rest_auth.registration.urls')), + path('registration/verify-email/', views.VerifyEmailView.as_view(), name='email_confirm'), + path('password/reset/', PasswordResetView.as_view(), name='password_reset'), + path('password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', TemplateView.as_view(template_name="password_reset_confirm.html"), name='password_reset_confirm'), + path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path('current_user/', views.current_user), + path('users/', views.UserList.as_view()), + ) + +#EmailAddress.objects.filter(user=self.request.user, verified=True).exists() -urlpatterns = [ - path('obtain_token/', obtain_jwt_token), - path('refresh_token/', refresh_jwt_token), - path('validate_token/', verify_jwt_token), - path('current_user/', current_user), - path('users/', UserList.as_view()), -] +#(?P[-:\w]+)/$' diff --git a/project/userauth/views.py b/project/userauth/views.py index 4df48c58..54a4bbe5 100644 --- a/project/userauth/views.py +++ b/project/userauth/views.py @@ -1,8 +1,14 @@ +from django.contrib.auth import get_user_model +from django.http import HttpResponseRedirect from rest_framework import permissions, status from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.views import APIView -from .serializers import UserSerializer, UserSerializerWithToken +from dj_rest_auth.registration.views import VerifyEmailView +from dj_rest_auth.views import LoginView +from allauth.account.views import ConfirmEmailView +from allauth.account.models import EmailConfirmation, EmailConfirmationHMAC +from .serializers import UserSerializer, VerifyEmailSerializer, UserLoginSerializer @api_view(['GET']) @@ -24,8 +30,36 @@ class UserList(APIView): permission_classes = (permissions.AllowAny,) def post(self, request, format=None): - serializer = UserSerializerWithToken(data=request.data) + serializer = UserSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class VerifyEmailView(APIView, ConfirmEmailView): + permission_classes = (permissions.AllowAny,) + allowed_methods = ('POST', 'OPTIONS', 'HEAD') + + def get_serializer(self, *args, **kwargs): + return VerifyEmailSerializer(*args, **kwargs) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.kwargs['key'] = serializer.validated_data['key'] + confirmation = self.get_object() + confirmation.confirm(self.request) + return Response({'detail': ('HiHi!!')}, status=status.HTTP_200_OK) + +class CustomLoginView(LoginView): + + permission_classes = (permissions.AllowAny,) + allowed_methods = ('POST', 'OPTIONS', 'HEAD') + + def get_serializer(self, *args, **kwargs): + return UserLoginSerializer(*args, **kwargs) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) diff --git a/project/users/urls.py b/project/users/urls.py index e2b75707..cf22b1a2 100644 --- a/project/users/urls.py +++ b/project/users/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import include, path from users.views import ( user_redirect_view, @@ -7,6 +7,7 @@ ) app_name = "users" + urlpatterns = [ path("~redirect/", view=user_redirect_view, name="redirect"), path("~update/", view=user_update_view, name="update"), From cdadba1b19f6c542806609ad9378fdd2650cc9ea Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:48:44 -0700 Subject: [PATCH 06/32] Removed django-rest-jwt and django-rest-auth added simplejwt and dj-rest-auth settings. --- project/config/settings/base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/project/config/settings/base.py b/project/config/settings/base.py index 6105773a..4018957e 100644 --- a/project/config/settings/base.py +++ b/project/config/settings/base.py @@ -297,7 +297,7 @@ # # ------------------------------------------------------------------------------ ACCOUNT_ADAPTER = "userauth.adapter.CustomAccountAdapter" CUSTOM_ACCOUNT_CONFIRM_EMAIL_URL = "verify-email/?key={0}" -CUSTOM_ACCOUNT_PASSWORD_RESET_CONFIRM_URL = "password/reset/confirm/" +CUSTOM_ACCOUNT_PASSWORD_RESET_CONFIRM_URL = "password/reset///" #URL_FRONT = "http://localhost:8000/" #ACCOUNT_ADAPTER = "users.adapters.AccountAdapter" ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) @@ -326,6 +326,12 @@ OLD_PASSWORD_FIELD_ENABLED = True LOGOUT_ON_PASSWORD_CHANGE = True +REST_AUTH_SERIALIZERS = { + 'USER_DETAILS_SERIALIZER': 'userauth.serializers.CustomUserDetailSerializer', + 'PASSWORD_RESET_SERIALIZER' : 'userauth.serializers.CustomPasswordResetSerializer', + 'PASSWORD_RESET_CONFIRM_SERIALIZER': 'userauth.serializers.CustomPasswordResetConfirmSerializer', +} + REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], @@ -346,9 +352,9 @@ ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', - # 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, } From 782e05b35f4453359b21aad57326d343db127e22 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:49:34 -0700 Subject: [PATCH 07/32] Added routes for registration and allauth. --- project/config/urls.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/project/config/urls.py b/project/config/urls.py index ad2802a7..c52fd57b 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -20,13 +20,16 @@ path(settings.ADMIN_URL, admin.site.urls), # User management - path("users/", include("users.urls", namespace="users")), + #currently inactive endpoint, but can re-activate if needed + #path("users/", include("users.urls", namespace="users")), + + #we have to include these for registration email validation, but otherwise these paths are NOT used path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here #this is a route for logging into the "browsable api" if not needed for testing, it should be omitted. path('api/v1/', include('rest_framework.urls', namespace='rest_framework')), - path('api/v1/auth/', include('userauth.urls', namespace="userauth")), + path('api/v1/auth/', include(('userauth.urls', 'userauth'), namespace="userauth")), path('api/v1/', include('resources.urls', namespace='resources')), From 040c6f3bda8dfc677b496ebc157684ae19add331 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:50:11 -0700 Subject: [PATCH 08/32] Added password reset email template. --- .../registration/password_reset_email.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 project/core/templates/registration/password_reset_email.html diff --git a/project/core/templates/registration/password_reset_email.html b/project/core/templates/registration/password_reset_email.html new file mode 100644 index 00000000..2d258766 --- /dev/null +++ b/project/core/templates/registration/password_reset_email.html @@ -0,0 +1,14 @@ +{% load i18n %}{% autoescape off %} +2 {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} +3 +4 {% trans "Please go to the following page and choose a new password:" %} +5 {% block reset_link %} +6 {{ protocol }}://{{ domain }}{% url 'userauth:password_reset_confirm' uidb64=uid token=token %} +7 {% endblock %} +8 {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} +9 +10 {% trans "Thanks for using our site!" %} +11 +12 {% blocktrans %}The {{ site_name }} team{% endblocktrans %} +13 +14 {% endautoescape %} From ef3a5ad08e3d18471c462603bfadeef07253f5fd Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:51:18 -0700 Subject: [PATCH 09/32] Added confirm_email method. Will probably remove it as uneeded later. --- project/userauth/adapter.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/project/userauth/adapter.py b/project/userauth/adapter.py index 0f6c97df..55dbbbf8 100644 --- a/project/userauth/adapter.py +++ b/project/userauth/adapter.py @@ -1,4 +1,5 @@ from allauth.account.adapter import DefaultAccountAdapter +from django.contrib.sites.models import Site from allauth.utils import build_absolute_uri from django.conf import settings @@ -8,3 +9,11 @@ def get_email_confirmation_url(self, request, emailconfirmation): url = settings.CUSTOM_ACCOUNT_CONFIRM_EMAIL_URL.format(emailconfirmation.key) result = build_absolute_uri(request, url) return result + + def confirm_email(self, request, email_address): + """ + Marks the email address as confirmed on the db + """ + email_address.verified = True + email_address.set_as_primary(conditional=True) + email_address.save() From 7e6ff72b7b48061ccaaf882225a1fcb58898b8e8 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:52:17 -0700 Subject: [PATCH 10/32] Added CustomUserDetailsSerializer. Stubbed other serializers for confrm emails and password reset. --- project/userauth/serializers.py | 83 +++++++++++---------------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/project/userauth/serializers.py b/project/userauth/serializers.py index a41e3511..6d2b8d25 100644 --- a/project/userauth/serializers.py +++ b/project/userauth/serializers.py @@ -1,11 +1,12 @@ from rest_framework import serializers from rest_framework_jwt.settings import api_settings -from dj_rest_auth.serializers import LoginSerializer -from django.conf import settings -from allauth.account.models import EmailAddress +from dj_rest_auth.serializers import JWTSerializer, UserDetailsSerializer +from dj_rest_auth.registration.serializers import VerifyEmailSerializer +from dj_rest_auth.serializers import PasswordResetSerializer, PasswordResetConfirmSerializer from django.contrib.auth import get_user_model +#customize this for a user profile view class UserSerializer(serializers.ModelSerializer): """ We use get_user_model here because of the custom User model. @@ -14,69 +15,39 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_superuser') + fields = ('id', 'username', 'first_name', 'last_name', 'is_superuser',) lookup_field = 'username' -class UserSerializerWithToken(serializers.ModelSerializer): - password = serializers.CharField(write_only=True) - token = serializers.SerializerMethodField() +#customize this for a user profile change +class CustomUserDetailSerializer(UserDetailsSerializer): + """ + We use get_user_model here because of the custom User model. + See: https://wsvincent.com/django-referencing-the-user-model/. + """ class Meta: model = get_user_model() - fields = ('username', 'token', 'password', 'first_name', 'last_name', 'email') - extra_kwargs = {'password': {'write_only': True}} - - def get_token(self, obj): - jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - - payload = jwt_payload_handler(obj) - token = jwt_encode_handler(payload) - - return token - - - def create(self, validated_data): - password = validated_data.pop('password', None) - instance = self.Meta.model(**validated_data) - - if password is not None: - instance.set_password(password) - instance.save() - - return instance - -class VerifyEmailSerializer(serializers.ModelSerializer): - - def post(self, request): - serializer = self.get_serializer(data=request.DATA) - - if serializer.is_valid(): - user = UserSerializer(read_only=True) - - # check if settings swith is on / then check validity - if settings.ACCOUNT_EMAIL_VERIFICATION == settings.ACCOUNT_EMAIL_VERIFICATION_MANDATORY: - email_address = user.emailaddress_set.get(email=user.email) - - if not email_address.verified: - raise serializers.ValidationError(f'Your email is not verified. Please verify your email before continuing.') - - token = serializer.object.get('token') - response_data = jwt_response_payload_handler(token, user, request) + fields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_superuser') + lookup_field = 'username' - return response_data +#customize this for a user email validation link +class CustomVerifyEmailSerializer(VerifyEmailSerializer): + key = serializers.CharField() -class UserLoginSerializer(LoginSerializer): - user = get_user_model() +#customize this for the password reset email. +class CustomPasswordResetSerializer(PasswordResetSerializer): + """ + Serializer for requesting a password reset e-mail. + """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def get_email_options(self): + """Override this method to change default e-mail options""" + return {} - def post(self): - if settings.ACCOUNT_EMAIL_VERIFICATION == 'mandatory': - if not EmailAddress.objects.get(email=user.get('email').verified()): - raise serializers.ValidationError(f'Your email is not verified. Please verify your email before continuing.') +#customize this for the passowrd reset confirmation +class CustomPasswordResetConfirmSerializer(PasswordResetConfirmSerializer): + pass From e97153b4f0c66fd88c95caa6532c29d805b4f849 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:53:05 -0700 Subject: [PATCH 11/32] Added routes for registration, email confirmation, password reset, login, logout, and user details. Reorganized. --- project/userauth/urls.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/project/userauth/urls.py b/project/userauth/urls.py index 5993f906..19f71ae8 100644 --- a/project/userauth/urls.py +++ b/project/userauth/urls.py @@ -1,5 +1,4 @@ -from django.urls import include, path, re_path -from django.views.generic import RedirectView, TemplateView +from django.urls import include, path, re_path, reverse_lazy from rest_framework import routers from dj_rest_auth.views import PasswordResetView, PasswordResetConfirmView from rest_framework_simplejwt.views import ( @@ -14,19 +13,14 @@ router = routers.SimpleRouter(r'userauth') urlpatterns = ( - path('', include(router.urls)), path('', include('dj_rest_auth.urls')), + path('', include(router.urls)), path('registration/', include('dj_rest_auth.registration.urls')), - path('registration/verify-email/', views.VerifyEmailView.as_view(), name='email_confirm'), + path('registration/verify-email/', views.CustomVerifyEmailView.as_view(), name='account_email_verification_sent'), path('password/reset/', PasswordResetView.as_view(), name='password_reset'), - path('password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', TemplateView.as_view(template_name="password_reset_confirm.html"), name='password_reset_confirm'), + path('password/reset/confirm///', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), path('current_user/', views.current_user), - path('users/', views.UserList.as_view()), ) - -#EmailAddress.objects.filter(user=self.request.user, verified=True).exists() - -#(?P[-:\w]+)/$' From b6c4ac215faa5b9d72c5e562cb46fdbfa39e1717 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:54:14 -0700 Subject: [PATCH 12/32] Added CustomVerifyEmailView to enable email verify emails from the backend. --- project/userauth/views.py | 47 ++++++++++----------------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/project/userauth/views.py b/project/userauth/views.py index 54a4bbe5..79c57a05 100644 --- a/project/userauth/views.py +++ b/project/userauth/views.py @@ -1,14 +1,15 @@ from django.contrib.auth import get_user_model -from django.http import HttpResponseRedirect +from django.contrib.sites.models import Site +from django.contrib.auth import views as auth_views +from django.utils.translation import ugettext_lazy as _ from rest_framework import permissions, status from rest_framework.decorators import api_view from rest_framework.response import Response +from rest_framework.exceptions import NotFound, MethodNotAllowed from rest_framework.views import APIView from dj_rest_auth.registration.views import VerifyEmailView -from dj_rest_auth.views import LoginView -from allauth.account.views import ConfirmEmailView -from allauth.account.models import EmailConfirmation, EmailConfirmationHMAC -from .serializers import UserSerializer, VerifyEmailSerializer, UserLoginSerializer +from dj_rest_auth.views import PasswordResetConfirmView as dj_PasswordResetConfirmView +from .serializers import UserSerializer, CustomVerifyEmailSerializer @api_view(['GET']) @@ -21,45 +22,21 @@ def current_user(request): return Response(serializer.data) -class UserList(APIView): - """ - Create a new user. It's called 'UserList' because normally we'd have a get - method here too, for retrieving a list of all User objects. - """ - - permission_classes = (permissions.AllowAny,) - - def post(self, request, format=None): - serializer = UserSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class VerifyEmailView(APIView, ConfirmEmailView): +#this is required for the allauth "validate your email address" email to work. +class CustomVerifyEmailView(VerifyEmailView): permission_classes = (permissions.AllowAny,) allowed_methods = ('POST', 'OPTIONS', 'HEAD') def get_serializer(self, *args, **kwargs): return VerifyEmailSerializer(*args, **kwargs) + def get(self, *args, **kwargs): + raise MethodNotAllowed('GET') + def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.kwargs['key'] = serializer.validated_data['key'] confirmation = self.get_object() confirmation.confirm(self.request) - return Response({'detail': ('HiHi!!')}, status=status.HTTP_200_OK) - -class CustomLoginView(LoginView): - - permission_classes = (permissions.AllowAny,) - allowed_methods = ('POST', 'OPTIONS', 'HEAD') - - def get_serializer(self, *args, **kwargs): - return UserLoginSerializer(*args, **kwargs) - - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) + return Response({'detail': _('ok')}, status=status.HTTP_200_OK) From f06a74c444f57f651b46f649557b53d9babd8af6 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 21 Sep 2020 20:54:47 -0700 Subject: [PATCH 13/32] Added tests for registration flow, token authorization, email verification, etc. --- project/userauth/tests.py | 849 +++++++++++++++++++++++++++++++++++++- 1 file changed, 846 insertions(+), 3 deletions(-) diff --git a/project/userauth/tests.py b/project/userauth/tests.py index 7b499d1e..1193d5d2 100644 --- a/project/userauth/tests.py +++ b/project/userauth/tests.py @@ -1,15 +1,857 @@ +import re +import pytest, pytest_django +import datetime from unittest.mock import patch +from allauth.account.models import EmailAddress from rest_framework import status, serializers -from rest_framework.test import APITestCase +from rest_framework.test import APITestCase, URLPatternsTestCase from rest_framework_jwt.settings import api_settings +from rest_framework_simplejwt.tokens import Token, AccessToken, RefreshToken +from rest_framework_simplejwt.utils import (aware_utcnow, datetime_from_epoch, datetime_to_epoch, format_lazy) from users.factories import UserFactory from factory import PostGenerationMethodCall +from django.core import mail from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ +from django.db import models -jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER -jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER +class UserauthTests(APITestCase): + + + def setUp(self): + #since the factories haven't been re-written, we're hardcoding here + self.user = { + "username": 'PetuniaPiglet', + "email": 'Petunia@thepiggyfarm.net', + "password1": 'codebuddies', + "password2": 'codebuddies' + } + + def test_registration_get(self): + """Ensure that a GET request is not accepted for the endpoint.""" + + url = '/api/v1/auth/registration/' + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEqual(response.data['detail'], "Method \"GET\" not allowed.") + + def test_registration_post(self): + """Ensure that a user is created in the db and a validation email message is returned upon user registering.""" + + url = '/api/v1/auth/registration/' + data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data['detail'], "Verification e-mail sent.") + + def test_registered_user_creation(self): + """Test that a registration POST creates a user in the DB.""" + + url = '/api/v1/auth/registration/' + data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + response = self.client.post(url, data, format='json') + model = get_user_model() + new_user = model.objects.get(username=self.user['username']) + + assert new_user.username == self.user['username'] + assert new_user.email == self.user['email'] + + def test_registration_emailaddress_validation_email(self): + """Ensure that a validation email is sent upon user registering.""" + + url = '/api/v1/auth/registration/' + data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data['detail'], "Verification e-mail sent.") + + # did Django actually send an email? + assert len(mail.outbox) == 1, "Inbox is not empty" + + def test_validation_email_content(self): + # start by registering a user + url = '/api/v1/auth/registration/' + data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + response = self.client.post(url, data, format='json') + + #is the email subject what we expect it to be? + verify_email_message = mail.outbox[0] + self.assertEqual(verify_email_message.subject, 'Codebuddies: Please Confirm Your E-mail Address') + + #extracting what we need for the verification link + uri_regex = re.compile(r"(\/api\/v1\/auth\/registration\/verify-email\/)(\?key=)([\w:-]+)") + confirmation_uri = re.search(uri_regex, verify_email_message.body) + + # is the uri for the verification link correct? + verification_path = "/api/v1/auth/registration/verify-email/" + self.assertEqual(confirmation_uri[1], verification_path) + + def test_verify_email_path_get(self): + """Ensure that a GET request is not accepted for the endpoint.""" + + url = '/api/v1/auth/registration/verify-email/' + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEqual(response.data['detail'], "Method \"GET\" not allowed.") + + + @pytest.mark.django_db(transaction=True) + def test_verify_email_path_post(self): + + # start by registering a user + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + response = self.client.post(reg_url, reg_data, format='json') + + # grab email from outbox so we can extract the verification link + email_message = mail.outbox[0] + verify_email_message = email_message.body + + # extracting what we need for the verification post action + uri_regex = re.compile(r"(\/api\/v1\/auth\/registration\/verify-email\/)(\?key=)([\w:]+)") + confirmation_uri = re.search(uri_regex, verify_email_message) + + # now, let's post the key to trigger validation + validate_email_url = f'/api/v1/auth/registration/verify-email/' + validate_key_data = {"key": confirmation_uri[3]} + validation_response = self.client.post(validate_email_url, validate_key_data, format='json') + print(validation_response) + + # did the post result in the correct status messages? + self.assertEqual(validation_response.status_code, status.HTTP_200_OK) + self.assertEqual(validation_response.data["detail"], "ok") + + + @pytest.mark.django_db(transaction=True) + def test_verify_email_marked_valid_after_post(self): + + #start by registering a user + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + response = self.client.post(reg_url, reg_data, format='json') + + #grab email from outbox so we can extract the verification link + verify_email_message = mail.outbox[0] + + #extracting what we need for the verification post action + uri_regex = re.compile(r"(\/api\/v1\/auth\/registration\/verify-email\/)(\?key=)([\w:-]+)") + confirmation_uri = re.search(uri_regex, verify_email_message.body) + + + #now, let's post the key to trigger validation + email_url = '/api/v1/auth/registration/verify-email/' + key_data = {"key": confirmation_uri[3]} + response = self.client.post(email_url, key_data, format='json') + + #did the post succeed in marking the email as valid in the DB? + model = get_user_model() + email_to_verify = EmailAddress.objects.get(email=reg_data['email']) + user = model.objects.get(pk=email_to_verify.user_id) + + self.assertEqual(email_to_verify.verified, True) + + def test_login_with_unverified_email(self): + + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + reg_response = self.client.post(reg_url, reg_data, format='json') + + login_url = '/api/v1/auth/login/' + login_data = { + "username": self.user['username'], + "email": self.user['email'], + "password": self.user['password1'] + } + + response = self.client.post(login_url, login_data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['non_field_errors'], ["E-mail is not verified."]) + + + @pytest.mark.django_db(transaction=True) + def test_login_with_verified_email(self): + + #first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + email_to_verify = EmailAddress.objects.get(email=reg_data['email']) + + #we set the verified flag to True for the account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + #we grab the user object based on the updated email for use in logging in + user = model.objects.get(pk=email_to_verify.user_id) + + #we login via the login endpoint + login_url = '/api/v1/auth/login/' + login_data = { + "username": user.username, + "email": user.email, + "password": "codebuddies", + } + response = self.client.post(login_url, login_data, format='json') + + #we validate that the user we posted is logged in, and an access_token and refresh_token are returned + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['user']['email'], user.email) + self.assertEqual(response.data['user']['username'], user.username) + self.assertContains(response, "access_token") + self.assertContains(response, "refresh_token") + + + @pytest.mark.django_db(transaction=True) + def test_login_bad_password(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + # next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + email_to_verify = EmailAddress.objects.get(email=reg_data['email']) + + # we set the verified flag to True for the account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + # we grab the user object based on the updated email for use in logging in + user = model.objects.get(pk=email_to_verify.user_id) + + #we attempt a login via the login endpoint, but with a bad password + login_url = '/api/v1/auth/login/' + login_data = { + "username": user.username, + "email": user.email, + "password": "bad_password", + } + response = self.client.post(login_url, login_data, format='json') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data['non_field_errors'], ["Unable to log in with provided credentials."]) + + + @pytest.mark.django_db(transaction=True) + def test_logout_get(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + # next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + email_to_verify = EmailAddress.objects.get(email=reg_data['email']) + + # we set the verified flag to True for the account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + # we grab the user object based on the updated email for use in logging in + user = model.objects.get(pk=email_to_verify.user_id) + + #we login via the login endpoint + login_url = '/api/v1/auth/login/' + login_data = { + "username": user.username, + "email": user.email, + "password": "codebuddies", + } + response = self.client.post(login_url, login_data, format='json') + + #next, we try to logout using GET + logout_url = '/api/v1/auth/logout/' + + response = self.client.get(logout_url, format='json') + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEqual(response.data['detail'], "Method \"GET\" not allowed.") + + + @pytest.mark.django_db(transaction=True) + def test_logout_post(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + # next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + email_to_verify = EmailAddress.objects.get(email=reg_data['email']) + + # we set the verified flag to True for the account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + # we grab the user object based on the updated email for use in logging in + user = model.objects.get(pk=email_to_verify.user_id) + + #we login via the login endpoint + login_url = '/api/v1/auth/login/' + login_data = { + "username": user.username, + "email": user.email, + "password": "codebuddies", + } + response = self.client.post(login_url, login_data, format='json') + + #next, we trigger logout and verify + logout_url = '/api/v1/auth/logout/' + + response = self.client.post(logout_url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['detail'], "Successfully logged out.") + + def test_password_reset_request_get(self): + """Ensure that a GET request is not accepted for the endpoint.""" + + url = '/api/v1/auth/password/reset/' + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEqual(response.data['detail'], "Method \"GET\" not allowed.") + + + def test_password_reset_request_post(self): + """Ensure that a password reset email is sent upon POST to reset endpoint.""" + + reset_url = '/api/v1/auth/password/reset/' + reset_data = {"email": self.user['email']} + + response = self.client.post(reset_url, reset_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['detail'], "Password reset e-mail has been sent.") + + + @pytest.mark.django_db(transaction=True) + def test_password_reset_email_sent(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we clean out the email outbox + mail.outbox.clear() + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + user_to_reset = model.objects.get(username=reg_data['username']) + email_to_verify = EmailAddress.objects.get(email=user_to_reset.email) + + # we set the verified flag to True for the reset_user_account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + reset_url = '/api/v1/auth/password/reset/' + reset_data = {"email": email_to_verify.email} + response = self.client.post(reset_url, reset_data, format='json') + + # did Django actually send an email? + assert len(mail.outbox) == 1, "Inbox is not empty" + + + @pytest.mark.django_db(transaction=True) + def test_passowrd_reset_email_content(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we clean out the email outbox + mail.outbox.clear() + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + user_to_reset = model.objects.get(username=reg_data['username']) + email_to_verify = EmailAddress.objects.get(email=user_to_reset.email) + + # we set the verified flag to True for the reset_user_account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + reset_url = '/api/v1/auth/password/reset/' + reset_data = {"email": email_to_verify.email} + response = self.client.post(reset_url, reset_data, format='json') + + # is the email subject and addressee what we expect them to be? + reset_email_message = mail.outbox[0] + self.assertEqual(reset_email_message.subject, 'Password reset on CBV3 Django Prototype') + self.assertEqual(reset_email_message.to[0], user_to_reset.email) + + # extracting what we need for the reset link + reset_uri_regex = re.compile(r"(\/api\/v1\/auth\/password\/reset\/confirm\/)([A-Z]+)([\w:-]+)") + reset_uri = re.search(reset_uri_regex, reset_email_message.body) + + # is the uri for the verification link correct? + reset_path = "/api/v1/auth/password/reset/confirm/" + self.assertEqual(reset_uri[1], reset_path) + + + @pytest.mark.django_db(transaction=True) + def test_password_reset_path_get(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we clean out the email outbox + mail.outbox.clear() + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + user_to_reset = model.objects.get(username=reg_data['username']) + email_to_verify = EmailAddress.objects.get(email=user_to_reset.email) + + # we set the verified flag to True for the reset_user_account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + reset_url = '/api/v1/auth/password/reset/' + reset_data = {"email": email_to_verify.email} + response = self.client.post(reset_url, reset_data, format='json') + + # extracting what we need for the reset link + reset_email_message = mail.outbox[0] + reset_uri_regex = re.compile(r"(\/api\/v1\/auth\/password\/reset\/confirm\/)([\w]+)\/([\w:-]+)\/") + reset_uri = re.search(reset_uri_regex, reset_email_message.body) + + # now we hit the uri with the info, but as a GET + password_reset_confirm_uri = f"/api/v1/auth/password/reset/confirm/{reset_uri[2]}/{reset_uri[3]}/" + reset_response = self.client.get(password_reset_confirm_uri) + + self.assertEqual(reset_response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEqual(reset_response.data['detail'], "Method \"GET\" not allowed.") + + + @pytest.mark.django_db(transaction=True) + def test_password_reset_post(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we clean out the email outbox + mail.outbox.clear() + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + user_to_reset = model.objects.get(username=reg_data['username']) + email_to_verify = EmailAddress.objects.get(email=user_to_reset.email) + + # we set the verified flag to True for the reset_user_account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + reset_url = '/api/v1/auth/password/reset/' + reset_email_address = {"email": email_to_verify.email} + response = self.client.post(reset_url, reset_email_address, format='json') + + # extracting what we need for the reset link + reset_email_message = mail.outbox[0] + reset_uri_regex = re.compile(r"(\/api\/v1\/auth\/password\/reset\/confirm\/)([\w]+)\/([\w:-]+)\/") + reset_uri = re.search(reset_uri_regex, reset_email_message.body) + reset_uid = reset_uri[2] + reset_token = reset_uri[3] + + # now we POST to the uri with the UID, TOKEN and the reset data + password_reset_confirm_uri = f"/api/v1/auth/password/reset/confirm/{reset_uid}/{reset_token}/" + password_reset_confirm_data = { + "new_password1": "codebuddies_II", + "new_password2": "codebuddies_II", + "uid": reset_uid, + "token": reset_token + } + + password_reset_confirm_response = self.client.post(password_reset_confirm_uri, password_reset_confirm_data, format='json') + self.assertEqual(password_reset_confirm_response.status_code, status.HTTP_200_OK) + self.assertEqual(password_reset_confirm_response.data['detail'], "Password has been reset with the new password.") + + + @pytest.mark.django_db(transaction=True) + def test_password_reset_db_change(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we clean out the email outbox + mail.outbox.clear() + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + user_to_reset = model.objects.get(username=reg_data['username']) + email_to_verify = EmailAddress.objects.get(email=user_to_reset.email) + + # we set the verified flag to True for the reset_user_account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + reset_url = '/api/v1/auth/password/reset/' + reset_email_address = {"email": email_to_verify.email} + response = self.client.post(reset_url, reset_email_address, format='json') + + # extracting what we need for the reset link + reset_email_message = mail.outbox[0] + reset_uri_regex = re.compile(r"(\/api\/v1\/auth\/password\/reset\/confirm\/)([\w]+)\/([\w:-]+)\/") + reset_uri = re.search(reset_uri_regex, reset_email_message.body) + reset_uid = reset_uri[2] + reset_token = reset_uri[3] + + # now we POST to the uri with the UID, TOKEN and the reset data + password_reset_confirm_uri = f"/api/v1/auth/password/reset/confirm/{reset_uid}/{reset_token}/" + password_reset_confirm_data = { + "new_password1": "codebuddies_II", + "new_password2": "codebuddies_II", + "uid": reset_uid, + "token": reset_token + } + + password_reset_confirm_response = self.client.post(password_reset_confirm_uri, password_reset_confirm_data, format='json') + self.assertEqual(password_reset_confirm_response.status_code, status.HTTP_200_OK) + self.assertEqual(password_reset_confirm_response.data['detail'], "Password has been reset with the new password.") + + + @pytest.mark.django_db(transaction=True) + def test_login_with_reset_password(self): + + # first we register through the registration endpoint + reg_url = '/api/v1/auth/registration/' + reg_data = { + "username": self.user['username'], + "email": self.user['email'], + "password1": self.user['password1'], + "password2": self.user['password2'] + } + + self.client.post(reg_url, reg_data, format='json') + + #next, we clean out the email outbox + mail.outbox.clear() + + #next, we retrieve the newly created user model and their corresponding account_emailaddress + model = get_user_model() + user_to_reset = model.objects.get(username=reg_data['username']) + email_to_verify = EmailAddress.objects.get(email=user_to_reset.email) + + # we set the verified flag to True for the reset_user_account_emailaddress and save + email_to_verify.verified = 1 + email_to_verify.save() + + reset_url = '/api/v1/auth/password/reset/' + reset_email_address = {"email": email_to_verify.email} + response = self.client.post(reset_url, reset_email_address, format='json') + + # extracting what we need for the reset link + reset_email_message = mail.outbox[0] + reset_uri_regex = re.compile(r"(\/api\/v1\/auth\/password\/reset\/confirm\/)([\w]+)\/([\w:-]+)\/") + reset_uri = re.search(reset_uri_regex, reset_email_message.body) + reset_uid = reset_uri[2] + reset_token = reset_uri[3] + + # now we POST to the uri with the UID, TOKEN and the reset data + password_reset_confirm_uri = f"/api/v1/auth/password/reset/confirm/{reset_uid}/{reset_token}/" + password_reset_confirm_data = { + "new_password1": "codebuddies_II", + "new_password2": "codebuddies_II", + "uid": reset_uid, + "token": reset_token + } + + self.client.post(password_reset_confirm_uri, password_reset_confirm_data, format='json') + + + #finally, we attempt a login with the newly reset password + new_login_uri = '/api/v1/auth/login/' + new_login_data = { + "username": user_to_reset.username, + "email": user_to_reset.email, + "password": "codebuddies_II", + } + + new_login_response = self.client.post(new_login_uri, new_login_data, format='json') + + # finally, we validate that the user we posted is + # logged in with the new password and an access_token and refresh_token are returned + self.assertEqual(new_login_response.status_code, status.HTTP_200_OK) + self.assertEqual(new_login_response.data['user']['email'], user_to_reset.email) + self.assertEqual(new_login_response.data['user']['username'], user_to_reset.username) + self.assertContains(new_login_response, "access_token") + self.assertContains(new_login_response, "refresh_token") + + @pytest.mark.django_db(transaction=True) + def test_view_user_details_authed(self): + + token_uri = '/api/v1/auth/token/' + user_to_view = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_auth_data = { + "username": user_to_view.username, + "password": 'codebuddies' + } + + authed_user_tokens = self.client.post(token_uri, user_auth_data, format='json') + authed_user_access_token = authed_user_tokens.data['access'] + authed_user_refresh_token = authed_user_tokens.data['refresh'] + + self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + authed_user_access_token) + + user_details_uri = '/api/v1/auth/user/' + user_details_response = self.client.get(user_details_uri) + + self.assertEqual(user_details_response.status_code, status.HTTP_200_OK) + self.assertEqual(user_details_response.data['username'], user_to_view.username) + + + def test_view_user_details_unauthed(self): + + details_uri = '/api/v1/auth/user/' + + details_response = self.client.get(details_uri) + self.assertEqual(details_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(details_response.data['detail'], "Authentication credentials were not provided.") + + def test_current_user_method_authed(self): + pass + + def test_current_user_method_unauthed(self): + pass + + def test_JWTtoken_obtain_pair(self): + token_uri = '/api/v1/auth/token/' + user_to_auth = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_auth_data = { + "username": user_to_auth.username, + "password": 'codebuddies' + } + + JWT_user_tokens = self.client.post(token_uri, user_auth_data, format='json') + self.assertEqual(JWT_user_tokens.status_code, status.HTTP_200_OK) + self.assertContains(JWT_user_tokens, "access") + self.assertContains(JWT_user_tokens, "refresh") + + def test_JWTtoken_refresh(self): + token_uri = '/api/v1/auth/token/' + user_to_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_refresh_data = { + "username": user_to_refresh.username, + "password": 'codebuddies' + } + + JWT_user_obtain_tokens = self.client.post(token_uri, user_refresh_data, format='json') + + #now we refresh the token + refresh_uri = '/api/v1/auth/token/refresh/' + data_to_refresh = { + "refresh": JWT_user_obtain_tokens.data['refresh'], + } + + renewed_token = self.client.post(refresh_uri, data_to_refresh, format='json') + + self.assertEqual(renewed_token.status_code, status.HTTP_200_OK) + self.assertContains(renewed_token, "access") + + def test_JWTtoken_validate_access(self): + + token_uri = '/api/v1/auth/token/' + user_to_validate = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_validate_data = { + "username": user_to_validate.username, + "password": 'codebuddies' + } + + JWT_user_validate_tokens = self.client.post(token_uri, user_validate_data, format='json') + + # now we validate the token + validation_uri = '/api/v1/auth/token/verify/' + data_to_validate = { + "token": JWT_user_validate_tokens.data['access'], + } + + validated_token = self.client.post(validation_uri, data_to_validate, format='json') + self.assertEqual(validated_token.status_code, status.HTTP_200_OK) + + def test_JWTtoken_validate_refresh(self): + + token_uri = '/api/v1/auth/token/' + user_to_validate = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_validate_data = { + "username": user_to_validate.username, + "password": 'codebuddies' + } + + JWT_user_validate_tokens = self.client.post(token_uri, user_validate_data, format='json') + + # now we validate the token + validation_uri = '/api/v1/auth/token/verify/' + data_to_validate = { + "token": JWT_user_validate_tokens.data['refresh'], + } + + validated_token = self.client.post(validation_uri, data_to_validate, format='json') + self.assertEqual(validated_token.status_code, status.HTTP_200_OK) + + def test_JWTtoken_expired_access(self): + + #make a user to auth + user_to_expire = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + + #make a date that's yesterday + today = datetime.datetime.now() + diff = datetime.timedelta(days=1) + start_time = today - diff + + #now we manually create a token with our user and a token that expired yesterday + expired_token = AccessToken.for_user(user_to_expire) + expired_token.set_exp(from_time=start_time) + + # now we attempt to validate the expired token + expiration_validation_uri = '/api/v1/auth/token/verify/' + data_to_validate_expired = {"token": str(expired_token),} + expired_token_response = self.client.post(expiration_validation_uri, data_to_validate_expired, format='json') + + + #did we get rejected by the API? + self.assertEqual(expired_token_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(expired_token_response.data['detail'], "Token is invalid or expired") + self.assertEqual(expired_token_response.data['code'], "token_not_valid") + + + # self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) + # self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) + + def test_JWTtoken_refresh_expired_access(self): + + # make a user to auth + user_to_expire_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + + # make a date that's yesterday + today = datetime.datetime.now() + diff = datetime.timedelta(days=1) + start_time = today - diff + + # now we manually create a token with our user and a token that expired yesterday + expired_token = AccessToken.for_user(user_to_expire_refresh) + expired_token.set_exp(from_time=start_time) + + # now we attempt to validate the expired token + expiration_refresh_uri = '/api/v1/auth/token/refresh/' + data_to_refresh_expired = {"refresh": str(expired_token), } + expired_refresh_response = self.client.post(expiration_refresh_uri, data_to_refresh_expired, format='json') + + # did we get rejected by the API? + self.assertEqual(expired_refresh_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(expired_refresh_response.data['detail'], "Token is invalid or expired") + self.assertEqual(expired_refresh_response.data['code'], "token_not_valid") + + + @patch('rest_framework_simplejwt.tokens.Token.check_exp') + def test_JWTtoken_expired_after_refresh(self, check_exp): + pass + +''' class UserauthTests(APITestCase): def setUp(self): @@ -178,3 +1020,4 @@ def test_create_new_user(self): self.assertEqual((response.data['first_name'], response.data['last_name']),('Cali', 'French')) self.assertEqual(response.data['email'], 'asificare@mailme.net') self.assertContains(response, 'token', status_code=201) +''' From 221b7b6a142e7412c534e0aa0b46d8eb4980a9de Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 23 Sep 2020 21:13:40 -0700 Subject: [PATCH 14/32] Fixed tests that were failing due to auth change. --- project/resources/tests.py | 58 ++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/project/resources/tests.py b/project/resources/tests.py index 827a070e..09aefc71 100644 --- a/project/resources/tests.py +++ b/project/resources/tests.py @@ -1,18 +1,11 @@ -from unittest import skip -from pytest import raises from random import randint -from rest_framework import status -from rest_framework.test import APITestCase -from rest_framework_jwt.settings import api_settings +from rest_framework import status, serializers +from rest_framework.test import APITestCase, URLPatternsTestCase from users.factories import UserFactory from resources.factories import ResourceFactory from factory import PostGenerationMethodCall, LazyAttribute, create, create_batch -jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER -jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - - class PublicResourcesTests(APITestCase): # Viewing resources, viewing a single resource, and search don't require user authentication @@ -73,13 +66,16 @@ def test_create_a_resource(self): class AuthedResourcesTests(APITestCase): def setUp(self): - self.user = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + token_uri = '/api/v1/auth/token/' + user_to_auth = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_auth_data = { + "username": user_to_auth.username, + "password": 'codebuddies' + } - url = '/auth/obtain_token/' - data = {"username": self.user.username, "password": "codebuddies"} - token_response = self.client.post(url, data, format='json') + JWT_user_tokens = self.client.post(token_uri, user_auth_data, format='json') - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) + self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + JWT_user_tokens.data['access']) def test_patch_one_resource(self): new_resource = create(ResourceFactory) @@ -189,20 +185,22 @@ def test_create_one_resource_without_media_type(self): self.assertEqual(response.data['media_type'], '') def test_create_one_resource_with_invalid_media_type(self): - with raises(KeyError, match=r"The media type should be one of the following:"): - url = '/api/v1/resources/' - data = {"title": "The Best Medium-Hard Data Analyst SQL Interview Questions", - "author": "Zachary Thomas", - "description": "The first 70% of SQL is pretty straightforward but the remaining 30% can be pretty tricky. These are good practice problems for that tricky 30% part.", - "url": "https://quip.com/2gwZArKuWk7W", - "referring_url": "https://quip.com", - "other_referring_source": "twitter.com/lpnotes", - "date_published": "2020-04-19T03:27:06Z", - "created": "2020-05-02T03:27:06.485Z", - "modified": "2020-05-02T03:27:06.485Z", - "media_type": "DOP", - "tags": ["SQLt", "BackEnd", "Databases"] - } - - response = self.client.post(url, data, format='json') + url = '/api/v1/resources/' + data = {"title": "The Best Medium-Hard Data Analyst SQL Interview Questions", + "author": "Zachary Thomas", + "description": "The first 70% of SQL is pretty straightforward but the remaining 30% can be pretty tricky. These are good practice problems for that tricky 30% part.", + "url": "https://quip.com/2gwZArKuWk7W", + "referring_url": "https://quip.com", + "other_referring_source": "twitter.com/lpnotes", + "date_published": "2020-04-19T03:27:06Z", + "created": "2020-05-02T03:27:06.485Z", + "modified": "2020-05-02T03:27:06.485Z", + "media_type": "DOP", + "tags": ["SQLt", "BackEnd", "Databases"] + } + + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data[0], "Invalid media type. The media type should be one of the following: VID, POD, PODEP, TALK, TUTOR, COURSE, BOOK, BLOG, GAME, EVENT, TOOL, LIB, WEB") From e8197e7d1f2e99ab9696f518844ae008c5db35c7 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 23 Sep 2020 21:14:05 -0700 Subject: [PATCH 15/32] Added final simpleJWT tests for auth. --- project/userauth/tests.py | 342 ++++++++++++++++---------------------- 1 file changed, 143 insertions(+), 199 deletions(-) diff --git a/project/userauth/tests.py b/project/userauth/tests.py index 1193d5d2..be1ecb7b 100644 --- a/project/userauth/tests.py +++ b/project/userauth/tests.py @@ -1,20 +1,14 @@ import re import pytest, pytest_django import datetime -from unittest.mock import patch from allauth.account.models import EmailAddress from rest_framework import status, serializers from rest_framework.test import APITestCase, URLPatternsTestCase -from rest_framework_jwt.settings import api_settings from rest_framework_simplejwt.tokens import Token, AccessToken, RefreshToken -from rest_framework_simplejwt.utils import (aware_utcnow, datetime_from_epoch, datetime_to_epoch, format_lazy) from users.factories import UserFactory from factory import PostGenerationMethodCall from django.core import mail from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ -from django.db import models - class UserauthTests(APITestCase): @@ -704,7 +698,6 @@ def test_view_user_details_authed(self): self.assertEqual(user_details_response.status_code, status.HTTP_200_OK) self.assertEqual(user_details_response.data['username'], user_to_view.username) - def test_view_user_details_unauthed(self): details_uri = '/api/v1/auth/user/' @@ -714,10 +707,37 @@ def test_view_user_details_unauthed(self): self.assertEqual(details_response.data['detail'], "Authentication credentials were not provided.") def test_current_user_method_authed(self): - pass + + token_uri = '/api/v1/auth/token/' + user_to_auth = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_to_auth_data = { + "username": user_to_auth.username, + "password": 'codebuddies' + } + + authed_user_tokens = self.client.post(token_uri, user_to_auth_data, format='json') + authed_user_access_token = authed_user_tokens.data['access'] + authed_user_refresh_token = authed_user_tokens.data['refresh'] + authed_user_uri = '/api/v1/auth/current_user/' + + self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + authed_user_access_token) + current_user_request = self.client.get(authed_user_uri) + + #do we get back the "current users" view info? + self.assertEqual(current_user_request.status_code, status.HTTP_200_OK) + self.assertEqual(current_user_request.data['username'], user_to_auth.username) + self.assertContains(current_user_request, 'is_superuser') + self.assertContains(current_user_request, 'last_name') + self.assertContains(current_user_request, 'id') def test_current_user_method_unauthed(self): - pass + + unauthed_user_uri = '/api/v1/auth/current_user/' + + unauthed_current_user_request = self.client.get(unauthed_user_uri) + + #do we get rejcted by the api? + self.assertEqual(unauthed_current_user_request.status_code, status.HTTP_401_UNAUTHORIZED) def test_JWTtoken_obtain_pair(self): token_uri = '/api/v1/auth/token/' @@ -732,28 +752,7 @@ def test_JWTtoken_obtain_pair(self): self.assertContains(JWT_user_tokens, "access") self.assertContains(JWT_user_tokens, "refresh") - def test_JWTtoken_refresh(self): - token_uri = '/api/v1/auth/token/' - user_to_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) - user_refresh_data = { - "username": user_to_refresh.username, - "password": 'codebuddies' - } - - JWT_user_obtain_tokens = self.client.post(token_uri, user_refresh_data, format='json') - - #now we refresh the token - refresh_uri = '/api/v1/auth/token/refresh/' - data_to_refresh = { - "refresh": JWT_user_obtain_tokens.data['refresh'], - } - - renewed_token = self.client.post(refresh_uri, data_to_refresh, format='json') - - self.assertEqual(renewed_token.status_code, status.HTTP_200_OK) - self.assertContains(renewed_token, "access") - - def test_JWTtoken_validate_access(self): + def test_JWTtoken_verify_active_access_token(self): token_uri = '/api/v1/auth/token/' user_to_validate = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) @@ -773,7 +772,7 @@ def test_JWTtoken_validate_access(self): validated_token = self.client.post(validation_uri, data_to_validate, format='json') self.assertEqual(validated_token.status_code, status.HTTP_200_OK) - def test_JWTtoken_validate_refresh(self): + def test_JWTtoken_verify_active_refresh_token(self): token_uri = '/api/v1/auth/token/' user_to_validate = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) @@ -793,7 +792,7 @@ def test_JWTtoken_validate_refresh(self): validated_token = self.client.post(validation_uri, data_to_validate, format='json') self.assertEqual(validated_token.status_code, status.HTTP_200_OK) - def test_JWTtoken_expired_access(self): + def test_JWTtoken_verify_with_expired_access_token(self): #make a user to auth user_to_expire = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) @@ -818,11 +817,75 @@ def test_JWTtoken_expired_access(self): self.assertEqual(expired_token_response.data['detail'], "Token is invalid or expired") self.assertEqual(expired_token_response.data['code'], "token_not_valid") + def test_JWTtoken_verify_with_expired_refresh_token(self): + + #make a user to auth + user_to_expire_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + + #make a date that's yesterday + today = datetime.datetime.now() + diff = datetime.timedelta(days=1) + start_time = today - diff + + #now we manually create a token with our user and a token that expired yesterday + expired_refresh_token = RefreshToken.for_user(user_to_expire_refresh) + expired_refresh_token.set_exp(from_time=start_time) + + # now we attempt to validate the expired token + expiration_validation_uri = '/api/v1/auth/token/verify/' + data_to_validate_expired_refresh = {"token": str(expired_refresh_token),} + expired_token_response = self.client.post(expiration_validation_uri, data_to_validate_expired_refresh, format='json') + + + #did we get rejected by the API? + self.assertEqual(expired_token_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(expired_token_response.data['detail'], "Token is invalid or expired") + self.assertEqual(expired_token_response.data['code'], "token_not_valid") + + def test_JWTtoken_refresh_access_token_with_active_refresh_token(self): + token_uri = '/api/v1/auth/token/' + user_to_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_refresh_data = { + "username": user_to_refresh.username, + "password": 'codebuddies' + } + + JWT_user_obtain_tokens = self.client.post(token_uri, user_refresh_data, format='json') + + #now we call refresh with the active refresh token + refresh_uri = '/api/v1/auth/token/refresh/' + data_to_refresh = { + "refresh": JWT_user_obtain_tokens.data['refresh'], + } + + renewed_token = self.client.post(refresh_uri, data_to_refresh, format='json') + + # Did we get a new access token in exchange for the refresh request? + self.assertEqual(renewed_token.status_code, status.HTTP_200_OK) + self.assertContains(renewed_token, "access") - # self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - # self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) + def test_JWTtoken_refresh_access_token_with_active_access_token(self): + token_uri = '/api/v1/auth/token/' + user_to_refresh_access = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) + user_refresh_access_data = { + "username": user_to_refresh_access.username, + "password": 'codebuddies' + } - def test_JWTtoken_refresh_expired_access(self): + JWT_user_obtain_tokens_to_refresh = self.client.post(token_uri, user_refresh_access_data, format='json') + + #now we call refresh with the active refresh token + refresh_uri = '/api/v1/auth/token/refresh/' + access_data_to_refresh = { + "refresh": JWT_user_obtain_tokens_to_refresh.data['access'], + } + + renewed_access_token = self.client.post(refresh_uri, access_data_to_refresh, format='json') + + # Did we get rejected from refresh request due to it being an access token? + self.assertEqual(renewed_access_token.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_JWTtoken_refresh_expired_access_token_with_expired_access_token(self): # make a user to auth user_to_expire_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) @@ -846,178 +909,59 @@ def test_JWTtoken_refresh_expired_access(self): self.assertEqual(expired_refresh_response.data['detail'], "Token is invalid or expired") self.assertEqual(expired_refresh_response.data['code'], "token_not_valid") + def test_JWTtoken_refresh_expired_access_token_with_valid_refresh_token(self): + # make a user to auth + user_to_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) - @patch('rest_framework_simplejwt.tokens.Token.check_exp') - def test_JWTtoken_expired_after_refresh(self, check_exp): - pass - -''' -class UserauthTests(APITestCase): - - def setUp(self): - self.user = UserFactory( - password=PostGenerationMethodCall('set_password', 'codebuddies') - ) - - def test_jwt_not_authed(self): - """ - Ensure that if we aren't authed with a token, we don't get to view the - current_user - """ - - url = '/auth/current_user/' - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # make a date that's yesterday + today = datetime.datetime.now() + diff = datetime.timedelta(days=1) + diff_refresh = datetime.timedelta(hours=1) + start_time = today - diff + start_time_refresh = today + diff_refresh - def test_jwt_auth(self): - """ - Ensure we can obtain a token with a valid UN and PW combo. - """ + # now we manually create a access token with our user and a token that expired yesterday + expired_token = AccessToken.for_user(user_to_refresh) + expired_token.set_exp(from_time=start_time) - url = '/auth/obtain_token/' - data = {"username": self.user.username, "password": "codebuddies"} - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertContains(response, 'token') - - - def test_jwt_validate(self): - """ - Ensure we can validate a previously acquired token. - """ - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - token = token_response.data['token'] - url = '/auth/validate_token/' - data = {"token": token} - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertContains(response, token) - self.assertContains(response, self.user.username) - - - def test_jwt_current_user(self): - """ - Ensure that if we obtain a token in the 'browser', - we can retrieve the current_user based on the browser token - """ - - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - url = '/auth/current_user/' - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertContains(response, self.user.username) - self.assertContains(response, 'is_superuser') - - - def test_jwt_refresh(self): - """ - Ensure that if we ask for a token refresh based on our current token - we get a refreshed token in return. - """ - - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - url = '/auth/refresh_token/' - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - data = {"token": token_response.data['token']} - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(token_response.data['token'], response.data['token'], msg=None) - - - @patch('rest_framework_jwt.serializers.RefreshAuthTokenSerializer.validate') - def test_jwt_expired_refresh(self, validate_mock): - """ - Ensure that a request to refresh and expired token fails. - """ - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - url = '/auth/refresh_token/' - data = {"token": token_response.data['token']} - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - validate_mock.side_effect = serializers.ValidationError('Refresh has expired.') - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + # next, we manually crate a valid refresh token with our user and a now() date + refresh_token = RefreshToken.for_user(user_to_refresh) + refresh_token.set_exp(from_time=start_time_refresh) + # now we attempt to refresh the expired access token with the refresh token + expiration_refresh_uri = '/api/v1/auth/token/refresh/' + data_to_refresh_expired = {"refresh": str(refresh_token), } + refresh_response = self.client.post(expiration_refresh_uri, data_to_refresh_expired, format='json') - @patch('rest_framework_jwt.serializers._check_payload') - def test_jwt_expired_token_validate(self, validate_mock): - """ - Ensure that a request to validate an expired token fails. - """ - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - url = '/auth/validate_token/' - data = {"token": token_response.data['token']} - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - validate_mock.side_effect = serializers.ValidationError('Token has expired.') - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + # did we get a new, valid access token from the API? + self.assertEqual(refresh_response.status_code, status.HTTP_200_OK) + self.assertContains(refresh_response, 'access') + def test_JWTtoken_refresh_expired_access_token_with_expired_refresh_token(self): + # make a user to auth + user_to_refresh = UserFactory(password=PostGenerationMethodCall('set_password', 'codebuddies')) - @patch('rest_framework_jwt.serializers._check_payload') - def test_jwt_expired_token_access(self, validate_mock): - """ - Ensure that a request to a protected api endpoint fails with an - expired token. - """ - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - url = '/api/v1/resources/' - data = {"token": token_response.data['token']} - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - validate_mock.side_effect = serializers.ValidationError('Token has expired.') - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + # make a date that's yesterday + today = datetime.datetime.now() + diff = datetime.timedelta(days=1) + start_time = today - diff + start_time_refresh_past = today - diff + # now we manually create a access token with our user and a token that expired yesterday + expired_token = AccessToken.for_user(user_to_refresh) + expired_token.set_exp(from_time=start_time) - def test_create_new_user(self): - """ - Ensure that a new user is created in the DB and a token for that user - is returned with valid confirmation data. - """ + # next, we manually crate a refresh token with our user and a now() date + past_refresh_token = RefreshToken.for_user(user_to_refresh) + past_refresh_token.set_exp(from_time=start_time_refresh_past) - url = '/auth/users/' - data = { - "username": "claudette", - "password": "codebuddies", - "first_name": "Cali", - "last_name": "French", - "email": "asificare@mailme.net" - } - token_response = self.client.post( - '/auth/obtain_token/', - {"username": self.user.username, "password": "codebuddies"}, - format='json' - ) - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_response.data['token']) - response = self.client.post(url, data, format='json') + # now we attempt to refresh the expired access token with the refresh token + expiration_refresh_uri = '/api/v1/auth/token/refresh/' + data_to_refresh_past = {"refresh": str(past_refresh_token), } + past_refresh_response = self.client.post(expiration_refresh_uri, data_to_refresh_past, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data['username'], 'claudette') - self.assertEqual((response.data['first_name'], response.data['last_name']),('Cali', 'French')) - self.assertEqual(response.data['email'], 'asificare@mailme.net') - self.assertContains(response, 'token', status_code=201) -''' + # did we get rejected because the refresh token has expired? + self.assertEqual(past_refresh_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(past_refresh_response.data['detail'], "Token is invalid or expired") + self.assertEqual(past_refresh_response.data['code'], "token_not_valid") From f2202246de186274cc99089ac2661b290f7f6caf Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 23 Sep 2020 21:14:30 -0700 Subject: [PATCH 16/32] Added skips for User tests broken due to auth changes. --- project/users/tests/test_models.py | 2 +- project/users/tests/test_urls.py | 6 +++--- project/users/tests/test_views.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/project/users/tests/test_models.py b/project/users/tests/test_models.py index 54863632..12efb61e 100644 --- a/project/users/tests/test_models.py +++ b/project/users/tests/test_models.py @@ -3,6 +3,6 @@ pytestmark = pytest.mark.django_db - +@pytest.mark.skip(reason="App needs rewrite after auth change.") def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL): assert user.get_absolute_url() == f"/users/{user.username}/" diff --git a/project/users/tests/test_urls.py b/project/users/tests/test_urls.py index c6361920..90842c11 100644 --- a/project/users/tests/test_urls.py +++ b/project/users/tests/test_urls.py @@ -4,7 +4,7 @@ pytestmark = pytest.mark.django_db - +@pytest.mark.skip(reason="App needs rewrite after auth change.") def test_detail(user: settings.AUTH_USER_MODEL): assert ( reverse("users:detail", kwargs={"username": user.username}) @@ -12,12 +12,12 @@ def test_detail(user: settings.AUTH_USER_MODEL): ) assert resolve(f"/users/{user.username}/").view_name == "users:detail" - +@pytest.mark.skip(reason="App needs rewrite after auth change.") def test_update(): assert reverse("users:update") == "/users/~update/" assert resolve("/users/~update/").view_name == "users:update" - +@pytest.mark.skip(reason="App needs rewrite after auth change.") def test_redirect(): assert reverse("users:redirect") == "/users/~redirect/" assert resolve("/users/~redirect/").view_name == "users:redirect" diff --git a/project/users/tests/test_views.py b/project/users/tests/test_views.py index 6bf42cb6..9df7fc02 100644 --- a/project/users/tests/test_views.py +++ b/project/users/tests/test_views.py @@ -6,7 +6,7 @@ pytestmark = pytest.mark.django_db - +@pytest.mark.skip(reason="App needs rewrite after auth change.") class TestUserUpdateView: """ TODO: @@ -38,7 +38,7 @@ def test_get_object( assert view.get_object() == user - +@pytest.mark.skip(reason="App needs rewrite after auth change.") class TestUserRedirectView: def test_get_redirect_url( self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory From bb47eb0669c01eacb4e9f3a2204dec72d9696c45 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 23 Sep 2020 22:47:52 -0700 Subject: [PATCH 17/32] Removed django-rest_auth and django-rest-jwt from requirememts. --- project/requirements/base.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/project/requirements/base.txt b/project/requirements/base.txt index f1c15b54..9f2c0d57 100644 --- a/project/requirements/base.txt +++ b/project/requirements/base.txt @@ -32,8 +32,6 @@ django-taggit==1.2.0 # https://github.com/jazzband/django-taggit djangorestframework==3.10.2 # https://github.com/encode/django-rest-framework coreapi==2.3.3 # https://github.com/core-api/python-client django_taggit_serializer==0.1.7 #https://github.com/glemmaPaul/django-taggit-serializer -drf-jwt==1.13.4 # https://github.com/Styria-Digital/django-rest-framework-jwt djangorestframework-simplejwt #https://github.com/SimpleJWT/django-rest-framework-simplejwt -django-rest-auth==0.9.5 #https://github.com/Tivix/django-rest-auth dj-rest-auth==1.1.1 #https://github.com/jazzband/dj-rest-auth django-rest-authtoken==2.1.3 #https://pypi.org/project/django-rest-authtoken/ From 9fd6eb7275ea3a1c1556e28f1fc4800b22e56e53 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 23 Sep 2020 22:54:11 -0700 Subject: [PATCH 18/32] Removed unused import that was causing an error. --- project/userauth/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/project/userauth/serializers.py b/project/userauth/serializers.py index 6d2b8d25..1a556217 100644 --- a/project/userauth/serializers.py +++ b/project/userauth/serializers.py @@ -1,5 +1,4 @@ from rest_framework import serializers -from rest_framework_jwt.settings import api_settings from dj_rest_auth.serializers import JWTSerializer, UserDetailsSerializer from dj_rest_auth.registration.serializers import VerifyEmailSerializer from dj_rest_auth.serializers import PasswordResetSerializer, PasswordResetConfirmSerializer From 2670780aceb011c8250c1dcffc6d5ce9f56596d2 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 13:50:52 -0700 Subject: [PATCH 19/32] Removed excess space from email_confirmation_subject.txt filename. --- ...l _confirmation_subject.txt => email_confirmation_subject.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename project/core/templates/account/email/{email _confirmation_subject.txt => email_confirmation_subject.txt} (100%) diff --git a/project/core/templates/account/email/email _confirmation_subject.txt b/project/core/templates/account/email/email_confirmation_subject.txt similarity index 100% rename from project/core/templates/account/email/email _confirmation_subject.txt rename to project/core/templates/account/email/email_confirmation_subject.txt From 9451986b49ae45161d4707ca5340c61a9112cdc2 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 14:06:01 -0700 Subject: [PATCH 20/32] Revert "Removed excess space from email_confirmation_subject.txt filename." This reverts commit 2670780aceb011c8250c1dcffc6d5ce9f56596d2. This file rename breaks allauth and all auth test cases for some reason. It also causes a python failure due to an endless import recursion. Not sure why/how this came about, but the easiest thing is to leave it as-is, and revert the change. --- ...l_confirmation_subject.txt => email _confirmation_subject.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename project/core/templates/account/email/{email_confirmation_subject.txt => email _confirmation_subject.txt} (100%) diff --git a/project/core/templates/account/email/email_confirmation_subject.txt b/project/core/templates/account/email/email _confirmation_subject.txt similarity index 100% rename from project/core/templates/account/email/email_confirmation_subject.txt rename to project/core/templates/account/email/email _confirmation_subject.txt From aa6218ae62241284a4c8d14e85b3ee6839a0d133 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 14:31:41 -0700 Subject: [PATCH 21/32] Attempted to fix intermittent email verification post test failure by updating settings and tests. --- project/config/urls.py | 4 ++-- project/userauth/tests.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/config/urls.py b/project/config/urls.py index c52fd57b..ad3279e6 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -20,8 +20,8 @@ path(settings.ADMIN_URL, admin.site.urls), # User management - #currently inactive endpoint, but can re-activate if needed - #path("users/", include("users.urls", namespace="users")), + #currently an unused endpoint, but can be used if needed for extended user profiles, etc. + path("users/", include("users.urls", namespace="users")), #we have to include these for registration email validation, but otherwise these paths are NOT used path("accounts/", include("allauth.urls")), diff --git a/project/userauth/tests.py b/project/userauth/tests.py index be1ecb7b..40aeecc2 100644 --- a/project/userauth/tests.py +++ b/project/userauth/tests.py @@ -136,7 +136,7 @@ def test_verify_email_path_post(self): confirmation_uri = re.search(uri_regex, verify_email_message) # now, let's post the key to trigger validation - validate_email_url = f'/api/v1/auth/registration/verify-email/' + validate_email_url = f'/api/v1/auth/registration/verify-email/?key={confirmation_uri[3]}/' validate_key_data = {"key": confirmation_uri[3]} validation_response = self.client.post(validate_email_url, validate_key_data, format='json') print(validation_response) From f91a22d4fa0e53ec0f40e75a7bd747abdce9c7a2 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 16:04:47 -0700 Subject: [PATCH 22/32] Changed order of included URLs. --- project/config/urls.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/project/config/urls.py b/project/config/urls.py index ad3279e6..ce11e23b 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -6,6 +6,7 @@ from django.views import defaults as default_views from rest_framework import routers, serializers, viewsets from resources.urls import router as resources_router +from userauth.views import CustomVerifyEmailView from userauth.urls import router as userauth_router router = routers.DefaultRouter() @@ -23,15 +24,15 @@ #currently an unused endpoint, but can be used if needed for extended user profiles, etc. path("users/", include("users.urls", namespace="users")), - #we have to include these for registration email validation, but otherwise these paths are NOT used - path("accounts/", include("allauth.urls")), - # Your stuff: custom urls includes go here #this is a route for logging into the "browsable api" if not needed for testing, it should be omitted. path('api/v1/', include('rest_framework.urls', namespace='rest_framework')), path('api/v1/auth/', include(('userauth.urls', 'userauth'), namespace="userauth")), path('api/v1/', include('resources.urls', namespace='resources')), + #we have to include these for registration email validation, but otherwise these paths are NOT used + path("accounts/", include("allauth.urls")), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From a2b2b4bd8e0c97131f4053de60e45991eab88990 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 16:10:15 -0700 Subject: [PATCH 23/32] Added specific path for email link verification post and changed path regex. --- project/userauth/urls.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/userauth/urls.py b/project/userauth/urls.py index 19f71ae8..c9b45a50 100644 --- a/project/userauth/urls.py +++ b/project/userauth/urls.py @@ -15,8 +15,9 @@ urlpatterns = ( path('', include('dj_rest_auth.urls')), path('', include(router.urls)), - path('registration/', include('dj_rest_auth.registration.urls')), - path('registration/verify-email/', views.CustomVerifyEmailView.as_view(), name='account_email_verification_sent'), + path('registration/', include('dj_rest_auth.registration.urls'), name='registration'), + path('registration/verify-email/$', views.CustomVerifyEmailView.as_view(), name='account_email_verification_sent'), + path('registration/verify-email/(?P[-:\w]+)/$', views.CustomVerifyEmailView.as_view(), name='account_confirm_email'), path('password/reset/', PasswordResetView.as_view(), name='password_reset'), path('password/reset/confirm///', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), From 569ee99ec033f2e0ddb338ba9f6623a6661a0925 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 16:11:22 -0700 Subject: [PATCH 24/32] Fun trick: If your regex fails to capture all the key generations cases, it will fail intermittently - not consistantly. --- project/userauth/tests.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/project/userauth/tests.py b/project/userauth/tests.py index 40aeecc2..83dcdaea 100644 --- a/project/userauth/tests.py +++ b/project/userauth/tests.py @@ -118,25 +118,25 @@ def test_verify_email_path_get(self): def test_verify_email_path_post(self): # start by registering a user - reg_url = '/api/v1/auth/registration/' - reg_data = { + new_user_reg_url = '/api/v1/auth/registration/' + new_user_reg_data = { "username": self.user['username'], "email": self.user['email'], "password1": self.user['password1'], "password2": self.user['password2'] } - response = self.client.post(reg_url, reg_data, format='json') + response = self.client.post(new_user_reg_url, new_user_reg_data, format='json') # grab email from outbox so we can extract the verification link email_message = mail.outbox[0] verify_email_message = email_message.body # extracting what we need for the verification post action - uri_regex = re.compile(r"(\/api\/v1\/auth\/registration\/verify-email\/)(\?key=)([\w:]+)") + uri_regex = re.compile(r"(\/api\/v1\/auth\/registration\/verify-email\/)(\?key=)([\w:-]+)") confirmation_uri = re.search(uri_regex, verify_email_message) # now, let's post the key to trigger validation - validate_email_url = f'/api/v1/auth/registration/verify-email/?key={confirmation_uri[3]}/' + validate_email_url = f'{confirmation_uri[0]}' validate_key_data = {"key": confirmation_uri[3]} validation_response = self.client.post(validate_email_url, validate_key_data, format='json') print(validation_response) @@ -172,6 +172,7 @@ def test_verify_email_marked_valid_after_post(self): key_data = {"key": confirmation_uri[3]} response = self.client.post(email_url, key_data, format='json') + #did the post succeed in marking the email as valid in the DB? model = get_user_model() email_to_verify = EmailAddress.objects.get(email=reg_data['email']) From 35c5610ef3161789b5bd51d9d9fd2f43d9d2d072 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 16:12:29 -0700 Subject: [PATCH 25/32] Removed print statement from email post verificaton test case. --- project/userauth/tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/project/userauth/tests.py b/project/userauth/tests.py index 83dcdaea..d3b7371f 100644 --- a/project/userauth/tests.py +++ b/project/userauth/tests.py @@ -139,7 +139,6 @@ def test_verify_email_path_post(self): validate_email_url = f'{confirmation_uri[0]}' validate_key_data = {"key": confirmation_uri[3]} validation_response = self.client.post(validate_email_url, validate_key_data, format='json') - print(validation_response) # did the post result in the correct status messages? self.assertEqual(validation_response.status_code, status.HTTP_200_OK) From 75e9461c42a77aafc59fe806511135c6ca05c829 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 16:46:51 -0700 Subject: [PATCH 26/32] Corrected circular reference that was breaking tests and renamed file to exclude extra space. --- .../templates/account/email/email _confirmation_subject.txt | 1 - .../templates/account/email/email_confirmation_subject.txt | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 project/core/templates/account/email/email _confirmation_subject.txt create mode 100644 project/core/templates/account/email/email_confirmation_subject.txt diff --git a/project/core/templates/account/email/email _confirmation_subject.txt b/project/core/templates/account/email/email _confirmation_subject.txt deleted file mode 100644 index 4c85ebb9..00000000 --- a/project/core/templates/account/email/email _confirmation_subject.txt +++ /dev/null @@ -1 +0,0 @@ -{% include "account/email/email_confirmation_subject.txt" %} diff --git a/project/core/templates/account/email/email_confirmation_subject.txt b/project/core/templates/account/email/email_confirmation_subject.txt new file mode 100644 index 00000000..b0a876f5 --- /dev/null +++ b/project/core/templates/account/email/email_confirmation_subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}Please Confirm Your E-mail Address{% endblocktrans %} +{% endautoescape %} From b2ba2c2f4715668b94174cef1b18544a5d01fc6b Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 24 Sep 2020 16:53:21 -0700 Subject: [PATCH 27/32] Removed regex from path url to avoid djang deprecation and migration warnings. --- project/userauth/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/userauth/urls.py b/project/userauth/urls.py index c9b45a50..aa25e3e0 100644 --- a/project/userauth/urls.py +++ b/project/userauth/urls.py @@ -16,8 +16,8 @@ path('', include('dj_rest_auth.urls')), path('', include(router.urls)), path('registration/', include('dj_rest_auth.registration.urls'), name='registration'), - path('registration/verify-email/$', views.CustomVerifyEmailView.as_view(), name='account_email_verification_sent'), - path('registration/verify-email/(?P[-:\w]+)/$', views.CustomVerifyEmailView.as_view(), name='account_confirm_email'), + path('registration/verify-email/', views.CustomVerifyEmailView.as_view(), name='account_email_verification_sent'), + path('registration/verify-email/', views.CustomVerifyEmailView.as_view(), name='account_confirm_email'), path('password/reset/', PasswordResetView.as_view(), name='password_reset'), path('password/reset/confirm///', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), From 69342912db3e0e8764fd4e794b724958d1fd6872 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Fri, 25 Sep 2020 22:55:09 -0700 Subject: [PATCH 28/32] New stub files after running create app script for django. --- project/hangouts/__init__.py | 0 project/hangouts/admin.py | 3 +++ project/hangouts/apps.py | 5 +++++ project/hangouts/migrations/__init__.py | 0 project/hangouts/models.py | 3 +++ project/hangouts/tests.py | 3 +++ project/hangouts/views.py | 3 +++ 7 files changed, 17 insertions(+) create mode 100644 project/hangouts/__init__.py create mode 100644 project/hangouts/admin.py create mode 100644 project/hangouts/apps.py create mode 100644 project/hangouts/migrations/__init__.py create mode 100644 project/hangouts/models.py create mode 100644 project/hangouts/tests.py create mode 100644 project/hangouts/views.py diff --git a/project/hangouts/__init__.py b/project/hangouts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project/hangouts/admin.py b/project/hangouts/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/project/hangouts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/project/hangouts/apps.py b/project/hangouts/apps.py new file mode 100644 index 00000000..8c515da1 --- /dev/null +++ b/project/hangouts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class HangoutsConfig(AppConfig): + name = 'hangouts' diff --git a/project/hangouts/migrations/__init__.py b/project/hangouts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project/hangouts/models.py b/project/hangouts/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/project/hangouts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/project/hangouts/tests.py b/project/hangouts/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/project/hangouts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/project/hangouts/views.py b/project/hangouts/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/project/hangouts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 738f7c1d15a319af8d8a20bb90d850d9cfa949f2 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Sat, 26 Sep 2020 02:25:00 -0700 Subject: [PATCH 29/32] Registered Hangouts as an app. --- project/config/settings/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/config/settings/base.py b/project/config/settings/base.py index 4018957e..4ebc0620 100644 --- a/project/config/settings/base.py +++ b/project/config/settings/base.py @@ -92,7 +92,8 @@ "resources.apps.ResourcesConfig", "tagging.apps.TaggingConfig", 'userauth.apps.UserauthConfig', - 'osprojects.apps.OsprojectsConfig' + 'osprojects.apps.OsprojectsConfig', + 'hangouts.apps.HangoutsConfig' # Your stuff: custom apps go here ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps From 11c40e2c88db236e8c7eedf27965b680e04b35ab Mon Sep 17 00:00:00 2001 From: BethanyG Date: Sat, 26 Sep 2020 02:25:30 -0700 Subject: [PATCH 30/32] WIP for registering Hangouts in admin. Issues with properly inlining model. --- project/hangouts/admin.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/project/hangouts/admin.py b/project/hangouts/admin.py index 8c38f3f3..4caa48ca 100644 --- a/project/hangouts/admin.py +++ b/project/hangouts/admin.py @@ -1,3 +1,30 @@ from django.contrib import admin +from .models import Hangout, HangoutSessions, HangoutResponses # Register your models here. +class HangoutSessionsInline(admin.StackedInline): + model = HangoutSessions + readonly_fields = ('id', 'guid') + + +class HangoutResponsesInline(admin.StackedInline): + model = HangoutResponses + readonly_fields = ('id', 'guid') + + +class HangoutAdmin(admin.ModelAdmin): + model = Hangout + readonly_fields = ('id', 'guid') + list_display = ['tag_list'] + inlines = ['HangoutSessionsInline', 'HangoutResponsesInline'] + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('tags') + + def tag_list(self, obj): + return u", ".join(o.name for o in obj.tags.all()) + + +admin.site.register(Hangout) +admin.site.register(HangoutResponses) +admin.site.register(HangoutSessions) From 92b8944f7c3c1090e9dcdaca39a5d838183ecf57 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Sat, 26 Sep 2020 02:26:08 -0700 Subject: [PATCH 31/32] Inital migration file for hangouts, hangoutsessions, and hangoutresponses. --- project/hangouts/migrations/0001_initial.py | 76 +++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 project/hangouts/migrations/0001_initial.py diff --git a/project/hangouts/migrations/0001_initial.py b/project/hangouts/migrations/0001_initial.py new file mode 100644 index 00000000..d0645620 --- /dev/null +++ b/project/hangouts/migrations/0001_initial.py @@ -0,0 +1,76 @@ +# Generated by Django 2.2.4 on 2020-09-26 08:22 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import hangouts.models +import taggit.managers +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('tagging', '0003_auto_20200508_1230'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('resources', '0007_auto_20200303_1258'), + ] + + operations = [ + migrations.CreateModel( + name='Hangout', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('guid', models.UUIDField(default=uuid.uuid1, editable=False)), + ('status', models.CharField(blank=True, max_length=200)), + ('hangout_type', models.CharField(choices=[('WATCH', 'Watch Me Code'), ('PRES', 'Presentation'), ('COWRK', 'Co-work with Me'), ('STUDY', 'Study Group'), ('PAIR', 'Pairing'), ('ACNT', 'Keep Me Accountable'), ('DISC', 'Discussion'), ('TEACH', 'I have something to teach')], max_length=6)), + ('title', models.CharField(max_length=200)), + ('slug', models.SlugField(allow_unicode=True, max_length=100, verbose_name='Slug')), + ('short_description', models.TextField(max_length=300)), + ('long_description', models.TextField(blank=True, max_length=600, null=True)), + ('open_to_RSVP', models.BooleanField(default=False)), + ('start_time', models.DateTimeField(default=django.utils.timezone.now)), + ('end_time', models.DateTimeField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('recurring', models.BooleanField(default=False)), + ('internal_platform', models.BooleanField(default=True)), + ('external_platform_link', models.URLField(blank=True, max_length=300, null=True)), + ('related_resources', models.ManyToManyField(blank=True, related_name='related_hangouts', to='resources.Resource')), + ('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='tagging.TaggedItems', to='tagging.CustomTag', verbose_name='Tags')), + ('user', models.ForeignKey(on_delete=models.SET(hangouts.models.get_sentinel_user), to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='HangoutSessions', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('guid', models.UUIDField(default=uuid.uuid1, editable=False)), + ('status', models.CharField(blank=True, max_length=200)), + ('start_time', models.DateTimeField(default=django.utils.timezone.now)), + ('end_time', models.DateTimeField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(default=django.utils.timezone.now)), + ('hangout_id', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_sessions', to='hangouts.Hangout')), + ('related_resources', models.ManyToManyField(blank=True, related_name='related_hangout_sessions', to='resources.Resource')), + ], + ), + migrations.CreateModel( + name='HangoutResponses', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('guid', models.UUIDField(default=uuid.uuid1, editable=False)), + ('express_interest', models.BooleanField(default=False)), + ('request_to_join', models.BooleanField(default=False)), + ('rsvp', models.BooleanField(default=False)), + ('response_comment', models.TextField(blank=True, max_length=300, null=True)), + ('status', models.TextField(max_length=10)), + ('hangout_id', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_responses', to='hangouts.Hangout')), + ('hangout_session_id', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_session_responses', to='hangouts.HangoutSessions')), + ('user_id', models.ForeignKey(on_delete=models.SET(hangouts.models.get_sentinel_user), to=settings.AUTH_USER_MODEL)), + ], + ), + ] From afa7f2519503169cf25a842b9864bd801fbe126a Mon Sep 17 00:00:00 2001 From: BethanyG Date: Sat, 26 Sep 2020 02:26:27 -0700 Subject: [PATCH 32/32] First pass at hangouts model. --- project/hangouts/models.py | 98 +++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/project/hangouts/models.py b/project/hangouts/models.py index 71a83623..1db2a01b 100644 --- a/project/hangouts/models.py +++ b/project/hangouts/models.py @@ -1,3 +1,99 @@ +import uuid +import datetime +from taggit.managers import TaggableManager +from django.conf import settings from django.db import models +from django.utils import timezone +from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_lazy as _ +from resources.models import Resource +from tagging.managers import CustomTaggableManager +from tagging.models import CustomTag, TaggedItems + + +def get_sentinel_user(): + return get_user_model().objects.get_or_create(username='deleted')[0] + + +def get_tags_display(self): + return self.tags.values_list('name', flat=True) + + +class Hangout(models.Model): + HANGOUT_TYPES = [ + ('WATCH', 'Watch Me Code'), + ('PRES', 'Presentation'), + ('COWRK', 'Co-work with Me'), + ('STUDY', 'Study Group'), + ('PAIR', 'Pairing'), + ('ACNT', 'Keep Me Accountable'), + ('DISC', 'Discussion'), + ('TEACH', 'I have something to teach'), + ] + + guid = models.UUIDField(default=uuid.uuid1, editable=False) + + #One of scheduled, pending, rescheduled, stale, hold, closed, completed + status = models.CharField(blank=True, max_length=200) + hangout_type = models.CharField(max_length=6, choices=HANGOUT_TYPES) + + #we are going to require a title + title = models.CharField(max_length=200, blank=False) + slug = models.SlugField(verbose_name=_("Slug"), max_length=100, allow_unicode=True) + short_description = models.TextField(max_length=300, blank=False, null=False) + long_description = models.TextField(max_length=600, blank=True, null=True) + + # user who "owns" the hangout we'll pull this from their TOKEN + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET(get_sentinel_user)) + + #sort of a public/private thing and confirmed/not confirmed thing + open_to_RSVP = models.BooleanField(blank=False, null=False, default=False) + + #Calendar date + start time would be derived from the datetime object + start_time = models.DateTimeField(default=timezone.now) + + #Calendar date + end time would be derived from the datetime object + end_time = models.DateTimeField(blank=False, null=False) + + # creation date of hangout entry + created = models.DateTimeField(auto_now_add=True) + + # modification date of hangout entry + modified = models.DateTimeField(default=timezone.now) + + recurring = models.BooleanField(null=False, default=False) + + internal_platform = models.BooleanField(null=False, default=True) + external_platform_link = models.URLField(max_length=300, blank=True, null=True) + + related_resources = models.ManyToManyField(Resource, blank=True, related_name='related_hangouts') + + # Allow tags to be used across entities + # E.g. so we can create composite views showing all entities sharing a common tag + tags = TaggableManager(through=TaggedItems, manager=CustomTaggableManager, blank=True) + + +class HangoutSessions(models.Model): + guid = models.UUIDField(default=uuid.uuid1, editable=False) + hangout_id = models.ForeignKey(Hangout, on_delete=models.CASCADE, blank=True, null=True, related_name='related_sessions') + status = models.CharField(blank=True, max_length=200) #scheduled, pending, rescheduled, stale, hold, closed, completed + start_time = models.DateTimeField(default=timezone.now) + end_time = models.DateTimeField(blank=False, null=False) + related_resources = models.ManyToManyField(Resource, blank=True, related_name='related_hangout_sessions') + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(default=timezone.now) + + +class HangoutResponses(models.Model): + guid = models.UUIDField(default=uuid.uuid1, editable=False) + hangout_id = models.ForeignKey(Hangout, on_delete=models.CASCADE, blank=True, null=True, + related_name='related_responses') + hangout_session_id = models.ForeignKey(HangoutSessions, on_delete=models.CASCADE, blank=True, null=True, + related_name='related_session_responses') + user_id = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET(get_sentinel_user)) + express_interest = models.BooleanField(blank=False, null=False, default=False) + request_to_join = models.BooleanField(blank=False, null=False, default=False) + rsvp = models.BooleanField(blank=False, null=False, default=False) + response_comment = models.TextField(max_length=300, blank=True, null=True) + status = models.TextField(max_length=10, blank=False, null=False) -# Create your models here.