From a1b9f113530cd35ab2b5fc4bf649c2037908c179 Mon Sep 17 00:00:00 2001 From: Wayne Lambert Date: Mon, 11 Dec 2023 14:01:03 +0000 Subject: [PATCH] Runs Ruff formatting Addresses the following: - Runs Ruff formatting over the project - Updates type hints --- aa_project/tests/test_base.py | 102 ++++---- aa_project/tests/test_prod.py | 12 +- aa_project/urls.py | 59 +++-- aa_project/wsgi.py | 2 +- apps/api/apps.py | 2 +- apps/api/serializers.py | 115 +++++---- apps/api/tests/test_urls.py | 12 +- apps/api/tests/test_views.py | 36 +-- apps/api/urls.py | 10 +- apps/api/views.py | 9 +- apps/blog/apps.py | 2 +- apps/blog/feeds.py | 10 +- apps/blog/forms.py | 42 ++-- apps/blog/managers.py | 2 +- apps/blog/migrations/0001_initial.py | 82 ++++-- .../migrations/0002_auto_20200921_1523.py | 14 +- .../migrations/0003_auto_20200922_2116.py | 10 +- .../migrations/0004_auto_20200930_2049.py | 11 +- .../migrations/0005_auto_20200930_2054.py | 11 +- apps/blog/models.py | 36 ++- apps/blog/search.py | 4 +- apps/blog/sitemap.py | 8 +- apps/blog/templatetags/blog_tags.py | 6 +- apps/blog/tests/helpers.py | 12 +- apps/blog/tests/test_blog_tags.py | 10 +- apps/blog/tests/test_forms.py | 58 ++--- apps/blog/tests/test_models.py | 112 +++++---- apps/blog/tests/test_urls.py | 61 +++-- apps/blog/tests/test_views.py | 141 ++++++----- apps/blog/urls.py | 47 ++-- apps/blog/views.py | 138 +++++----- apps/conftest.py | 149 +++++------ apps/contacts/admin.py | 17 +- apps/contacts/apps.py | 2 +- apps/contacts/migrations/0001_initial.py | 23 +- apps/contacts/tests/conftest.py | 20 +- apps/contacts/tests/helpers.py | 17 +- apps/contacts/tests/test_forms.py | 24 +- apps/contacts/tests/test_models.py | 33 +-- apps/contacts/tests/test_urls.py | 9 +- apps/contacts/tests/test_views.py | 33 +-- apps/contacts/urls.py | 6 +- apps/contacts/views.py | 16 +- apps/countdown_letters/admin.py | 45 ++-- apps/countdown_letters/apps.py | 4 +- apps/countdown_letters/forms.py | 7 +- apps/countdown_letters/logic.py | 137 +++++----- .../migrations/0001_initial.py | 41 +-- apps/countdown_letters/models.py | 2 +- apps/countdown_letters/oxford_api.py | 5 +- apps/countdown_letters/tests/conftest.py | 236 ++++++++++++++---- apps/countdown_letters/tests/test_logic.py | 80 +++--- apps/countdown_letters/tests/test_models.py | 40 +-- apps/countdown_letters/tests/test_urls.py | 20 +- apps/countdown_letters/tests/test_utils.py | 22 +- .../tests/test_validations.py | 12 +- apps/countdown_letters/tests/test_views.py | 65 ++--- apps/countdown_letters/urls.py | 8 +- apps/countdown_letters/utils.py | 34 +-- apps/countdown_letters/views.py | 57 ++--- apps/countdown_numbers/admin.py | 42 ++-- apps/countdown_numbers/apps.py | 4 +- apps/countdown_numbers/forms.py | 9 +- apps/countdown_numbers/logic.py | 52 ++-- .../migrations/0001_initial.py | 35 +-- apps/countdown_numbers/models.py | 2 +- .../templatetags/template_helpers.py | 12 +- apps/countdown_numbers/tests/conftest.py | 2 +- apps/countdown_numbers/tests/test_logic.py | 92 +++---- apps/countdown_numbers/tests/test_models.py | 33 +-- .../tests/test_template_helpers.py | 27 +- apps/countdown_numbers/tests/test_urls.py | 20 +- .../tests/test_validations.py | 61 +++-- apps/countdown_numbers/tests/test_views.py | 78 +++--- apps/countdown_numbers/urls.py | 8 +- apps/countdown_numbers/utils.py | 18 +- apps/countdown_numbers/validations.py | 47 ++-- apps/countdown_numbers/views.py | 62 +++-- apps/cv/apps.py | 2 +- apps/cv/tests/test_urls.py | 3 +- apps/cv/tests/test_views.py | 6 +- apps/cv/urls.py | 4 +- apps/cv/views.py | 2 +- apps/helpers.py | 3 +- apps/pages/apps.py | 2 +- apps/pages/templatetags/ext_links.py | 48 ++-- apps/pages/tests/helpers.py | 2 +- apps/pages/tests/test_ext_links.py | 76 +++--- apps/pages/tests/test_urls.py | 95 +++---- apps/pages/tests/test_views.py | 204 ++++++++------- apps/pages/urls.py | 88 ++++--- apps/pages/views.py | 47 ++-- apps/roulette/apps.py | 2 +- apps/roulette/logging.py | 6 +- apps/roulette/logic.py | 64 ++--- apps/roulette/tests/helpers.py | 14 +- apps/roulette/tests/test_logging.py | 2 +- apps/roulette/tests/test_logic.py | 24 +- apps/roulette/tests/test_urls.py | 20 +- apps/roulette/tests/test_views.py | 20 +- apps/roulette/urls.py | 8 +- apps/roulette/views.py | 20 +- apps/scraping/apps.py | 2 +- apps/scraping/churchill.py | 9 +- apps/scraping/gettysburg.py | 8 +- apps/scraping/referendum.py | 82 +++--- apps/scraping/tests/test_churchill.py | 6 +- apps/scraping/tests/test_gettysburg.py | 6 +- apps/scraping/tests/test_referendum.py | 8 +- apps/scraping/tests/test_urls.py | 27 +- apps/scraping/tests/test_views.py | 6 +- apps/scraping/urls.py | 10 +- apps/scraping/views.py | 2 +- apps/text_analysis/apps.py | 2 +- apps/text_analysis/tests/conftest.py | 8 +- apps/text_analysis/tests/test_urls.py | 13 +- apps/text_analysis/tests/test_utils.py | 17 +- apps/text_analysis/tests/test_views.py | 24 +- apps/text_analysis/urls.py | 6 +- apps/text_analysis/utils.py | 6 +- apps/text_analysis/views.py | 14 +- apps/users/admin.py | 23 +- apps/users/apps.py | 2 +- apps/users/forms.py | 56 +++-- apps/users/migrations/0001_initial.py | 37 ++- .../migrations/0002_auto_20200929_1338.py | 19 +- .../migrations/0003_auto_20201001_2136.py | 11 +- .../0004_generate_email_token_model.py | 63 +++-- apps/users/mixins.py | 7 +- apps/users/models.py | 37 +-- apps/users/tests/test_forms.py | 188 +++++++------- apps/users/tests/test_mixins.py | 39 ++- apps/users/tests/test_models.py | 45 ++-- apps/users/tests/test_urls.py | 22 +- apps/users/tests/test_views.py | 93 +++---- apps/users/urls.py | 60 +++-- apps/users/utils.py | 4 +- apps/users/views.py | 222 ++++++++-------- conftest.py | 18 +- docker/prod/gunicorn/conf.py | 8 +- manage.py | 10 +- 141 files changed, 2728 insertions(+), 2198 deletions(-) diff --git a/aa_project/tests/test_base.py b/aa_project/tests/test_base.py index d21b1bf2..c2db2b5f 100644 --- a/aa_project/tests/test_base.py +++ b/aa_project/tests/test_base.py @@ -6,95 +6,103 @@ class TestLoginLogoutUTLs: - """ Asserts redirection URLS are set up as intended """ + """Asserts redirection URLS are set up as intended""" def test_login_url(self): - assert base.LOGIN_URL == 'blog:users:login' + assert base.LOGIN_URL == "blog:users:login" def test_login_redirect_url(self): - assert base.LOGIN_REDIRECT_URL == 'blog:home' + assert base.LOGIN_REDIRECT_URL == "blog:home" def test_logout_redirect_url(self): - assert base.LOGOUT_REDIRECT_URL == 'blog:home' + assert base.LOGOUT_REDIRECT_URL == "blog:home" class TestThirdPartyAppsAreInstalled: - """ Asserts that required third party apps are - correctly integrated within the project. """ + """Asserts that required third party apps are + correctly integrated within the project.""" def test_whitenoise_in_installed_apps(self): - assert 'whitenoise.runserver_nostatic' in base.INSTALLED_APPS + assert "whitenoise.runserver_nostatic" in base.INSTALLED_APPS def test_rest_framework_in_installed_apps(self): - assert 'rest_framework' in base.INSTALLED_APPS + assert "rest_framework" in base.INSTALLED_APPS def test_guardian_in_installed_apps(self): - assert 'guardian' in base.INSTALLED_APPS + assert "guardian" in base.INSTALLED_APPS def test_crispy_forms_in_installed_apps(self): - assert 'crispy_forms' in base.INSTALLED_APPS + assert "crispy_forms" in base.INSTALLED_APPS def test_crispy_bootstrap4_in_installed_apps(self): - assert 'crispy_bootstrap4' in base.INSTALLED_APPS + assert "crispy_bootstrap4" in base.INSTALLED_APPS def test_storages_in_installed_apps(self): - assert 'storages' in base.INSTALLED_APPS + assert "storages" in base.INSTALLED_APPS def test_django_recaptcha_in_installed_apps(self): - assert 'django_recaptcha' in base.INSTALLED_APPS + assert "django_recaptcha" in base.INSTALLED_APPS def test_widget_tweaks_in_installed_apps(self): - assert 'widget_tweaks' in base.INSTALLED_APPS + assert "widget_tweaks" in base.INSTALLED_APPS def test_tinymce_in_installed_apps(self): - assert 'tinymce' in base.INSTALLED_APPS + assert "tinymce" in base.INSTALLED_APPS class TestMiddlewareIsConfigured: def test_whitenoise_is_in_middleware_config(self): - assert 'whitenoise.middleware.WhiteNoiseMiddleware' in base.MIDDLEWARE + assert "whitenoise.middleware.WhiteNoiseMiddleware" in base.MIDDLEWARE class TestTemplatesAreConfigured: def test_template_backend_is_configured(self): - assert base.TEMPLATES[0]['BACKEND'] == 'django.template.backends.django.DjangoTemplates' + assert base.TEMPLATES[0]["BACKEND"] == "django.template.backends.django.DjangoTemplates" def test_template_directories_are_present(self): - temp_dirs = base.TEMPLATES[0]['DIRS'] - assert os.path.join(base.BASE_DIR, 'templates') in temp_dirs - assert os.path.join(base.BASE_DIR, 'aa_project/templates/') in temp_dirs - assert os.path.join(base.BASE_DIR, 'aa_project/templates/admin/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'blog/templates/blog/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'cv/templates/cv/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'contacts/templates/contacts/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'countdown_letters/templates/countdown_letters/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'countdown_numbers/templates/countdown_numbers/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'pages/templates/pages/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'roulette/templates/roulette/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'scraping/templates/scraping/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'text_analysis/templates/text_analysis/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'users/templates/users/') in temp_dirs - assert os.path.join(base.APPS_DIR, 'users/templates/registration/') in temp_dirs - - -@pytest.mark.skipif('GITHUB_RUN_ID' in os.environ, reason='Different DB credentials in GitHub Actions') + temp_dirs = base.TEMPLATES[0]["DIRS"] + assert os.path.join(base.BASE_DIR, "templates") in temp_dirs + assert os.path.join(base.BASE_DIR, "aa_project/templates/") in temp_dirs + assert os.path.join(base.BASE_DIR, "aa_project/templates/admin/") in temp_dirs + assert os.path.join(base.APPS_DIR, "blog/templates/blog/") in temp_dirs + assert os.path.join(base.APPS_DIR, "cv/templates/cv/") in temp_dirs + assert os.path.join(base.APPS_DIR, "contacts/templates/contacts/") in temp_dirs + assert ( + os.path.join(base.APPS_DIR, "countdown_letters/templates/countdown_letters/") + in temp_dirs + ) + assert ( + os.path.join(base.APPS_DIR, "countdown_numbers/templates/countdown_numbers/") + in temp_dirs + ) + assert os.path.join(base.APPS_DIR, "pages/templates/pages/") in temp_dirs + assert os.path.join(base.APPS_DIR, "roulette/templates/roulette/") in temp_dirs + assert os.path.join(base.APPS_DIR, "scraping/templates/scraping/") in temp_dirs + assert os.path.join(base.APPS_DIR, "text_analysis/templates/text_analysis/") in temp_dirs + assert os.path.join(base.APPS_DIR, "users/templates/users/") in temp_dirs + assert os.path.join(base.APPS_DIR, "users/templates/registration/") in temp_dirs + + +@pytest.mark.skipif( + "GITHUB_RUN_ID" in os.environ, reason="Different DB credentials in GitHub Actions" +) class TestDatabaseIsSecurelyConfigured: def test_secure_database_setup(self): - assert base.DATABASES['default']['NAME'] == os.environ['DB_NAME'] - assert base.DATABASES['default']['USER'] == os.environ['DB_USER'] - assert base.DATABASES['default']['PASSWORD'] == os.environ['DB_PASS'] - assert base.DATABASES['default']['HOST'] == os.environ['DB_DOCKER_POSTGRES_SERVICE'] - assert base.DATABASES['default']['PORT'] == os.environ['DB_PORT'] + assert base.DATABASES["default"]["NAME"] == os.environ["DB_NAME"] + assert base.DATABASES["default"]["USER"] == os.environ["DB_USER"] + assert base.DATABASES["default"]["PASSWORD"] == os.environ["DB_PASS"] + assert base.DATABASES["default"]["HOST"] == os.environ["DB_DOCKER_POSTGRES_SERVICE"] + assert base.DATABASES["default"]["PORT"] == os.environ["DB_PORT"] class TestEmailProviderConfigured: def test_amazon_ses_setup(self): - assert base.EMAIL_BACKEND == 'django_ses.SESBackend' + assert base.EMAIL_BACKEND == "django_ses.SESBackend" assert base.EMAIL_USE_TLS - assert base.EMAIL_HOST_SES == os.environ['EMAIL_HOST_SES'] - assert base.EMAIL_HOST_USER_SES == os.environ['EMAIL_HOST_USER_SES'] - assert base.EMAIL_HOST_PASSWORD_SES == os.environ['EMAIL_HOST_PASSWORD_SES'] - assert base.AWS_ACCESS_KEY_ID == os.environ['AWS_ACCESS_KEY_ID'] - assert base.AWS_SECRET_ACCESS_KEY == os.environ['AWS_SECRET_ACCESS_KEY'] + assert base.EMAIL_HOST_SES == os.environ["EMAIL_HOST_SES"] + assert base.EMAIL_HOST_USER_SES == os.environ["EMAIL_HOST_USER_SES"] + assert base.EMAIL_HOST_PASSWORD_SES == os.environ["EMAIL_HOST_PASSWORD_SES"] + assert base.AWS_ACCESS_KEY_ID == os.environ["AWS_ACCESS_KEY_ID"] + assert base.AWS_SECRET_ACCESS_KEY == os.environ["AWS_SECRET_ACCESS_KEY"] assert base.EMAIL_PORT == 587 - assert base.DEFAULT_FROM_EMAIL_SES == os.environ['DEFAULT_FROM_EMAIL_SES'] + assert base.DEFAULT_FROM_EMAIL_SES == os.environ["DEFAULT_FROM_EMAIL_SES"] diff --git a/aa_project/tests/test_prod.py b/aa_project/tests/test_prod.py index dbdd992f..ccf56ec2 100644 --- a/aa_project/tests/test_prod.py +++ b/aa_project/tests/test_prod.py @@ -3,9 +3,9 @@ class TestAllowedHostsConfigured: def test_allowed_hosts_have_required_hosts(self): - assert 'wl-portfolio.herokuapp.com' in prod.ALLOWED_HOSTS - assert 'waynelambert.dev' in prod.ALLOWED_HOSTS - assert 'www.waynelambert.dev' in prod.ALLOWED_HOSTS + assert "wl-portfolio.herokuapp.com" in prod.ALLOWED_HOSTS + assert "waynelambert.dev" in prod.ALLOWED_HOSTS + assert "www.waynelambert.dev" in prod.ALLOWED_HOSTS class TestPreDeploymentChecklistCompleted: @@ -15,9 +15,9 @@ def test_checklist_completed(self): assert prod.SECURE_SSL_REDIRECT assert prod.SESSION_COOKIE_SECURE assert prod.CSRF_COOKIE_SECURE - assert prod.X_FRAME_OPTIONS == 'DENY' + assert prod.X_FRAME_OPTIONS == "DENY" assert prod.SECURE_HSTS_SECONDS == 2592000 assert prod.SECURE_HSTS_INCLUDE_SUBDOMAINS assert prod.SECURE_HSTS_PRELOAD - assert prod.SECURE_PROXY_SSL_HEADER == ('HTTP_X_FORWARDED_PROTO', 'https') - assert prod.SECURE_REFERRER_POLICY == 'same-origin' + assert prod.SECURE_PROXY_SSL_HEADER == ("HTTP_X_FORWARDED_PROTO", "https") + assert prod.SECURE_REFERRER_POLICY == "same-origin" diff --git a/aa_project/urls.py b/aa_project/urls.py index 034ab695..6b240f2a 100644 --- a/aa_project/urls.py +++ b/aa_project/urls.py @@ -16,66 +16,71 @@ handler400 = BadRequestView.as_view() handler403 = PermissionDeniedView.as_view() handler404 = PageNotFoundView.as_view() -handler500 = 'apps.pages.views.handler500' +handler500 = "apps.pages.views.handler500" urlpatterns = [ - path('tinymce/', include('tinymce.urls')), - path('', include(tf_urls)), - path('', include('pages.urls', namespace='pages')), - path('blog/', include('blog.urls', namespace='blog')), - path('contact/', include('contacts.urls', namespace='contacts')), - path('cv/', include('cv.urls', namespace='cv')), - path('countdown-letters/', include('countdown_letters.urls', namespace='countdown_letters')), - path('countdown-numbers/', include('countdown_numbers.urls', namespace='countdown_numbers')), - path('text-analysis/', include('text_analysis.urls', namespace='text_analysis')), - path('roulette/', include('roulette.urls', namespace='roulette')), - path('scraping/', include('scraping.urls', namespace='scraping')), - path('api/', include('api.urls', namespace='api')), + path("tinymce/", include("tinymce.urls")), + path("", include(tf_urls)), + path("", include("pages.urls", namespace="pages")), + path("blog/", include("blog.urls", namespace="blog")), + path("contact/", include("contacts.urls", namespace="contacts")), + path("cv/", include("cv.urls", namespace="cv")), + path("countdown-letters/", include("countdown_letters.urls", namespace="countdown_letters")), + path("countdown-numbers/", include("countdown_numbers.urls", namespace="countdown_numbers")), + path("text-analysis/", include("text_analysis.urls", namespace="text_analysis")), + path("roulette/", include("roulette.urls", namespace="roulette")), + path("scraping/", include("scraping.urls", namespace="scraping")), + path("api/", include("api.urls", namespace="api")), ] # Admin Site URLs urlpatterns += [ path( - f'{DJANGO_ADMIN_LOGIN_PATH}/password_reset/', + f"{DJANGO_ADMIN_LOGIN_PATH}/password_reset/", auth_views.PasswordResetView.as_view(), - name='admin_password_reset' + name="admin_password_reset", ), path( - f'{DJANGO_ADMIN_LOGIN_PATH}/password_reset/done/', + f"{DJANGO_ADMIN_LOGIN_PATH}/password_reset/done/", auth_views.PasswordResetDoneView.as_view(), - name='password_reset_done' + name="password_reset_done", ), path( - 'reset///', + "reset///", auth_views.PasswordResetConfirmView.as_view(), - name='password_reset_confirm' + name="password_reset_confirm", ), path( - 'reset/done/', + "reset/done/", auth_views.PasswordResetCompleteView.as_view(), - name='password_reset_complete' + name="password_reset_complete", ), - path(f'{DJANGO_ADMIN_LOGIN_PATH}/', admin.site.urls), + path(f"{DJANGO_ADMIN_LOGIN_PATH}/", admin.site.urls), ] # Sitemap Config sitemaps = { - 'categories': CategorySitemap, - 'posts': PostSitemap, + "categories": CategorySitemap, + "posts": PostSitemap, } urlpatterns += [ - path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, - name='django.contrib.sitemaps.views.sitemap'), + path( + "sitemap.xml", + sitemap, + {"sitemaps": sitemaps}, + name="django.contrib.sitemaps.views.sitemap", + ), ] # Django Debug Toolbar Config if settings.DEBUG: import debug_toolbar + urlpatterns += [ - path('__debug__/', include(debug_toolbar.urls)), + path("__debug__/", include(debug_toolbar.urls)), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/aa_project/wsgi.py b/aa_project/wsgi.py index d76dbc8b..a42166a5 100644 --- a/aa_project/wsgi.py +++ b/aa_project/wsgi.py @@ -3,6 +3,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', os.environ['DJANGO_SETTINGS_MODULE']) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.environ["DJANGO_SETTINGS_MODULE"]) application = get_wsgi_application() diff --git a/apps/api/apps.py b/apps/api/apps.py index 2f5d0eb7..f53d3be2 100644 --- a/apps/api/apps.py +++ b/apps/api/apps.py @@ -2,4 +2,4 @@ class ApiConfig(AppConfig): - name = 'apps.api' + name = "apps.api" diff --git a/apps/api/serializers.py b/apps/api/serializers.py index 48f0a803..b1634077 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -12,58 +12,56 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() fields = ( - 'username', - 'first_name', - 'last_name', + "username", + "first_name", + "last_name", ) class ProfileSerializer(serializers.ModelSerializer): - full_name = serializers.Field() class Meta: model = Profile fields = ( - 'user', - 'slug', - 'author_view', - 'profile_picture', - 'full_name', + "user", + "slug", + "author_view", + "profile_picture", + "full_name", ) class CategorySerializer(serializers.ModelSerializer): - class Meta: model = Category fields = ( - 'id', - 'name', - 'slug', - 'created_date', + "id", + "name", + "slug", + "created_date", ) - ordering = ['name'] + ordering = ["name"] class PostSerializer(serializers.ModelSerializer): - status = serializers.CharField(source='get_status_display') + status = serializers.CharField(source="get_status_display") word_count = serializers.IntegerField() reading_time = serializers.IntegerField() - post_absolute_url = serializers.URLField(source='get_absolute_url') - - author_username = serializers.CharField(source='author.get_username') - author_first_name = serializers.CharField(source='author.first_name') - author_last_name = serializers.CharField(source='author.last_name') - author_full_name = serializers.CharField(source='author.get_full_name') - author_initials = serializers.CharField(source='author.profile.initials') - author_display_name = serializers.CharField(source='author.profile.display_name') - author_join_year = serializers.IntegerField(source='author.profile.join_year') - author_view = serializers.IntegerField(source='author.profile.author_view') - author_created_date = serializers.DateTimeField(source='author.profile.created_date') - author_updated_date = serializers.DateTimeField(source='author.profile.updated_date') - author_absolute_url = serializers.URLField(source='author.profile.get_absolute_url') - author_profile_picture = serializers.ImageField(source='author.profile.profile_picture') + post_absolute_url = serializers.URLField(source="get_absolute_url") + + author_username = serializers.CharField(source="author.get_username") + author_first_name = serializers.CharField(source="author.first_name") + author_last_name = serializers.CharField(source="author.last_name") + author_full_name = serializers.CharField(source="author.get_full_name") + author_initials = serializers.CharField(source="author.profile.initials") + author_display_name = serializers.CharField(source="author.profile.display_name") + author_join_year = serializers.IntegerField(source="author.profile.join_year") + author_view = serializers.IntegerField(source="author.profile.author_view") + author_created_date = serializers.DateTimeField(source="author.profile.created_date") + author_updated_date = serializers.DateTimeField(source="author.profile.updated_date") + author_absolute_url = serializers.URLField(source="author.profile.get_absolute_url") + author_profile_picture = serializers.ImageField(source="author.profile.profile_picture") categories = CategorySerializer(many=True, read_only=True) @@ -71,38 +69,39 @@ class Meta: model = Post fields = ( # Post fields - 'id', - 'title', - 'slug', - 'content', - 'reference_url', - 'publish_date', - 'updated_date', - 'image', - 'status', - 'word_count', - 'reading_time', - 'post_absolute_url', - + "id", + "title", + "slug", + "content", + "reference_url", + "publish_date", + "updated_date", + "image", + "status", + "word_count", + "reading_time", + "post_absolute_url", # Author fields - 'author_username', - 'author_first_name', - 'author_last_name', - 'author_full_name', - 'author_initials', - 'author_display_name', - 'author_join_year', - 'author_view', - 'author_created_date', - 'author_updated_date', - 'author_absolute_url', - 'author_profile_picture', - + "author_username", + "author_first_name", + "author_last_name", + "author_full_name", + "author_initials", + "author_display_name", + "author_join_year", + "author_view", + "author_created_date", + "author_updated_date", + "author_absolute_url", + "author_profile_picture", # Category fields - 'categories', + "categories", ) def get_status(self, obj): return obj.get_status_display() # pragma: no cover - ordering = ('-updated_date', '-publish_date', ) + ordering = ( + "-updated_date", + "-publish_date", + ) diff --git a/apps/api/tests/test_urls.py b/apps/api/tests/test_urls.py index 4341d235..cba3b87a 100644 --- a/apps/api/tests/test_urls.py +++ b/apps/api/tests/test_urls.py @@ -5,16 +5,16 @@ class TestURLs: def test_category_list_api_url(self): - """ Verify that the `/api/blog/categories/` url invokes intended view """ - path = reverse('api:blog_categories') + """Verify that the `/api/blog/categories/` url invokes intended view""" + path = reverse("api:blog_categories") assert path, CategoryListAPIView.as_view().__name__ def test_post_list_api_url(self): - """ Verify that the `/api/blog/posts/` url invokes intended view """ - path = reverse('api:posts') + """Verify that the `/api/blog/posts/` url invokes intended view""" + path = reverse("api:posts") assert path, PostListAPIView.as_view().__name__ def test_post_detail_api_url(self): - """ Verify that the `/api/blog/posts/3/` url invokes intended view """ - path = reverse('api:post_detail', kwargs={'pk': 3}) + """Verify that the `/api/blog/posts/3/` url invokes intended view""" + path = reverse("api:post_detail", kwargs={"pk": 3}) assert path, PostDetailAPIView.as_view().__name__ diff --git a/apps/api/tests/test_views.py b/apps/api/tests/test_views.py index 0713828b..e2300276 100644 --- a/apps/api/tests/test_views.py +++ b/apps/api/tests/test_views.py @@ -10,53 +10,53 @@ class TestCategoryListAPIView: def test_site_visitor_can_access_category_list_api(self, request, rf, unauth_user): - """ Asserts a random visitor can access the category list API """ - url = reverse('api:blog_categories') + """Asserts a random visitor can access the category list API""" + url = reverse("api:blog_categories") request = rf.get(url) request.user = unauth_user response = CategoryListAPIView.as_view()(request) - assert response.status_code == 200, 'Should return an OK status code' + assert response.status_code == 200, "Should return an OK status code" def test_auth_user_can_access_category_list_api(self, request, rf, auth_user): - """ Asserts an authenticated user can access the category list API """ - url = reverse('api:blog_categories') + """Asserts an authenticated user can access the category list API""" + url = reverse("api:blog_categories") request = rf.get(url) request.user = auth_user response = CategoryListAPIView.as_view()(request) - assert response.status_code == 200, 'Should return an OK status code' + assert response.status_code == 200, "Should return an OK status code" class TestPostListAPIView: def test_site_visitor_can_access_post_list_api(self, request, rf, unauth_user): - """ Asserts a random visitor can access the post list API """ - url = reverse('api:posts') + """Asserts a random visitor can access the post list API""" + url = reverse("api:posts") request = rf.get(url) request.user = unauth_user response = PostListAPIView.as_view()(request) - assert response.status_code == 200, 'Should return an OK status code' + assert response.status_code == 200, "Should return an OK status code" def test_auth_user_can_access_post_list_api(self, request, rf, auth_user): - """ Asserts an authenticated user can access the post list API """ - url = reverse('api:posts') + """Asserts an authenticated user can access the post list API""" + url = reverse("api:posts") request = rf.get(url) request.user = auth_user response = PostListAPIView.as_view()(request) - assert response.status_code == 200, 'Should return an OK status code' + assert response.status_code == 200, "Should return an OK status code" class TestPostDetailAPIView: def test_site_visitor_can_access_post_detail_api(self, request, rf, unauth_user, pub_post): - """ Asserts a random visitor can access the post detail API """ - url = reverse('api:post_detail', kwargs={'pk': pub_post.id}) + """Asserts a random visitor can access the post detail API""" + url = reverse("api:post_detail", kwargs={"pk": pub_post.id}) request = rf.get(url) request.user = unauth_user response = PostDetailAPIView.as_view()(request, pk=pub_post.id) - assert response.status_code == 200, 'Should return an OK status code' + assert response.status_code == 200, "Should return an OK status code" def test_auth_user_can_access_post_detail_api(self, request, rf, auth_user, pub_post): - """ Asserts an authenticated user can access the post detail API """ - url = reverse('api:post_detail', kwargs={'pk': pub_post.id}) + """Asserts an authenticated user can access the post detail API""" + url = reverse("api:post_detail", kwargs={"pk": pub_post.id}) request = rf.get(url) request.user = auth_user response = PostDetailAPIView.as_view()(request, pk=pub_post.id) - assert response.status_code == 200, 'Should return an OK status code' + assert response.status_code == 200, "Should return an OK status code" diff --git a/apps/api/urls.py b/apps/api/urls.py index bda3844d..bbce32c8 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,11 +3,11 @@ from apps.api.views import CategoryListAPIView, PostDetailAPIView, PostListAPIView -app_name = 'api' +app_name = "api" urlpatterns = [ - path('api-auth/', include('rest_framework.urls')), - path('blog/posts/', PostListAPIView.as_view(), name='posts'), - path('blog/posts//', PostDetailAPIView.as_view(), name='post_detail'), - path('blog/categories/', CategoryListAPIView.as_view(), name='blog_categories'), + path("api-auth/", include("rest_framework.urls")), + path("blog/posts/", PostListAPIView.as_view(), name="posts"), + path("blog/posts//", PostDetailAPIView.as_view(), name="post_detail"), + path("blog/categories/", CategoryListAPIView.as_view(), name="blog_categories"), ] diff --git a/apps/api/views.py b/apps/api/views.py index 87617302..a38655b7 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -11,10 +11,11 @@ class CategoryListAPIView(ListAPIView): The configured endpoint purposefully does not permit a POST method as I do not want others creating new categories in the database. """ + name = "Category List API" - queryset = Category.objects.all().prefetch_related('posts') + queryset = Category.objects.all().prefetch_related("posts") serializer_class = CategorySerializer - lookup_fields = ('name') + lookup_fields = "name" class PostListAPIView(ListAPIView): @@ -25,10 +26,11 @@ class PostListAPIView(ListAPIView): The configured endpoint purposefully does not permit a POST method as I do not want others creating new posts in the database. """ + name = "Post List API" queryset = Post.published.all() serializer_class = PostSerializer - lookup_fields = ('title') + lookup_fields = "title" class PostDetailAPIView(RetrieveAPIView): @@ -38,6 +40,7 @@ class PostDetailAPIView(RetrieveAPIView): The configured endpoint uses a RetrieveAPIView to limit to read-only access. I do not others creating new posts in the database. """ + name = "Post Detail API" queryset = Post.published.all() serializer_class = PostSerializer diff --git a/apps/blog/apps.py b/apps/blog/apps.py index 95606066..d0fbcb9c 100644 --- a/apps/blog/apps.py +++ b/apps/blog/apps.py @@ -2,4 +2,4 @@ class BlogConfig(AppConfig): - name = 'apps.blog' + name = "apps.blog" diff --git a/apps/blog/feeds.py b/apps/blog/feeds.py index bf1b0648..462f3d6c 100644 --- a/apps/blog/feeds.py +++ b/apps/blog/feeds.py @@ -29,11 +29,11 @@ def item_updateddate(self, item): return item.updated_date def item_enclosures(self, item): - return [Enclosure( - item.image.url, - str(item.image.size), - f"image/{item.image.name.split('.')[-1]}" - )] + return [ + Enclosure( + item.image.url, str(item.image.size), f"image/{item.image.name.split('.')[-1]}" + ) + ] class AtomLatestPostsFeed(LatestPostsFeed): diff --git a/apps/blog/forms.py b/apps/blog/forms.py index e11808ac..6367fb27 100644 --- a/apps/blog/forms.py +++ b/apps/blog/forms.py @@ -4,42 +4,44 @@ class PostForm(forms.ModelForm): - class Meta: model = Post - fields = ('title', 'content', 'categories', 'reference_url', 'status', 'image') + fields = ("title", "content", "categories", "reference_url", "status", "image") widgets = { - 'status': forms.RadioSelect(attrs={ - 'class': 'custom-control-inline'}), - 'title': forms.TextInput(attrs={ - 'id': 'id_post_title', - 'onkeyup': 'character_count()', - 'class': 'col-sm-8', - 'placeholder': 'Post Title...' - }) + "status": forms.RadioSelect(attrs={"class": "custom-control-inline"}), + "title": forms.TextInput( + attrs={ + "id": "id_post_title", + "onkeyup": "character_count()", + "class": "col-sm-8", + "placeholder": "Post Title...", + } + ), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def clean_title(self): - starting_title = self.cleaned_data['title'] - words = starting_title.split(' ') - capitalized_exceptions = ('why', 'how', 'tip') - uncapitalized_exceptions = ('with', 'them', 'your') + starting_title = self.cleaned_data["title"] + words = starting_title.split(" ") + capitalized_exceptions = ("why", "how", "tip") + uncapitalized_exceptions = ("with", "them", "your") updated_words = [] for word in words: if not word.isupper(): - if (len(word.strip()) >= 4 or word in capitalized_exceptions) and \ - word not in uncapitalized_exceptions: + if ( + len(word.strip()) >= 4 or word in capitalized_exceptions + ) and word not in uncapitalized_exceptions: updated_words.append(word.title()) - elif (len(word.strip()) < 4 or word in uncapitalized_exceptions) and \ - word not in capitalized_exceptions: + elif ( + len(word.strip()) < 4 or word in uncapitalized_exceptions + ) and word not in capitalized_exceptions: updated_words.append(word.lower()) else: updated_words.append(word) - new_title = ' '.join(updated_words).replace("&", "and").strip() + new_title = " ".join(updated_words).replace("&", "and").strip() if new_title[0].islower(): new_title = f"{new_title[0].capitalize()}{new_title[1:]}" - return new_title[:-1] if new_title.endswith('.') else new_title + return new_title[:-1] if new_title.endswith(".") else new_title diff --git a/apps/blog/managers.py b/apps/blog/managers.py index da356364..3f1f231a 100644 --- a/apps/blog/managers.py +++ b/apps/blog/managers.py @@ -4,4 +4,4 @@ class PublishedManager(models.Manager): def get_queryset(self): qs = super().get_queryset().filter(status=1) - return qs.prefetch_related('categories').select_related('author__profile') + return qs.prefetch_related("categories").select_related("author__profile") diff --git a/apps/blog/migrations/0001_initial.py b/apps/blog/migrations/0001_initial.py index 8bc27582..2a73a6c8 100644 --- a/apps/blog/migrations/0001_initial.py +++ b/apps/blog/migrations/0001_initial.py @@ -8,7 +8,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -17,36 +16,75 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=16)), - ('slug', models.SlugField(max_length=16, unique=True)), - ('created_date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=16)), + ("slug", models.SlugField(max_length=16, unique=True)), + ("created_date", models.DateTimeField(auto_now_add=True)), ], options={ - 'verbose_name': 'category', - 'verbose_name_plural': 'categories', - 'ordering': ['name'], + "verbose_name": "category", + "verbose_name_plural": "categories", + "ordering": ["name"], }, ), migrations.CreateModel( - name='Post', + name="Post", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(help_text='The length of the post must be between 40 and 60 characters', max_length=60, validators=[django.core.validators.MinLengthValidator(40)])), - ('slug', models.SlugField(max_length=60, unique=True)), - ('content', tinymce.models.HTMLField()), - ('reference_url', models.URLField(blank=True)), - ('publish_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('image', models.ImageField(default='default-post.jpg', help_text='For bests results, use an image that is 1,200px wide x 600px high', max_length=200, upload_to='post_images')), - ('status', models.IntegerField(choices=[(0, 'Draft'), (1, 'Publish')], default=0)), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='author', to=settings.AUTH_USER_MODEL)), - ('categories', models.ManyToManyField(help_text='Select more than one category by holding down Ctrl or Cmd key', related_name='posts', to='blog.Category')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "title", + models.CharField( + help_text="The length of the post must be between 40 and 60 characters", + max_length=60, + validators=[django.core.validators.MinLengthValidator(40)], + ), + ), + ("slug", models.SlugField(max_length=60, unique=True)), + ("content", tinymce.models.HTMLField()), + ("reference_url", models.URLField(blank=True)), + ("publish_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "image", + models.ImageField( + default="default-post.jpg", + help_text="For bests results, use an image that is 1,200px wide x 600px high", + max_length=200, + upload_to="post_images", + ), + ), + ("status", models.IntegerField(choices=[(0, "Draft"), (1, "Publish")], default=0)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "categories", + models.ManyToManyField( + help_text="Select more than one category by holding down Ctrl or Cmd key", + related_name="posts", + to="blog.Category", + ), + ), ], options={ - 'ordering': ['-updated_date', '-publish_date'], + "ordering": ["-updated_date", "-publish_date"], }, ), ] diff --git a/apps/blog/migrations/0002_auto_20200921_1523.py b/apps/blog/migrations/0002_auto_20200921_1523.py index 15b7d5b2..ed2bf916 100644 --- a/apps/blog/migrations/0002_auto_20200921_1523.py +++ b/apps/blog/migrations/0002_auto_20200921_1523.py @@ -4,15 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('blog', '0001_initial'), + ("blog", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='post', - name='image', - field=models.ImageField(default='post_images/algorithm1.jpeg', help_text='For bests results, use an image that is 1,200px wide x 600px high', max_length=200, upload_to='post_images'), + model_name="post", + name="image", + field=models.ImageField( + default="post_images/algorithm1.jpeg", + help_text="For bests results, use an image that is 1,200px wide x 600px high", + max_length=200, + upload_to="post_images", + ), ), ] diff --git a/apps/blog/migrations/0003_auto_20200922_2116.py b/apps/blog/migrations/0003_auto_20200922_2116.py index b947bb9d..affe29c0 100644 --- a/apps/blog/migrations/0003_auto_20200922_2116.py +++ b/apps/blog/migrations/0003_auto_20200922_2116.py @@ -4,14 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('blog', '0002_auto_20200921_1523'), + ("blog", "0002_auto_20200921_1523"), ] operations = [ migrations.AlterModelOptions( - name='post', - options={'default_manager_name': 'objects', 'ordering': ['-updated_date', '-publish_date']}, + name="post", + options={ + "default_manager_name": "objects", + "ordering": ["-updated_date", "-publish_date"], + }, ), ] diff --git a/apps/blog/migrations/0004_auto_20200930_2049.py b/apps/blog/migrations/0004_auto_20200930_2049.py index 449a31e6..f6c9d119 100644 --- a/apps/blog/migrations/0004_auto_20200930_2049.py +++ b/apps/blog/migrations/0004_auto_20200930_2049.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('blog', '0003_auto_20200922_2116'), + ("blog", "0003_auto_20200922_2116"), ] operations = [ migrations.AlterField( - model_name='post', - name='publish_date', + model_name="post", + name="publish_date", field=models.DateTimeField(auto_now_add=True, db_index=True), ), migrations.AlterField( - model_name='post', - name='updated_date', + model_name="post", + name="updated_date", field=models.DateTimeField(auto_now=True, db_index=True), ), ] diff --git a/apps/blog/migrations/0005_auto_20200930_2054.py b/apps/blog/migrations/0005_auto_20200930_2054.py index c4fb3d48..dbaef463 100644 --- a/apps/blog/migrations/0005_auto_20200930_2054.py +++ b/apps/blog/migrations/0005_auto_20200930_2054.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('blog', '0004_auto_20200930_2049'), + ("blog", "0004_auto_20200930_2049"), ] operations = [ migrations.AlterField( - model_name='post', - name='status', - field=models.IntegerField(choices=[(0, 'Draft'), (1, 'Publish')], db_index=True, default=0), + model_name="post", + name="status", + field=models.IntegerField( + choices=[(0, "Draft"), (1, "Publish")], db_index=True, default=0 + ), ), ] diff --git a/apps/blog/models.py b/apps/blog/models.py index e4d07985..152d74e5 100644 --- a/apps/blog/models.py +++ b/apps/blog/models.py @@ -18,27 +18,24 @@ class Category(models.Model): created_date = models.DateTimeField(auto_now_add=True) class Meta: - app_label = 'blog' - verbose_name = 'category' - verbose_name_plural = 'categories' - ordering = ['name'] + app_label = "blog" + verbose_name = "category" + verbose_name_plural = "categories" + ordering = ["name"] def __str__(self): return str(self.name) def get_absolute_url(self): - return reverse('blog:category_posts', kwargs={'slug': self.slug}) + return reverse("blog:category_posts", kwargs={"slug": self.slug}) class Post(models.Model): - STATUS = ( - (0, 'Draft'), - (1, 'Publish') - ) + STATUS = ((0, "Draft"), (1, "Publish")) title = models.CharField( max_length=60, validators=[MinLengthValidator(40)], - help_text='The length of the post must be between 40 and 60 characters' + help_text="The length of the post must be between 40 and 60 characters", ) slug = models.SlugField(max_length=60, unique=True) content = HTMLField() @@ -46,18 +43,17 @@ class Post(models.Model): publish_date = models.DateTimeField(db_index=True, auto_now_add=True, editable=False) updated_date = models.DateTimeField(db_index=True, auto_now=True) image = models.ImageField( - default='post_images/algorithm1.jpeg', - upload_to='post_images', + default="post_images/algorithm1.jpeg", + upload_to="post_images", max_length=200, - help_text='For bests results, use an image that is 1,200px wide x 600px high', + help_text="For bests results, use an image that is 1,200px wide x 600px high", ) status = models.IntegerField(db_index=True, choices=STATUS, default=0) - author = models.ForeignKey( - get_user_model(), related_name='author', on_delete=models.CASCADE) + author = models.ForeignKey(get_user_model(), related_name="author", on_delete=models.CASCADE) categories = models.ManyToManyField( Category, - related_name='posts', - help_text='Select more than one category by holding down Ctrl or Cmd key' + related_name="posts", + help_text="Select more than one category by holding down Ctrl or Cmd key", ) # Model Managers @@ -65,8 +61,8 @@ class Post(models.Model): published = PublishedManager() class Meta: - ordering = ['-updated_date', '-publish_date'] - default_manager_name = 'objects' + ordering = ["-updated_date", "-publish_date"] + default_manager_name = "objects" def __str__(self): return str(self.title) @@ -107,7 +103,7 @@ def save(self): super(Post, self).save() def get_absolute_url(self): - return reverse('blog:post_detail', kwargs={'slug': self.slug}) + return reverse("blog:post_detail", kwargs={"slug": self.slug}) def get_excerpt(self, char): return self.content[:char] diff --git a/apps/blog/search.py b/apps/blog/search.py index 49d0ea06..d9f76ca0 100644 --- a/apps/blog/search.py +++ b/apps/blog/search.py @@ -2,6 +2,6 @@ def cleanup_string(q: str) -> str: - """ Performs initial cleanup removing superfluous characters """ + """Performs initial cleanup removing superfluous characters""" pattern = '[^a-zA-Z0-9" ]+' - return re.sub(pattern, '', q).strip().casefold() + return re.sub(pattern, "", q).strip().casefold() diff --git a/apps/blog/sitemap.py b/apps/blog/sitemap.py index 56e9fda5..f6318e5c 100644 --- a/apps/blog/sitemap.py +++ b/apps/blog/sitemap.py @@ -4,19 +4,19 @@ class CategorySitemap(Sitemap): - changefreq = 'weekly' + changefreq = "weekly" priority = 0.5 def items(self): - return Category.objects.all().prefetch_related('posts') + return Category.objects.all().prefetch_related("posts") class PostSitemap(Sitemap): - changefreq = 'daily' + changefreq = "daily" priority = 0.9 def items(self): - return Post.published.all().order_by('-publish_date') + return Post.published.all().order_by("-publish_date") def lastmod(self, obj): return obj.updated_date diff --git a/apps/blog/templatetags/blog_tags.py b/apps/blog/templatetags/blog_tags.py index 3e770405..b0a439d8 100644 --- a/apps/blog/templatetags/blog_tags.py +++ b/apps/blog/templatetags/blog_tags.py @@ -6,7 +6,7 @@ register = template.Library() -@register.inclusion_tag('blog/sidebar.html') +@register.inclusion_tag("blog/sidebar.html") def category_sidebar(): - blog_categories = Category.objects.all().order_by('name') - return {'blog_categories': blog_categories} + blog_categories = Category.objects.all().order_by("name") + return {"blog_categories": blog_categories} diff --git a/apps/blog/tests/helpers.py b/apps/blog/tests/helpers.py index f15bf64b..0dec28e6 100644 --- a/apps/blog/tests/helpers.py +++ b/apps/blog/tests/helpers.py @@ -2,12 +2,14 @@ import os +from typing import List + from aa_project.settings.base import APPS_DIR -def get_search_strings() -> list: - """ Returns list of search strings to test search functionality """ - search_strings_file = os.path.join(APPS_DIR, 'blog/tests/search_strings.txt') - with open(search_strings_file, 'r') as f: - words = [word.strip('\n') for word in f] +def get_search_strings() -> List: + """Compiles search strings to test search functionality""" + search_strings_file = os.path.join(APPS_DIR, "blog/tests/search_strings.txt") + with open(search_strings_file, "r") as f: + words = [word.strip("\n") for word in f] return words diff --git a/apps/blog/tests/test_blog_tags.py b/apps/blog/tests/test_blog_tags.py index 237049a7..cd9a0e61 100644 --- a/apps/blog/tests/test_blog_tags.py +++ b/apps/blog/tests/test_blog_tags.py @@ -11,10 +11,10 @@ class TestCategorySidebar: def test_category_sidebar(self): - """ Asserts blog_categories context contains all categories """ + """Asserts blog_categories context contains all categories""" categories = mixer.cycle(10).blend(Category) - assert categories[9].pk == 10, '10th instance should have a PK of 10' - assert Category.objects.count() == 10, 'Should have 10 objects in the database' + assert categories[9].pk == 10, "10th instance should have a PK of 10" + assert Category.objects.count() == 10, "Should have 10 objects in the database" blog_categories = category_sidebar() - qs = blog_categories['blog_categories'] - assert qs.count() == 10, 'Queryset should contain 10 categories' + qs = blog_categories["blog_categories"] + assert qs.count() == 10, "Queryset should contain 10 categories" diff --git a/apps/blog/tests/test_forms.py b/apps/blog/tests/test_forms.py index c02ce1eb..85676444 100644 --- a/apps/blog/tests/test_forms.py +++ b/apps/blog/tests/test_forms.py @@ -5,71 +5,71 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) -class TestPostForm: +class TestPostForm: def test_form_tests_for_all_fields(self, sample_post_data): - """ Checks all fields requiring testing are in the form """ + """Checks all fields requiring testing are in the form""" form = PostForm() for field in sample_post_data.keys(): - if field != 'validity': + if field != "validity": assert field in form.fields def test_date_fields_not_in_form(self): form = PostForm() - assert 'publish_date' not in form.fields - assert 'updated_date' not in form.fields + assert "publish_date" not in form.fields + assert "updated_date" not in form.fields def test_empty_form_is_invalid(self): form = PostForm(data={}) - assert not form.is_valid(), 'Should be invalid' + assert not form.is_valid(), "Should be invalid" def test_form_title_requires_min_num_chars(self, sample_post_data): form = PostForm(data=sample_post_data) - form.data['title'] = 'a sample title' - error_returned = form.errors['title'][0] - assert error_returned.startswith('Ensure this value has at least 40 characters (it has ') + form.data["title"] = "a sample title" + error_returned = form.errors["title"][0] + assert error_returned.startswith("Ensure this value has at least 40 characters (it has ") def test_form_title_is_cleaned(self, sample_post_data): form = PostForm(data=sample_post_data) - form.data['title'] = 'why and how IMO salt & vinegar crisps with cheese are best. ' + form.data["title"] = "why and how IMO salt & vinegar crisps with cheese are best. " form.save(commit=False) - title = form.cleaned_data['title'] - assert title == 'Why and How IMO Salt and Vinegar Crisps with Cheese are Best' + title = form.cleaned_data["title"] + assert title == "Why and How IMO Salt and Vinegar Crisps with Cheese are Best" def test_title_field_contains_help_text(self, sample_post_data): form = PostForm(data=sample_post_data) - help_text = form.fields['title'].help_text - assert help_text == 'The length of the post must be between 40 and 60 characters' + help_text = form.fields["title"].help_text + assert help_text == "The length of the post must be between 40 and 60 characters" def test_content_field_is_required(self, sample_post_data): form = PostForm(data=sample_post_data) - form.data['content'] = '' - error_returned = form.errors['content'][0] - assert error_returned == 'This field is required.' + form.data["content"] = "" + error_returned = form.errors["content"][0] + assert error_returned == "This field is required." def test_categories_field_is_required(self, sample_post_data): form = PostForm(data=sample_post_data) - form.data['categories'] = '' - error_returned = form.errors['categories'][0] - assert error_returned == 'This field is required.' + form.data["categories"] = "" + error_returned = form.errors["categories"][0] + assert error_returned == "This field is required." def test_categories_field_contains_help_text(self, sample_post_data): form = PostForm(data=sample_post_data) - help_text = form.fields['categories'].help_text - assert help_text == 'Select more than one category by holding down Ctrl or Cmd key' + help_text = form.fields["categories"].help_text + assert help_text == "Select more than one category by holding down Ctrl or Cmd key" def test_form_still_submits_without_image(self, sample_post_data): form = PostForm(data=sample_post_data) - form.data['image'] = None - assert form.is_valid(), 'Should still be valid' + form.data["image"] = None + assert form.is_valid(), "Should still be valid" def test_image_field_contains_help_text(self, sample_post_data): form = PostForm(data=sample_post_data) - help_text = form.fields['image'].help_text - assert help_text == 'For bests results, use an image that is 1,200px wide x 600px high' + help_text = form.fields["image"].help_text + assert help_text == "For bests results, use an image that is 1,200px wide x 600px high" def test_status_field_is_required(self, sample_post_data): form = PostForm(data=sample_post_data) - form.data['status'] = '' - error_returned = form.errors['status'][0] - assert error_returned == 'This field is required.' + form.data["status"] = "" + error_returned = form.errors["status"][0] + assert error_returned == "This field is required." diff --git a/apps/blog/tests/test_models.py b/apps/blog/tests/test_models.py index cfd0ec8a..e6ee7a25 100644 --- a/apps/blog/tests/test_models.py +++ b/apps/blog/tests/test_models.py @@ -18,42 +18,44 @@ user_model = get_user_model() + class TestCategory: def test_name_is_charfield(self): category = mixer.blend(Category, pk=1) field = category._meta.get_field("name") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_slug_is_slugfield(self): category = mixer.blend(Category, pk=1) field = category._meta.get_field("slug") - assert isinstance(field, models.SlugField), 'Should be a slug field' + assert isinstance(field, models.SlugField), "Should be a slug field" def test_created_date_is_datefield(self): category = mixer.blend(Category, pk=1) field = category._meta.get_field("created_date") - assert isinstance(field, models.DateField), 'Should be a date field' + assert isinstance(field, models.DateField), "Should be a date field" def test_category_str(self): - category = mixer.blend(Category, pk=1, name='Python') - assert str(category) == 'Python', 'Should be the same as the category name' + category = mixer.blend(Category, pk=1, name="Python") + assert str(category) == "Python", "Should be the same as the category name" def test_get_absolute_url(self): - category = mixer.blend(Category, pk=1, title='Example Category', slug='example-category') - assert category.get_absolute_url() == \ - reverse('blog:category_posts', kwargs={'slug': category.slug}) + category = mixer.blend(Category, pk=1, title="Example Category", slug="example-category") + assert category.get_absolute_url() == reverse( + "blog:category_posts", kwargs={"slug": category.slug} + ) class TestPost: def test_title_is_charfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("title") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_slug_is_slugfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("slug") - assert isinstance(field, models.SlugField), 'Should be a slug field' + assert isinstance(field, models.SlugField), "Should be a slug field" def test_slugification(self): post = mixer.blend(Post, author__pk=2) @@ -62,116 +64,124 @@ def test_slugification(self): for fragment in title_fragments: assert fragment.casefold() in post.slug if len(title_fragments) > 1: - assert '-' in post.slug, 'Should contain a hyphen' + assert "-" in post.slug, "Should contain a hyphen" def test_unique_slug_created(self): - post1 = mixer.blend(Post, author__pk=2, slug='example-slug') + post1 = mixer.blend(Post, author__pk=2, slug="example-slug") post1.save() - post2 = mixer.blend(Post, author__pk=2, slug='example-slug') + post2 = mixer.blend(Post, author__pk=2, slug="example-slug") post2.save() - assert post2.slug != post1.slug, 'Should be different slugs' - assert post2.slug == f"{post1.slug}1", 'Should append a new count to the slug' + assert post2.slug != post1.slug, "Should be different slugs" + assert post2.slug == f"{post1.slug}1", "Should append a new count to the slug" def test_content_is_htmlfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("content") - assert isinstance(field, HTMLField), \ - 'Should be an `HTML Field` from third party app, Django Tiny MCE' + assert isinstance( + field, HTMLField + ), "Should be an `HTML Field` from third party app, Django Tiny MCE" def test_reference_url_is_urlfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("reference_url") - assert isinstance(field, models.URLField), 'Should be a URL field' + assert isinstance(field, models.URLField), "Should be a URL field" def test_publish_date_is_datetimefield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("publish_date") - assert isinstance(field, models.DateTimeField), 'Should be a datetime field' + assert isinstance(field, models.DateTimeField), "Should be a datetime field" def test_publish_date_generates(self): post = mixer.blend(Post, author__pk=2) - assert post.publish_date.day == datetime.date.today().day, \ - 'Should generate the day of the week for today' + assert ( + post.publish_date.day == datetime.date.today().day + ), "Should generate the day of the week for today" def test_updated_date_is_datetimefield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("updated_date") - assert isinstance(field, models.DateTimeField), 'Should be a datetime field' + assert isinstance(field, models.DateTimeField), "Should be a datetime field" def test_updated_date_generates(self): post = mixer.blend(Post, author__pk=2) - assert post.updated_date.day == datetime.date.today().day, \ - 'Should generate the day of the week for today' + assert ( + post.updated_date.day == datetime.date.today().day + ), "Should generate the day of the week for today" def test_image_is_imagefield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("image") - assert isinstance(field, models.ImageField), 'Should be an image field' + assert isinstance(field, models.ImageField), "Should be an image field" def test_status_is_integerfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("status") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_status_defaults_as_draft(self): post = mixer.blend(Post, author__pk=2) - assert post.status == 0, 'Should default to `0`' + assert post.status == 0, "Should default to `0`" def test_author_is_foreignkeyfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("author") - assert isinstance(field, models.ForeignKey), 'Should be a foreign key field' + assert isinstance(field, models.ForeignKey), "Should be a foreign key field" def test_categories_is_manytomanyfield(self): post = mixer.blend(Post, author__pk=2) field = post._meta.get_field("categories") - assert isinstance(field, models.ManyToManyField), 'Should be a many-to-many field' + assert isinstance(field, models.ManyToManyField), "Should be a many-to-many field" def test_word_count(self): - post = mixer.blend(Post, author__pk=2, content='Beautiful is better than ugly.') - assert post.word_count == 5, \ - 'Calculated word count should be the number of words in the content string' + post = mixer.blend(Post, author__pk=2, content="Beautiful is better than ugly.") + assert ( + post.word_count == 5 + ), "Calculated word count should be the number of words in the content string" def test_reading_time(self): - test_text = 'lorem ipsum ' + test_text = "lorem ipsum " post = mixer.blend(Post, author__pk=2, content=test_text * 50) - assert post.reading_time == 2, \ - 'The duplicated lorem ipsum text of 100 words should be a 2 minute read' + assert ( + post.reading_time == 2 + ), "The duplicated lorem ipsum text of 100 words should be a 2 minute read" def test_get_excerpt(self): - post = mixer.blend(Post, author__pk=2, content='Test content ...') + post = mixer.blend(Post, author__pk=2, content="Test content ...") result = post.get_excerpt(16) - assert result == 'Test content ...', 'Should return first 16 characters' + assert result == "Test content ...", "Should return first 16 characters" def test_post_str(self): - post = mixer.blend(Post, author__pk=2, title='Example Post') - assert str(post) == 'Example Post', 'Should be the same as the post name' + post = mixer.blend(Post, author__pk=2, title="Example Post") + assert str(post) == "Example Post", "Should be the same as the post name" def test_publish_year(self): post = mixer.blend(Post, author__pk=2) - assert post.publish_year == post.publish_date.year, \ - "Year should be the same as the published date's year property" + assert ( + post.publish_year == post.publish_date.year + ), "Year should be the same as the published date's year property" def test_num_draft_posts(self): draft_posts = mixer.cycle(10).blend(Post, author__pk=2, status=0) published_posts = mixer.cycle(5).blend(Post, author__pk=2, status=1) total_posts = draft_posts + published_posts - assert len(total_posts) == Post.objects.count(), 'Should have 15 posts in the DB' - assert Post.num_draft_posts() == 10, 'Should have 10 `draft` posts in the DB' + assert len(total_posts) == Post.objects.count(), "Should have 15 posts in the DB" + assert Post.num_draft_posts() == 10, "Should have 10 `draft` posts in the DB" def test_published_posts_manager(self): draft_posts = mixer.cycle(10).blend(Post, author__pk=2, status=0) published_posts = mixer.cycle(5).blend(Post, author__pk=2, status=1) total_posts = draft_posts + published_posts - assert len(total_posts) == Post.objects.count(), 'Should have 15 posts in the DB' - assert Post.published.count() == 5, 'Should have 5 `published` posts in the DB' + assert len(total_posts) == Post.objects.count(), "Should have 15 posts in the DB" + assert Post.published.count() == 5, "Should have 5 `published` posts in the DB" def test_get_absolute_url(self): - post = mixer.blend(Post, author__pk=2, title='Example Post', slug='example-post') - assert post.get_absolute_url() == reverse('blog:post_detail', kwargs={'slug': post.slug}) + post = mixer.blend(Post, author__pk=2, title="Example Post", slug="example-post") + assert post.get_absolute_url() == reverse("blog:post_detail", kwargs={"slug": post.slug}) def test_multiple_post_slugs_appends_instance_id(self): - posts = mixer.cycle(10).blend(Post, author__pk=2, title='Example Post', slug='example-post') - assert posts[9].pk == 10, '10th instance should have a PK of 10' - assert Post.objects.count() == 10, 'Should have 10 objects in the database' - assert posts[9].slug == 'example-post9', '10th instance of DB save appends next id' + posts = mixer.cycle(10).blend( + Post, author__pk=2, title="Example Post", slug="example-post" + ) + assert posts[9].pk == 10, "10th instance should have a PK of 10" + assert Post.objects.count() == 10, "Should have 10 objects in the database" + assert posts[9].slug == "example-post9", "10th instance of DB save appends next id" diff --git a/apps/blog/tests/test_urls.py b/apps/blog/tests/test_urls.py index 279a322a..631348cc 100644 --- a/apps/blog/tests/test_urls.py +++ b/apps/blog/tests/test_urls.py @@ -1,63 +1,72 @@ from django.urls import reverse -from apps.blog.views import (AuthorPostListView, CategoryPostListView, ContentsListView, - HomeView, IndexListView, PostCreateView, PostDeleteView, - PostDetailView, PostUpdateView, SearchResultsView, - SearchView,) +from apps.blog.views import ( + AuthorPostListView, + CategoryPostListView, + ContentsListView, + HomeView, + IndexListView, + PostCreateView, + PostDeleteView, + PostDetailView, + PostUpdateView, + SearchResultsView, + SearchView, +) class TestUrls: def test_blog_home(self): - """ Verify that the `home` url invokes intended view """ - path = reverse('blog:home') + """Verify that the `home` url invokes intended view""" + path = reverse("blog:home") assert path, HomeView.as_view().__name__ def test_author_posts(self): - """ Verify that the `author_posts` url invokes intended view """ - path = reverse('blog:author_posts', kwargs={'username': 'test-username'}) + """Verify that the `author_posts` url invokes intended view""" + path = reverse("blog:author_posts", kwargs={"username": "test-username"}) assert path, AuthorPostListView.as_view().__name__ def test_category_posts(self): - """ Verify that the `category_posts` url invokes intended view """ - path = reverse('blog:category_posts', kwargs={'slug': 'test-category-slug'}) + """Verify that the `category_posts` url invokes intended view""" + path = reverse("blog:category_posts", kwargs={"slug": "test-category-slug"}) assert path, CategoryPostListView.as_view().__name__ def test_post_create(self): - """ Verify that the `post_create` url invokes intended view """ - path = reverse('blog:post_create') + """Verify that the `post_create` url invokes intended view""" + path = reverse("blog:post_create") assert path, PostCreateView.as_view().__name__ def test_post_detail(self): - """ Verify that the `post_detail` url invokes intended view """ - path = reverse('blog:post_detail', kwargs={'slug': 'test-post-slug'}) + """Verify that the `post_detail` url invokes intended view""" + path = reverse("blog:post_detail", kwargs={"slug": "test-post-slug"}) assert path, PostDetailView.as_view().__name__ def test_post_update(self): - """ Verify that the `post_update` url invokes intended view """ - path = reverse('blog:post_update', kwargs={'slug': 'test-post-slug'}) + """Verify that the `post_update` url invokes intended view""" + path = reverse("blog:post_update", kwargs={"slug": "test-post-slug"}) assert path, PostUpdateView.as_view().__name__ def test_post_delete(self): - """ Verify that the `post_delete` url invokes intended view """ - path = reverse('blog:post_delete', kwargs={'slug': 'test-post-slug'}) + """Verify that the `post_delete` url invokes intended view""" + path = reverse("blog:post_delete", kwargs={"slug": "test-post-slug"}) assert path, PostDeleteView.as_view().__name__ def test_search(self): - """ Verify that the `search` url invokes intended view """ - path = reverse('blog:search') + """Verify that the `search` url invokes intended view""" + path = reverse("blog:search") assert path, SearchView.as_view().__name__ def test_search_results(self): - """ Verify that the `search results` url invokes intended view """ - path = reverse('blog:search_results') + """Verify that the `search results` url invokes intended view""" + path = reverse("blog:search_results") assert path, SearchResultsView.as_view().__name__ def test_index(self): - """ Verify that the `index` url invokes intended view """ - path = reverse('blog:index') + """Verify that the `index` url invokes intended view""" + path = reverse("blog:index") assert path, IndexListView.as_view().__name__ def test_contents(self): - """ Verify that the `contents` url invokes intended view """ - path = reverse('blog:contents') + """Verify that the `contents` url invokes intended view""" + path = reverse("blog:contents") assert path, ContentsListView.as_view().__name__ diff --git a/apps/blog/tests/test_views.py b/apps/blog/tests/test_views.py index 883368cb..b7a64ffb 100644 --- a/apps/blog/tests/test_views.py +++ b/apps/blog/tests/test_views.py @@ -8,23 +8,29 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestHomeView: def test_all_users_can_access(self, rf, all_users): """ Asserts authenticated and unauthenticated user can access complete list of posts """ - path = reverse('blog:home') + path = reverse("blog:home") request = rf.get(path) request.user = all_users response = blog_views.HomeView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestAuthorPostListView: def test_all_users_can_access(self, rf, pub_posts, all_users): """ @@ -32,134 +38,146 @@ def test_all_users_can_access(self, rf, pub_posts, all_users): list of posts written by another author """ author_username = pub_posts[0].author.get_username() - kwargs = {'username': author_username} - path = reverse('blog:author_posts', kwargs=kwargs) + kwargs = {"username": author_username} + path = reverse("blog:author_posts", kwargs=kwargs) request = rf.get(path) request.user = all_users response = blog_views.AuthorPostListView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestCategoryPostListView: def test_all_users_can_access(self, rf, category, all_users): """ Asserts authenticated and unauthenticated user can access list of posts in a given category """ - kwargs = {'slug': category.slug} - path = reverse('blog:category_posts', kwargs=kwargs) + kwargs = {"slug": category.slug} + path = reverse("blog:category_posts", kwargs=kwargs) request = rf.get(path) request.user = all_users response = blog_views.CategoryPostListView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestPostDetailView: def test_all_users_can_access(self, rf, pub_post, all_users): """ Asserts an authenticated and unauthenticated user can access a single post detail view """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_detail', kwargs=kwargs) + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_detail", kwargs=kwargs) request = rf.get(path) request.user = all_users response = blog_views.PostDetailView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" class TestPostCreateView: - path = reverse('blog:post_create') + path = reverse("blog:post_create") def test_auth_user_can_access(self, rf, auth_user): - """ Asserts authenticated user can access the view """ + """Asserts authenticated user can access the view""" request = rf.get(self.path) request.user = auth_user response = blog_views.PostCreateView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_unauth_user_cannot_access(self, rf, unauth_user): - """ Asserts unauthenticated user cannot access the view """ + """Asserts unauthenticated user cannot access the view""" request = rf.get(self.path) request.user = unauth_user response = blog_views.PostCreateView.as_view()(request) - assert response.status_code == 302, 'Should be redirected to the `login` page' - assert 'login/?next=' and '/new' in response.url, 'Should redirect to login screen' + assert response.status_code == 302, "Should be redirected to the `login` page" + assert "login/?next=" and "/new" in response.url, "Should redirect to login screen" class TestPostUpdateView: def test_author_can_access(self, rf, pub_post): - """ Asserts post's author can access the update view of a post """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_update', kwargs=kwargs) + """Asserts post's author can access the update view of a post""" + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_update", kwargs=kwargs) request = rf.get(path) request.user = pub_post.author response = blog_views.PostUpdateView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_unauth_user_cannot_access(self, rf, unauth_user, pub_post): - """ Asserts unauthenticated user cannot access post update view """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_update', kwargs=kwargs) + """Asserts unauthenticated user cannot access post update view""" + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_update", kwargs=kwargs) request = rf.get(path) request.user = unauth_user response = blog_views.PostUpdateView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Should return `OK` status code by logged in author' - assert '/login/?next=' in response.url, 'Should redirect to login page' - assert f"{pub_post.slug}/update" in response.url, 'Should redirect to login page' + assert response.status_code == 302, "Should return `OK` status code by logged in author" + assert "/login/?next=" in response.url, "Should redirect to login page" + assert f"{pub_post.slug}/update" in response.url, "Should redirect to login page" def test_author_can_update(self, rf, pub_post): - """ Asserts author can update the post """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_update', kwargs=kwargs) + """Asserts author can update the post""" + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_update", kwargs=kwargs) request = rf.post(path) request.user = pub_post.author response = blog_views.PostUpdateView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should redirect user to detail page upon update' + assert response.status_code == 200, "Should redirect user to detail page upon update" class TestPostDeleteView: def test_author_can_delete(self, rf, pub_post): - """ Asserts author can access the delete view of a single post """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_delete', kwargs=kwargs) + """Asserts author can access the delete view of a single post""" + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_delete", kwargs=kwargs) request = rf.get(path) request.user = pub_post.author response = blog_views.PostDeleteView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should return `OK` status code by author' + assert response.status_code == 200, "Should return `OK` status code by author" def test_unauth_user_cannot_delete(self, rf, unauth_user, pub_post): - """ Asserts unauthenticated user cannot delete the post """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_delete', kwargs=kwargs) + """Asserts unauthenticated user cannot delete the post""" + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_delete", kwargs=kwargs) request = rf.post(path) request.user = unauth_user response = blog_views.PostDeleteView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Should redirect user' - assert '/blog/' in response.url, '`blog` should appear in URL following redirect' + assert response.status_code == 302, "Should redirect user" + assert "/blog/" in response.url, "`blog` should appear in URL following redirect" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestSearchView: def test_all_users_can_access(self, rf, all_users): """ Asserts authenticated and unauthenticated users can retrieve the search page """ - path = reverse('blog:search') + path = reverse("blog:search") request = rf.get(path) request.user = all_users response = blog_views.SearchView.as_view()(request) - assert response.status_code == 200, 'Search results should be returned' + assert response.status_code == 200, "Search results should be returned" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestSearchResultsView: def test_all_users_can_access_searches(self, rf, pub_posts, search_terms, all_users): """ @@ -170,16 +188,19 @@ def test_all_users_can_access_searches(self, rf, pub_posts, search_terms, all_us request = rf.get(path) request.user = all_users response = blog_views.SearchResultsView.as_view()(request) - assert response.status_code == 200, 'Search results should be returned' + assert response.status_code == 200, "Search results should be returned" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestContentsListView: def test_all_users_can_access(self, rf, pub_posts, all_users): - """ Asserts unauthenticated users can access `contents` page """ - path = reverse('blog:contents') + """Asserts unauthenticated users can access `contents` page""" + path = reverse("blog:contents") request = rf.get(path) request.user = all_users response = blog_views.ContentsListView.as_view()(request) - assert response.status_code == 200, 'Should return `OK` status code by unauth user' + assert response.status_code == 200, "Should return `OK` status code by unauth user" diff --git a/apps/blog/urls.py b/apps/blog/urls.py index 3fa954f3..1ad83563 100644 --- a/apps/blog/urls.py +++ b/apps/blog/urls.py @@ -1,27 +1,36 @@ from django.urls import include, path from apps.blog.feeds import AtomLatestPostsFeed, LatestPostsFeed -from apps.blog.views import (AuthorPostListView, CategoryPostListView, ContentsListView, - HomeView, IndexListView, PostCreateView, PostDeleteView, - PostDetailView, PostUpdateView, SearchResultsView, - SearchView,) +from apps.blog.views import ( + AuthorPostListView, + CategoryPostListView, + ContentsListView, + HomeView, + IndexListView, + PostCreateView, + PostDeleteView, + PostDetailView, + PostUpdateView, + SearchResultsView, + SearchView, +) -app_name = 'blog' +app_name = "blog" urlpatterns = [ - path('', HomeView.as_view(), name='home'), - path('user//posts', AuthorPostListView.as_view(), name='author_posts'), - path('category//posts', CategoryPostListView.as_view(), name='category_posts'), - path('post/new/', PostCreateView.as_view(), name='post_create'), - path('post//', PostDetailView.as_view(), name='post_detail'), - path('post//update/', PostUpdateView.as_view(), name='post_update'), - path('post//delete/', PostDeleteView.as_view(), name='post_delete'), - path('search/', SearchView.as_view(), name='search'), - path('search/results/', SearchResultsView.as_view(), name='search_results'), - path('index/', IndexListView.as_view(), name='index'), - path('contents/', ContentsListView.as_view(), name='contents'), - path('sitenews/rss/', LatestPostsFeed(), name='post_feed'), - path('sitenews/atom/', AtomLatestPostsFeed(), name='post_feed'), - path('users/', include('users.urls', namespace='users')), + path("", HomeView.as_view(), name="home"), + path("user//posts", AuthorPostListView.as_view(), name="author_posts"), + path("category//posts", CategoryPostListView.as_view(), name="category_posts"), + path("post/new/", PostCreateView.as_view(), name="post_create"), + path("post//", PostDetailView.as_view(), name="post_detail"), + path("post//update/", PostUpdateView.as_view(), name="post_update"), + path("post//delete/", PostDeleteView.as_view(), name="post_delete"), + path("search/", SearchView.as_view(), name="search"), + path("search/results/", SearchResultsView.as_view(), name="search_results"), + path("index/", IndexListView.as_view(), name="index"), + path("contents/", ContentsListView.as_view(), name="contents"), + path("sitenews/rss/", LatestPostsFeed(), name="post_feed"), + path("sitenews/atom/", AtomLatestPostsFeed(), name="post_feed"), + path("users/", include("users.urls", namespace="users")), ] diff --git a/apps/blog/views.py b/apps/blog/views.py index 9e0f6b1e..2ea7664a 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -7,8 +7,14 @@ from django.shortcuts import get_list_or_404, get_object_or_404 from django.urls import reverse, reverse_lazy from django.utils.html import format_html -from django.views.generic import (CreateView, DeleteView, DetailView, ListView, - TemplateView, UpdateView,) +from django.views.generic import ( + CreateView, + DeleteView, + DetailView, + ListView, + TemplateView, + UpdateView, +) from apps.blog import search from apps.blog.forms import PostForm @@ -20,122 +26,135 @@ class PostView(ListView): Custom view sets default behaviour for all list views to subclass and inherit for their own implementation """ + queryset = Post.published.all() - context_object_name = 'posts' - category_list = Category.objects.all().prefetch_related('posts') - extra_context = {'categories_list': category_list} + context_object_name = "posts" + category_list = Category.objects.all().prefetch_related("posts") + extra_context = {"categories_list": category_list} def get_context_data(self, **kwargs): - """ Facilitates pagination and post count summary """ + """Facilitates pagination and post count summary""" context = super().get_context_data(**kwargs) - context['current_page'] = context.pop('page_obj', None) + context["current_page"] = context.pop("page_obj", None) return context class PostCreateView(LoginRequiredMixin, CreateView): - """ Permits a logged in user to create a new post """ + """Permits a logged in user to create a new post""" + form_class = PostForm - template_name = 'post_form.html' + template_name = "post_form.html" def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) def get_success_url(self): - """ Return the URL to redirect to after processing a valid form. """ + """Return the URL to redirect to after processing a valid form.""" if self.object.status == 0: - return reverse('blog:home') + return reverse("blog:home") else: return self.object.get_absolute_url() class HomeView(PostView): - """ Drives the list of posts returned on the blog's home page """ - template_name = 'blog/home.html' + """Drives the list of posts returned on the blog's home page""" + + template_name = "blog/home.html" paginate_by = 6 paginate_orphans = 3 class IndexListView(PostView): - """ Facilitates the short contents page """ - template_name = 'blog/index_page.html' + """Facilitates the short contents page""" + + template_name = "blog/index_page.html" class ContentsListView(PostView): - """ Facilitates the contents page """ - template_name = 'blog/contents.html' + """Facilitates the contents page""" + + template_name = "blog/contents.html" paginate_by = 10 paginate_orphans = 3 def get_context_data(self, **kwargs): - """ Get's the author's name/username for presenting in the template """ + """Get's the author's name/username for presenting in the template""" context = super().get_context_data(**kwargs) - context['author'] = Post.published.first().author + context["author"] = Post.published.first().author return context class AuthorPostListView(PostView): - """ Drives the list of posts written by a given author """ - template_name = 'blog/author_posts.html' + """Drives the list of posts written by a given author""" + + template_name = "blog/author_posts.html" paginate_by = 6 paginate_orphans = 3 def get_queryset(self): - user = get_object_or_404(get_user_model(), username=self.kwargs['username']) + user = get_object_or_404(get_user_model(), username=self.kwargs["username"]) return super().get_queryset().filter(author=user) def get_context_data(self, **kwargs): - """ Get's the author's name/username for presenting in the template """ + """Get's the author's name/username for presenting in the template""" context = super().get_context_data(**kwargs) - context['display_name'] = Post.published.first().author.profile.display_name + context["display_name"] = Post.published.first().author.profile.display_name return context class CategoryPostListView(PostView): - """ Drives the list of posts for a given category. """ - template_name = 'blog/category_posts.html' + """Drives the list of posts for a given category.""" + + template_name = "blog/category_posts.html" paginate_by = 6 paginate_orphans = 3 def get_queryset(self): - self.categories = get_list_or_404(Category, slug=self.kwargs['slug']) + self.categories = get_list_or_404(Category, slug=self.kwargs["slug"]) return self.queryset.filter(categories=self.categories[0].id) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['categories'] = self.categories + context["categories"] = self.categories return context class SearchView(TemplateView): - """ In minimalist style, à la Google, provides a search box """ - template_name = 'search.html' + """In minimalist style, à la Google, provides a search box""" + + template_name = "search.html" class SearchResultsView(PostView): - """ Facilitates the search results """ - template_name = 'blog/search_results.html' + """Facilitates the search results""" + + template_name = "blog/search_results.html" paginate_by = 10 paginate_orphans = 2 relevancy_factor = 0.2 def get_queryset(self): start_time = perf_counter() - initial_query = format_html(self.request.GET.get('q')) + initial_query = format_html(self.request.GET.get("q")) cleaned_query = search.cleanup_string(initial_query) if cleaned_query: - search_vector = SearchVector('title', weight='A') + SearchVector('content', weight='B') + search_vector = SearchVector("title", weight="A") + SearchVector("content", weight="B") search_query = SearchQuery(cleaned_query) search_rank = SearchRank(search_vector, search_query) - qs = Post.published.annotate( - rank=search_rank - ).filter(rank__gte=self.relevancy_factor).order_by('-rank') + qs = ( + Post.published.annotate(rank=search_rank) + .filter(rank__gte=self.relevancy_factor) + .order_by("-rank") + ) end_time = perf_counter() - self.extra_context.update({ - 'query': initial_query, - 'cleaned_query': cleaned_query, - 'time_taken': f"{end_time - start_time:.3f}" - }) + self.extra_context.update( + { + "query": initial_query, + "cleaned_query": cleaned_query, + "time_taken": f"{end_time - start_time:.3f}", + } + ) return qs else: msg = "A blank search cannot be submitted. Please enter a valid search query." @@ -143,38 +162,40 @@ def get_queryset(self): return self.queryset.none() def get_context_data(self, **kwargs): - """ Get's the author object for presenting in the template """ + """Get's the author object for presenting in the template""" context = super().get_context_data(**kwargs) - context['author'] = Post.published.first().author + context["author"] = Post.published.first().author return context class PostDetailView(DetailView): - """ Provides the individual post's page """ + """Provides the individual post's page""" + model = Post - category_list = Category.objects.all().prefetch_related('posts') - extra_context = {'categories_list': category_list} + category_list = Category.objects.all().prefetch_related("posts") + extra_context = {"categories_list": category_list} def get_context_data(self, **kwargs): - """ Facilitates detail page's pagination buttons """ + """Facilitates detail page's pagination buttons""" context = super().get_context_data(**kwargs) - context['author'] = Post.published.first().author - context['profile'] = context['author'].profile + context["author"] = Post.published.first().author + context["profile"] = context["author"].profile posts = Post.published.all() posts_count = len(posts) for idx, post in enumerate(posts): - if post.slug == self.kwargs['slug']: - context['prev_post'] = posts[posts_count - 1] if idx == 0 else posts[idx - 1] - context['next_post'] = posts[0] if idx == posts_count - 1 else posts[idx + 1] + if post.slug == self.kwargs["slug"]: + context["prev_post"] = posts[posts_count - 1] if idx == 0 else posts[idx - 1] + context["next_post"] = posts[0] if idx == posts_count - 1 else posts[idx + 1] return context class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): - """ Permits a logged in user to update an existing post they own """ + """Permits a logged in user to update an existing post they own""" + model = Post form_class = PostForm - template_name = 'post_form.html' + template_name = "post_form.html" def form_valid(self, form): form.instance.author = self.request.user @@ -186,10 +207,11 @@ def test_func(self) -> bool: class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): - """ Permits a logged in user to delete an existing post they own """ + """Permits a logged in user to delete an existing post they own""" + model = Post - template_name = 'post_confirm_delete.html' - success_url = reverse_lazy('blog:home') + template_name = "post_confirm_delete.html" + success_url = reverse_lazy("blog:home") def test_func(self) -> bool: post = self.get_object() diff --git a/apps/conftest.py b/apps/conftest.py index 69712bd4..c7088c74 100644 --- a/apps/conftest.py +++ b/apps/conftest.py @@ -18,70 +18,70 @@ from apps.users.utils import get_challenge_expiration_timestamp -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def random_user(django_user_model): - """ A random user """ + """A random user""" return mixer.blend(django_user_model, pk=2) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def third_user_as_author(django_user_model): - """ A random user """ + """A random user""" return mixer.blend(django_user_model, pk=3) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def test_password(): - """ A password used for an authenticated user fixture """ - return os.environ['PYTEST_TEST_PASSWORD'] + """A password used for an authenticated user fixture""" + return os.environ["PYTEST_TEST_PASSWORD"] -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def auth_user(client, django_user_model, test_password): - """ An authenticated user object using the specified user model """ + """An authenticated user object using the specified user model""" user = django_user_model.objects.create_user( - pk=2, - first_name='Wayne', - last_name='Lambert', - username='wayne-lambert', - email='wayne-lambert@example.com', - password=test_password, + pk=2, + first_name="Wayne", + last_name="Lambert", + username="wayne-lambert", + email="wayne-lambert@example.com", + password=test_password, ) client.login(username=user.get_username(), password=test_password) return user -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def device_auth_user(client, auth_user, test_password): - """ An two-factor authenticated user object using device token """ - auth_user.totpdevice_set.create(name='default', key=random_hex(), confirmed=True) + """An two-factor authenticated user object using device token""" + auth_user.totpdevice_set.create(name="default", key=random_hex(), confirmed=True) client.login(username=auth_user.get_username(), password=test_password) return auth_user -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def email_auth_user(client, auth_user, test_password): - """ An two-factor authenticated user object using email token """ + """An two-factor authenticated user object using email token""" email_token = EmailToken.objects.create( challenge_email_address=auth_user.email, - challenge_token='123456', + challenge_token="123456", challenge_generation_timestamp=timezone.now(), challenge_expiration_timestamp=get_challenge_expiration_timestamp(), challenge_completed=True, # Emulates challenge passed - user_id=auth_user.pk + user_id=auth_user.pk, ) email_token.save() client.login(username=auth_user.get_username(), password=test_password) return auth_user -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def unauth_user(): - """ An unauthenticated user (i.e. an anonymous user) """ + """An unauthenticated user (i.e. an anonymous user)""" return AnonymousUser() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def all_users(request, auth_user, unauth_user): """ A combined fixture containing both an authenticated and @@ -90,13 +90,13 @@ def all_users(request, auth_user, unauth_user): """ user_type = request.param users = { - 'auth_user': auth_user, - 'unauth_user': unauth_user, + "auth_user": auth_user, + "unauth_user": unauth_user, } return users[user_type] -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def fixed_user(django_user_model): """ A fixed user useful in scenarios where testing against known @@ -104,96 +104,101 @@ def fixed_user(django_user_model): """ return django_user_model.objects.create_user( pk=2, - first_name='Wayne', - last_name='Lambert', - username='wayne-lambert', - email='wayne-lambert@example.com', + first_name="Wayne", + last_name="Lambert", + username="wayne-lambert", + email="wayne-lambert@example.com", ) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def li_sec_user(django_user_model, client, test_password, **kwargs): """ Useful in tests where a secondary user tries to access a protected view and should therefore be greeted within a forbidden response """ - if 'username' not in kwargs: - kwargs['username'] = 'endeavour-morse' - kwargs['first_name'] = 'Endeavour' - kwargs['last_name'] = 'Morse' - kwargs['email'] = 'endeavour-morse@example.com' + if "username" not in kwargs: + kwargs["username"] = "endeavour-morse" + kwargs["first_name"] = "Endeavour" + kwargs["last_name"] = "Morse" + kwargs["email"] = "endeavour-morse@example.com" user = django_user_model.objects.create_user(pk=3, **kwargs) client.login(username=user.get_username(), password=test_password) return user -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def pub_post(random_user): - """ A random published blog post """ + """A random published blog post""" return mixer.blend(Post, author=random_user, status=1) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def draft_posts(third_user_as_author): - """ A random set of 10 draft blog posts """ + """A random set of 10 draft blog posts""" return mixer.cycle(10).blend(Post, author=third_user_as_author, status=0) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def pub_posts(third_user_as_author): - """ A random set of 10 published blog posts """ + """A random set of 10 published blog posts""" return mixer.cycle(10).blend(Post, author=third_user_as_author, status=1) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def category(): - """ A random blog category """ + """A random blog category""" return mixer.blend(Category, pk=1) -@pytest.fixture(scope='function', params=helpers.get_search_strings()) +@pytest.fixture(scope="function", params=helpers.get_search_strings()) def search_terms(request): - """ A fixture for parametrizing search terms in tests """ + """A fixture for parametrizing search terms in tests""" return request.param -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def test_image(): - """ A sample in-memory image for tests involving images """ - image = Image.new(mode='RGB', size=(200, 200)) + """A sample in-memory image for tests involving images""" + image = Image.new(mode="RGB", size=(200, 200)) image_io = BytesIO() - image.save(image_io, 'JPEG') + image.save(image_io, "JPEG") image_io.seek(0) - return InMemoryUploadedFile(file=image_io, field_name=None, - name='test-image.jpg', content_type='image/jpeg', - size=len(image_io.getvalue()), charset=None) + return InMemoryUploadedFile( + file=image_io, + field_name=None, + name="test-image.jpg", + content_type="image/jpeg", + size=len(image_io.getvalue()), + charset=None, + ) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def sample_post_data(): - """ A set of form data for completing a blog post form """ + """A set of form data for completing a blog post form""" return { - 'title': 'Test title which has a title of between 40 and 60 chars', - 'content': 'Test content', - 'categories': mixer.cycle(2).blend(Category), - 'reference_url': 'https://waynelambert.dev', - 'image': test_image, - 'status': 0, - 'validity': True, + "title": "Test title which has a title of between 40 and 60 chars", + "content": "Test content", + "categories": mixer.cycle(2).blend(Category), + "reference_url": "https://waynelambert.dev", + "image": test_image, + "status": 0, + "validity": True, } -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def sample_user_data(): - """ A set of form data for completing a user registration form """ + """A set of form data for completing a user registration form""" return { - 'username': 'wayne-lambert', - 'email': 'test-email@example.com', - 'first_name': 'Wayne', - 'last_name': 'Lambert', - 'password1': test_password, - 'password2': test_password, + "username": "wayne-lambert", + "email": "test-email@example.com", + "first_name": "Wayne", + "last_name": "Lambert", + "password1": test_password, + "password2": test_password, } diff --git a/apps/contacts/admin.py b/apps/contacts/admin.py index 1463dec1..7187fcca 100644 --- a/apps/contacts/admin.py +++ b/apps/contacts/admin.py @@ -5,9 +5,18 @@ class ContactAdmin(admin.ModelAdmin): model = Contact - list_display = ('full_name', 'email', 'submit_date',) - search_fields = ('first_name', 'last_name', 'email',) - readonly_fields = ('submit_date',) - ordering = ('first_name',) + list_display = ( + "full_name", + "email", + "submit_date", + ) + search_fields = ( + "first_name", + "last_name", + "email", + ) + readonly_fields = ("submit_date",) + ordering = ("first_name",) + admin.site.register(Contact, ContactAdmin) diff --git a/apps/contacts/apps.py b/apps/contacts/apps.py index 4503020a..910a567e 100644 --- a/apps/contacts/apps.py +++ b/apps/contacts/apps.py @@ -2,4 +2,4 @@ class ContactsConfig(AppConfig): - name = 'apps.contacts' + name = "apps.contacts" diff --git a/apps/contacts/migrations/0001_initial.py b/apps/contacts/migrations/0001_initial.py index 317d5879..fe9c8f66 100644 --- a/apps/contacts/migrations/0001_initial.py +++ b/apps/contacts/migrations/0001_initial.py @@ -4,22 +4,25 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Contact', + name="Contact", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_name', models.CharField(max_length=50)), - ('last_name', models.CharField(max_length=50)), - ('email', models.EmailField(max_length=254)), - ('message', models.TextField()), - ('submit_date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("first_name", models.CharField(max_length=50)), + ("last_name", models.CharField(max_length=50)), + ("email", models.EmailField(max_length=254)), + ("message", models.TextField()), + ("submit_date", models.DateTimeField(auto_now_add=True)), ], ), ] diff --git a/apps/contacts/tests/conftest.py b/apps/contacts/tests/conftest.py index 6dcaea0c..d0fa2e1f 100644 --- a/apps/contacts/tests/conftest.py +++ b/apps/contacts/tests/conftest.py @@ -12,19 +12,19 @@ from apps.contacts.models import Contact -@pytest.fixture(name='random_contact', scope='function') +@pytest.fixture(name="random_contact", scope="function") def random_contact(): - """ Sets up a random user using the `mixer` package """ + """Sets up a random user using the `mixer` package""" return mixer.blend(Contact) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def contact_data(): - """ Builds a sample contact for completing a contact form """ + """Builds a sample contact for completing a contact form""" return { - 'first_name': 'Wayne', - 'last_name': 'Lambert', - 'email': 'wayne.lambert@example.com', - 'message': 'Test Message', - 'captcha': 'PASSED', -} + "first_name": "Wayne", + "last_name": "Lambert", + "email": "wayne.lambert@example.com", + "message": "Test Message", + "captcha": "PASSED", + } diff --git a/apps/contacts/tests/helpers.py b/apps/contacts/tests/helpers.py index a9301b47..2dc97d5e 100644 --- a/apps/contacts/tests/helpers.py +++ b/apps/contacts/tests/helpers.py @@ -3,20 +3,15 @@ form_data = [ # 01 - True as all valid field entries - ('Wayne', 'Lambert', 'wayne.lambert@example.com', 'Test Message', True, True), - + ("Wayne", "Lambert", "wayne.lambert@example.com", "Test Message", True, True), # 02 - False as invalid email address (missing `@`) - ('Wayne', 'Lambert', 'wayne.lambertexample.com', 'Test Message', True, False), - + ("Wayne", "Lambert", "wayne.lambertexample.com", "Test Message", True, False), # 03 - False as invalid email address (missing `.com`) - ('Wayne', 'Lambert', 'wayne.lambert@example', 'Test Message', True, False), - + ("Wayne", "Lambert", "wayne.lambert@example", "Test Message", True, False), # 04 - False as first name is mandatory, therefore cannot be blank - ('', 'Lambert', 'wayne.lambert@example.com', 'Test Message', True, False), - + ("", "Lambert", "wayne.lambert@example.com", "Test Message", True, False), # 05 - False as last name is mandatory, therefore cannot be blank - ('Wayne', '', 'wayne.lambert@example.com', 'Test Message', True, False), - + ("Wayne", "", "wayne.lambert@example.com", "Test Message", True, False), # 06 - True as even though the reCAPTCHA fails, Google's test keys for dev always pass - ('Wayne', 'Lambert', 'wayne.lambert@example.com', 'Test Message', False, True), + ("Wayne", "Lambert", "wayne.lambert@example.com", "Test Message", False, True), ] diff --git a/apps/contacts/tests/test_forms.py b/apps/contacts/tests/test_forms.py index 7313bb13..c05a62f0 100644 --- a/apps/contacts/tests/test_forms.py +++ b/apps/contacts/tests/test_forms.py @@ -6,22 +6,24 @@ class TestCandidateRegisterForm: - - field_list = ('first_name', 'last_name', 'email', 'message', 'captcha', 'validity') + field_list = ("first_name", "last_name", "email", "message", "captcha", "validity") @pytest.mark.parametrize(argnames=field_list, argvalues=form_data) def test_candidate_register_form( - self, first_name, last_name, email, message, captcha, validity): + self, first_name, last_name, email, message, captcha, validity + ): """ Tests that variations of a contact form entry behaves as expected. URL: https://pypi.org/project/django-recaptcha/#local-development-and-functional-testing """ - form = ContactForm(data={ - 'first_name': first_name, - 'last_name': last_name, - 'email': email, - 'message': message, - 'captcha': captcha, - }) + form = ContactForm( + data={ + "first_name": first_name, + "last_name": last_name, + "email": email, + "message": message, + "captcha": captcha, + } + ) - assert form.is_valid() is validity, 'Asserts valid form submission for input variation' + assert form.is_valid() is validity, "Asserts valid form submission for input variation" diff --git a/apps/contacts/tests/test_models.py b/apps/contacts/tests/test_models.py index c183076a..18579cdf 100644 --- a/apps/contacts/tests/test_models.py +++ b/apps/contacts/tests/test_models.py @@ -9,48 +9,49 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) + class TestContact: def test_single_contact_save(self, random_contact): - assert random_contact.pk == 1, 'Should create a `Contact` instance' + assert random_contact.pk == 1, "Should create a `Contact` instance" def test_multi_contact_saves(self): contacts = mixer.cycle(10).blend(Contact) - assert contacts[9].pk == 10, '10th instance should have a PK of 10' - assert Contact.objects.count() == 10, 'Should have 10 objects in the database' + assert contacts[9].pk == 10, "10th instance should have a PK of 10" + assert Contact.objects.count() == 10, "Should have 10 objects in the database" def test_can_delete_contact(self): contacts = mixer.cycle(10).blend(Contact) contacts[4].delete() - assert Contact.objects.count() == 9, \ - 'Should have 9 objects remaining in the database' + assert Contact.objects.count() == 9, "Should have 9 objects remaining in the database" def test_first_name_is_charfield(self, random_contact): field = random_contact._meta.get_field("first_name") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_last_name_is_charfield(self, random_contact): field = random_contact._meta.get_field("last_name") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_email_is_emailfield(self, random_contact): field = random_contact._meta.get_field("email") - assert isinstance(field, models.EmailField), 'Should be an email field' + assert isinstance(field, models.EmailField), "Should be an email field" def test_message_is_textfield(self, random_contact): field = random_contact._meta.get_field("message") - assert isinstance(field, models.TextField), 'Should be a text field' + assert isinstance(field, models.TextField), "Should be a text field" def test_submit_date_is_datetimefield(self, random_contact): field = random_contact._meta.get_field("submit_date") - assert isinstance(field, models.DateTimeField), 'Should be a datetime field' + assert isinstance(field, models.DateTimeField), "Should be a datetime field" def test_full_name(self): contact = mixer.blend(Contact) - contact.first_name = 'wayne' - contact.last_name = 'lambert' - assert contact.full_name == 'Wayne Lambert', \ - 'Should return concatenation of first and last name with capitalised first letters' + contact.first_name = "wayne" + contact.last_name = "lambert" + assert ( + contact.full_name == "Wayne Lambert" + ), "Should return concatenation of first and last name with capitalised first letters" def test_contact_str(self): - contact = mixer.blend(Contact, first_name='Wayne', last_name='Lambert') - assert contact.__str__() == contact.full_name, 'Str should be set to full_name property' + contact = mixer.blend(Contact, first_name="Wayne", last_name="Lambert") + assert contact.__str__() == contact.full_name, "Str should be set to full_name property" diff --git a/apps/contacts/tests/test_urls.py b/apps/contacts/tests/test_urls.py index 15f8b9fa..2191a939 100644 --- a/apps/contacts/tests/test_urls.py +++ b/apps/contacts/tests/test_urls.py @@ -4,13 +4,12 @@ class TestURLs: - def test_contact(self): - """ Verify that the `contact` url invokes intended view """ - path = reverse('contacts:contact') + """Verify that the `contact` url invokes intended view""" + path = reverse("contacts:contact") assert path, ContactFormView.as_view().__name__ def test_contact_submitted(self): - """ Verify that the `submitted` url invokes intended view """ - path = reverse('contacts:submitted') + """Verify that the `submitted` url invokes intended view""" + path = reverse("contacts:submitted") assert path, ContactSubmittedView.as_view().__name__ diff --git a/apps/contacts/tests/test_views.py b/apps/contacts/tests/test_views.py index dd8cd5c8..f8b567dd 100644 --- a/apps/contacts/tests/test_views.py +++ b/apps/contacts/tests/test_views.py @@ -11,35 +11,40 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) + +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestGetContactViews: def test_contact_form_view(self, rf, all_users): - """ Asserts any user can GET the `contact` form """ - path = reverse('contacts:contact') + """Asserts any user can GET the `contact` form""" + path = reverse("contacts:contact") request = rf.get(path) request.user = all_users response = ContactFormView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_contact_submitted_view(self, rf, all_users): - """ Asserts any user can GET the `submitted` page upon form submission """ - path = reverse('contacts:submitted') + """Asserts any user can GET the `submitted` page upon form submission""" + path = reverse("contacts:submitted") request = rf.get(path) request.user = all_users response = ContactSubmittedView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" class TestPostContactView: def test_contact_form_post_view(self, rf, contact_data): - """ Asserts a random visitor can POST a contact form """ - path = reverse('contacts:contact') + """Asserts a random visitor can POST a contact form""" + path = reverse("contacts:contact") request = rf.post(path, contact_data) request.user = AnonymousUser() add_middleware_to_request(request, SessionMiddleware) response = ContactFormView.as_view()(request, contact_data) - assert Contact.objects.count() == 1, 'Should have one contact in the DB' - assert response.status_code == 302, 'Should redirect' - assert '/submitted/' in response.url, \ - 'Should reach `submitted` url pattern -> `contact_submitted` template' + assert Contact.objects.count() == 1, "Should have one contact in the DB" + assert response.status_code == 302, "Should redirect" + assert ( + "/submitted/" in response.url + ), "Should reach `submitted` url pattern -> `contact_submitted` template" diff --git a/apps/contacts/urls.py b/apps/contacts/urls.py index 0f1f4445..497445ef 100644 --- a/apps/contacts/urls.py +++ b/apps/contacts/urls.py @@ -3,9 +3,9 @@ from apps.contacts.views import ContactFormView, ContactSubmittedView -app_name = 'contacts' +app_name = "contacts" urlpatterns = [ - path('', ContactFormView.as_view(), name='contact'), - path('submitted/', ContactSubmittedView.as_view(), name='submitted'), + path("", ContactFormView.as_view(), name="contact"), + path("submitted/", ContactSubmittedView.as_view(), name="submitted"), ] diff --git a/apps/contacts/views.py b/apps/contacts/views.py index cd9ac6b9..33066250 100644 --- a/apps/contacts/views.py +++ b/apps/contacts/views.py @@ -8,19 +8,19 @@ class ContactFormView(FormView): form_class = ContactForm - template_name = 'contact.html' - success_url = reverse_lazy('contacts:submitted') + template_name = "contact.html" + success_url = reverse_lazy("contacts:submitted") def form_valid(self, form): - first_name = form.cleaned_data.get('first_name').capitalize() - last_name = form.cleaned_data.get('last_name').capitalize() - email = form.cleaned_data.get('email').casefold() - message = form.cleaned_data.get('message') + first_name = form.cleaned_data.get("first_name").capitalize() + last_name = form.cleaned_data.get("last_name").capitalize() + email = form.cleaned_data.get("email").casefold() + message = form.cleaned_data.get("message") send_mail( subject=f"Contact Form - from {first_name} {last_name}", message=message, from_email=settings.DEFAULT_FROM_EMAIL_SES, - recipient_list=['wayne.a.lambert@gmail.com', email], + recipient_list=["wayne.a.lambert@gmail.com", email], fail_silently=False, ) form.save() @@ -28,4 +28,4 @@ def form_valid(self, form): class ContactSubmittedView(TemplateView): - template_name = 'contact_submitted.html' + template_name = "contact_submitted.html" diff --git a/apps/countdown_letters/admin.py b/apps/countdown_letters/admin.py index 5c9389ef..43534dbe 100644 --- a/apps/countdown_letters/admin.py +++ b/apps/countdown_letters/admin.py @@ -7,36 +7,33 @@ class LettersGameAdmin(admin.ModelAdmin): model = LettersGame - list_display = ( - 'letters_chosen', 'players_word', 'comp_word', 'winning_word', 'entry_date') - ordering = ('-entry_date',) + list_display = ("letters_chosen", "players_word", "comp_word", "winning_word", "entry_date") + ordering = ("-entry_date",) readonly_fields = [field.name for field in LettersGame._meta.get_fields()] formfield_overrides = { - models.CharField: {'widget': TextInput(attrs={'size': '20'})}, - models.TextField: {'widget': Textarea(attrs={'rows': 3, 'cols': 60})}, - models.IntegerField: {'widget': TextInput(attrs={'size': '2'})}, + models.CharField: {"widget": TextInput(attrs={"size": "20"})}, + models.TextField: {"widget": Textarea(attrs={"rows": 3, "cols": 60})}, + models.IntegerField: {"widget": TextInput(attrs={"size": "2"})}, } fieldsets = ( - ('Selection', { - 'fields': ['letters_chosen'] - }), - ('Game', { - 'fields': (('players_word', 'eligible_answer', 'comp_word', 'winning_word')) - }), - ('Results', { - 'fields': ( - ('player_word_len', 'player_score'), - ('comp_word_len', 'comp_score') - ) - }), - ('Definition', { - 'fields': ('word_class', 'definition',) - }), - ('Dates', { - 'fields': ('entry_date', ) - }), + ("Selection", {"fields": ["letters_chosen"]}), + ("Game", {"fields": (("players_word", "eligible_answer", "comp_word", "winning_word"))}), + ( + "Results", + {"fields": (("player_word_len", "player_score"), ("comp_word_len", "comp_score"))}, + ), + ( + "Definition", + { + "fields": ( + "word_class", + "definition", + ) + }, + ), + ("Dates", {"fields": ("entry_date",)}), ) diff --git a/apps/countdown_letters/apps.py b/apps/countdown_letters/apps.py index d384678f..79ace24c 100644 --- a/apps/countdown_letters/apps.py +++ b/apps/countdown_letters/apps.py @@ -2,5 +2,5 @@ class CountdownLettersConfig(AppConfig): - name = 'apps.countdown_letters' - verbose_name = 'Countdown Letters' + name = "apps.countdown_letters" + verbose_name = "Countdown Letters" diff --git a/apps/countdown_letters/forms.py b/apps/countdown_letters/forms.py index 0e0811f2..129db277 100644 --- a/apps/countdown_letters/forms.py +++ b/apps/countdown_letters/forms.py @@ -2,14 +2,13 @@ class LetterSelectionForm(forms.Form): - num_vowels_selected = forms.IntegerField( - required=True, label='', min_value=3, max_value=5) + num_vowels_selected = forms.IntegerField(required=True, label="", min_value=3, max_value=5) class SelectedLettersForm(forms.Form): players_word = forms.CharField( required=True, - label='', + label="", strip=True, - widget=forms.TextInput(attrs={'placeholder': 'Enter your word...'}) + widget=forms.TextInput(attrs={"placeholder": "Enter your word..."}), ) diff --git a/apps/countdown_letters/logic.py b/apps/countdown_letters/logic.py index 394632a5..b2347aaf 100644 --- a/apps/countdown_letters/logic.py +++ b/apps/countdown_letters/logic.py @@ -8,7 +8,7 @@ from collections import Counter from random import choices, random -from typing import Dict +from typing import Dict, List, Set import requests @@ -22,58 +22,58 @@ class GameSetup: Sets up a game with the standard attributes of a game as at the game's starting point. """ + MAX_GAME_LETTERS: int = 9 @staticmethod - def get_weighted_vowels() -> list: + def get_weighted_vowels() -> List: """ - Creates a list of vowel letters with the number of vowels - required to produce the weighted distribution of the various - vowels for the game. + Compiles vowel letters with number of vowels required to produce + weighted distribution of the various vowels for the game. """ vowel_freq: Dict[str, int] = { - 'A': 15, - 'E': 21, - 'I': 13, - 'O': 13, - 'U': 5, + "A": 15, + "E": 21, + "I": 13, + "O": 13, + "U": 5, } - s: str = '' + s: str = "" for key, value in vowel_freq.items(): s += key * value return list(s) @staticmethod - def get_weighted_consonants() -> list: + def get_weighted_consonants() -> List: """ - Creates a list of consonant letters with the number of - consonants required to produce the weighted distribution of the - various consonants for the game. + Compiles consonant letters with the number of consonants + required to produce the weighted distribution of the various + consonants for the game. """ consonant_freq: Dict[str, int] = { - 'B': 2, - 'C': 3, - 'D': 6, - 'F': 2, - 'G': 3, - 'H': 2, - 'J': 1, - 'K': 1, - 'L': 5, - 'M': 4, - 'N': 8, - 'P': 4, - 'Q': 1, - 'R': 9, - 'S': 9, - 'T': 9, - 'V': 1, - 'W': 1, - 'X': 1, - 'Y': 1, - 'Z': 1, + "B": 2, + "C": 3, + "D": 6, + "F": 2, + "G": 3, + "H": 2, + "J": 1, + "K": 1, + "L": 5, + "M": 4, + "N": 8, + "P": 4, + "Q": 1, + "R": 9, + "S": 9, + "T": 9, + "V": 1, + "W": 1, + "X": 1, + "Y": 1, + "Z": 1, } - s: str = '' + s: str = "" for key, value in consonant_freq.items(): s += key * value return list(s) @@ -101,43 +101,45 @@ def get_letters_chosen(num_vowels: int) -> str: letters_chosen.append(consonant_picked) letters_chosen = sorted(letters_chosen, key=lambda k: random()) - return ''.join([item for list_item in letters_chosen for item in list_item]) + return "".join([item for list_item in letters_chosen for item in list_item]) -def get_words() -> set: +def get_words() -> Set: """ Returns all words from the `words.txt` file as a set Source: http://www.mieliestronk.com/corncob_lowercase.txt Words > 9 chars in length removed """ - words_filename = os.path.join(APPS_DIR, 'countdown_letters/words.txt') - with open(words_filename, 'r') as words_file: - words_set = {word.strip('\n') for word in words_file} + words_filename = os.path.join(APPS_DIR, "countdown_letters/words.txt") + with open(words_filename, "r") as words_file: + words_set = {word.strip("\n") for word in words_file} return words_set -def get_shortlisted_words(words: set, letters: str) -> list: +def get_shortlisted_words(words: Set, letters: str) -> List: """ Given a set of words and a string of the game's letters, returns a list of accumulatively gathered longest words sorted by word length """ - d = {} + longest_words = {} cumulative_max_letter_count = 0 letters_in_selection = list(letters) for tested_word in words: letters_in_tested_word = list(tested_word.upper()) if len(letters_in_tested_word) < len(letters_in_selection): common_letters = list( - (Counter(letters_in_selection) & Counter(letters_in_tested_word)).elements()) + (Counter(letters_in_selection) & Counter(letters_in_tested_word)).elements() + ) letter_count = len(common_letters) - if letter_count >= cumulative_max_letter_count and len( - tested_word) == len(common_letters): + if letter_count >= cumulative_max_letter_count and len(tested_word) == len( + common_letters + ): cumulative_max_letter_count = letter_count - d[tested_word] = cumulative_max_letter_count - return sorted(d.items(), key=lambda x: x[1], reverse=True) + longest_words[tested_word] = cumulative_max_letter_count + return sorted(longest_words.items(), key=lambda x: x[1], reverse=True) -def get_longest_possible_word(shortlisted_words: list) -> str: +def get_longest_possible_word(shortlisted_words: List) -> str: """ Given a shortlisted list of tuples, returns the word at the first indexed position @@ -150,11 +152,11 @@ def get_longest_possible_word(shortlisted_words: list) -> str: def get_game_score(word_len: int) -> int: - """ Retrieves the game score based on the achieved word length """ + """Retrieves the game score based on the achieved word length""" return word_len * 2 if word_len == 9 else word_len -def get_lemmas_response_json(word: str) -> dict: +def get_lemmas_response_json(word: str) -> Dict: """ Returns lemmas data component of given `word` from Oxford Online API The `lemmas` endpoint is used to determine presence in the dictionary. @@ -164,34 +166,35 @@ def get_lemmas_response_json(word: str) -> dict: return lemmas_response.json() -def lookup_definition_data(word: str) -> dict: +def lookup_definition_data(word: str) -> Dict: """ Retrieve dictionary definition of winning word using 'Oxford Dictionaries API'. """ - response = requests.get(url=API.WORDS_URL, params={'q': word}, headers=API.headers) + response = requests.get(url=API.WORDS_URL, params={"q": word}, headers=API.headers) if response.status_code == 200: try: json = response.json() - idx = 0 if json['results'][0]['type'] == 'headword' else 1 - d = json['results'][idx]['lexicalEntries'][0]['entries'][0]['senses'][0] - definition = d['definitions'][0].capitalize() - word_class = json['results'][0]['lexicalEntries'][idx]['lexicalCategory']['text'] + idx = 0 if json["results"][0]["type"] == "headword" else 1 + d = json["results"][idx]["lexicalEntries"][0]["entries"][0]["senses"][0] + definition = d["definitions"][0].capitalize() + word_class = json["results"][0]["lexicalEntries"][idx]["lexicalCategory"]["text"] except KeyError: - definition = (f"The definition for '{word}' cannot be found " + - "in the Oxford Dictionaries API.") - word_class = 'N/A' + definition = ( + f"The definition for '{word}' cannot be found " + "in the Oxford Dictionaries API." + ) + word_class = "N/A" return { - 'definition': definition, - 'word_class': word_class, + "definition": definition, + "word_class": word_class, } def get_result(player_word: str, comp_word: str) -> str: - """ Returns the winning player for the game """ + """Returns the winning player for the game""" if len(player_word) > len(comp_word): - return 'You win' + return "You win" elif len(player_word) < len(comp_word): - return 'Susie wins' - return 'Draw' + return "Susie wins" + return "Draw" diff --git a/apps/countdown_letters/migrations/0001_initial.py b/apps/countdown_letters/migrations/0001_initial.py index 9a07b391..afcda800 100644 --- a/apps/countdown_letters/migrations/0001_initial.py +++ b/apps/countdown_letters/migrations/0001_initial.py @@ -4,33 +4,36 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='LettersGame', + name="LettersGame", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('letters_chosen', models.CharField(max_length=9)), - ('players_word', models.CharField(max_length=9)), - ('comp_word', models.CharField(max_length=9)), - ('eligible_answer', models.BooleanField(default=False)), - ('winning_word', models.CharField(max_length=9)), - ('player_word_len', models.IntegerField(default=0)), - ('comp_word_len', models.IntegerField(default=0)), - ('player_score', models.IntegerField(default=0)), - ('comp_score', models.IntegerField(default=0)), - ('definition', models.TextField()), - ('word_class', models.CharField(max_length=255)), - ('result', models.CharField(max_length=255)), - ('entry_date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("letters_chosen", models.CharField(max_length=9)), + ("players_word", models.CharField(max_length=9)), + ("comp_word", models.CharField(max_length=9)), + ("eligible_answer", models.BooleanField(default=False)), + ("winning_word", models.CharField(max_length=9)), + ("player_word_len", models.IntegerField(default=0)), + ("comp_word_len", models.IntegerField(default=0)), + ("player_score", models.IntegerField(default=0)), + ("comp_score", models.IntegerField(default=0)), + ("definition", models.TextField()), + ("word_class", models.CharField(max_length=255)), + ("result", models.CharField(max_length=255)), + ("entry_date", models.DateTimeField(auto_now_add=True)), ], options={ - 'ordering': ('-entry_date',), + "ordering": ("-entry_date",), }, ), ] diff --git a/apps/countdown_letters/models.py b/apps/countdown_letters/models.py index f3b5f21f..94406948 100644 --- a/apps/countdown_letters/models.py +++ b/apps/countdown_letters/models.py @@ -17,7 +17,7 @@ class LettersGame(models.Model): entry_date = models.DateTimeField(auto_now_add=True) class Meta: - ordering = ('-entry_date', ) + ordering = ("-entry_date",) @property def entry_year(self) -> int: diff --git a/apps/countdown_letters/oxford_api.py b/apps/countdown_letters/oxford_api.py index 1a4380d1..f0416444 100644 --- a/apps/countdown_letters/oxford_api.py +++ b/apps/countdown_letters/oxford_api.py @@ -13,10 +13,11 @@ class API: """ Handles the API configuration for the Online Oxford Dictionary API. """ + headers = { "Accept": "application/json", - "app_id": os.environ['OD_APPLICATION_ID'], - "app_key": os.environ['OD_APPLICATION_KEY_1'], + "app_id": os.environ["OD_APPLICATION_ID"], + "app_key": os.environ["OD_APPLICATION_KEY_1"], } ENTRIES_URL = f"{os.environ['OD_API_BASE_URL']}{'entries/en-gb/'}" LEMMAS_URL = f"{os.environ['OD_API_BASE_URL']}{'lemmas/en-gb/'}" diff --git a/apps/countdown_letters/tests/conftest.py b/apps/countdown_letters/tests/conftest.py index 1534b6b8..39d2f4e9 100644 --- a/apps/countdown_letters/tests/conftest.py +++ b/apps/countdown_letters/tests/conftest.py @@ -1,5 +1,7 @@ """ Fixtures to facilitate the testing of the Countdown Letters app """ +from typing import Dict, List + import pytest from mixer.backend.django import mixer @@ -7,76 +9,212 @@ from apps.countdown_letters.models import LettersGame -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def vcr_config(): - """ Replaces the Oxford Online API request headers for `app_id` and - `app_key` with "DUMMY" in cassettes """ + """Replaces the Oxford Online API request headers for `app_id` and + `app_key` with "DUMMY" in cassettes""" return { - "filter_headers": [ - ('app_id', 'DUMMY'), - ('app_key', 'DUMMY') - ], + "filter_headers": [("app_id", "DUMMY"), ("app_key", "DUMMY")], } -@pytest.fixture(scope='function') -def expected_vowels_list() -> list: +@pytest.fixture(scope="function") +def expected_vowels_list() -> List: return [ - 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', - 'A', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', - 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'I', 'I', - 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'O', 'O', 'O', 'O', 'O', 'O', 'O', - 'O', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'U' + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "A", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "E", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "I", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "O", + "U", + "U", + "U", + "U", + "U", ] -@pytest.fixture(scope='function') -def expected_consonants_list() -> list: + +@pytest.fixture(scope="function") +def expected_consonants_list() -> List: return [ - 'B', 'B', 'C', 'C', 'C', 'D', 'D', 'D', 'D', 'D', 'D', 'F', 'F', 'G', - 'G', 'G', 'H', 'H', 'J', 'K', 'L', 'L', 'L', 'L', 'L', 'M', 'M', 'M', - 'M', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'P', 'P', 'P', 'P', 'Q', - 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'S', 'S', 'S', 'S', 'S', - 'S', 'S', 'S', 'S', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'V', - 'W', 'X', 'Y', 'Z' + "B", + "B", + "C", + "C", + "C", + "D", + "D", + "D", + "D", + "D", + "D", + "F", + "F", + "G", + "G", + "G", + "H", + "H", + "J", + "K", + "L", + "L", + "L", + "L", + "L", + "M", + "M", + "M", + "M", + "N", + "N", + "N", + "N", + "N", + "N", + "N", + "N", + "P", + "P", + "P", + "P", + "Q", + "R", + "R", + "R", + "R", + "R", + "R", + "R", + "R", + "R", + "S", + "S", + "S", + "S", + "S", + "S", + "S", + "S", + "S", + "T", + "T", + "T", + "T", + "T", + "T", + "T", + "T", + "T", + "V", + "W", + "X", + "Y", + "Z", ] -@pytest.fixture(scope='function') -def shortlisted_words() -> list: +@pytest.fixture(scope="function") +def shortlisted_words() -> List: return [ - ('strode', 6), - ('stored', 6), - ('droves', 6), - ('strove', 6), - ('voters', 6), - ('doters', 6), - ('troves', 6), - ('stover', 6), - ('sorted', 6), - ('roves', 5), - ('redos', 5), - ('overs', 5), - ('revs', 4), - ('rode', 4) + ("strode", 6), + ("stored", 6), + ("droves", 6), + ("strove", 6), + ("voters", 6), + ("doters", 6), + ("troves", 6), + ("stover", 6), + ("sorted", 6), + ("roves", 5), + ("redos", 5), + ("overs", 5), + ("revs", 4), + ("rode", 4), ] -@pytest.fixture(scope='function') -def false_shortlisted_words() -> list: + +@pytest.fixture(scope="function") +def false_shortlisted_words() -> List[Dict[str, int]]: return [ - ('monseiur', 8), - ('bonjuor', 7), - ('bonsior', 7), - ('maadme', 6), + ("monseiur", 8), + ("bonjuor", 7), + ("bonsior", 7), + ("maadme", 6), ] -@pytest.fixture(scope='function') -def given_answers_list() -> list: +@pytest.fixture(scope="function") +def given_answers_list() -> List: return [ - 'sorted', 'tree', 'sort', 'strove', # Should all return True - 'sarted', 'traa', 'sart', 'strave', # Should all return False + "sorted", + "tree", + "sort", + "strove", # Should all return True + "sarted", + "traa", + "sart", + "strave", # Should all return False ] -@pytest.fixture(scope='function') + +@pytest.fixture(scope="function") def letters_game(): return mixer.blend(LettersGame, pk=1) diff --git a/apps/countdown_letters/tests/test_logic.py b/apps/countdown_letters/tests/test_logic.py index e7a36d6f..ab58c7f1 100644 --- a/apps/countdown_letters/tests/test_logic.py +++ b/apps/countdown_letters/tests/test_logic.py @@ -12,11 +12,10 @@ def test_get_weighted_vowels(self, expected_vowels_list: list): Tests the vowels returned from the function matches the actual allocation of each vowel according to the rules of the game. """ - assert isinstance(expected_vowels_list, list), 'Fixture should be a list' + assert isinstance(expected_vowels_list, list), "Fixture should be a list" function_vowels_list = logic.GameSetup.get_weighted_vowels() - assert isinstance(function_vowels_list, list), 'Should be a list instance' - assert expected_vowels_list == function_vowels_list, \ - "Vowels list should meet game's rules" + assert isinstance(function_vowels_list, list), "Should be a list instance" + assert expected_vowels_list == function_vowels_list, "Vowels list should meet game's rules" def test_get_weighted_consonants(self, expected_consonants_list: list): """ @@ -24,21 +23,22 @@ def test_get_weighted_consonants(self, expected_consonants_list: list): actual allocation of each consonant according to the rules of the game. """ - assert isinstance(expected_consonants_list, list), 'Fixture should be a list' + assert isinstance(expected_consonants_list, list), "Fixture should be a list" function_consonants_list = logic.GameSetup.get_weighted_consonants() assert isinstance(function_consonants_list, list) - assert expected_consonants_list == function_consonants_list, \ - "Consonants list should meet game's rules" + assert ( + expected_consonants_list == function_consonants_list + ), "Consonants list should meet game's rules" -@pytest.mark.parametrize(argnames='num_vowels', argvalues=[3, 4, 5]) +@pytest.mark.parametrize(argnames="num_vowels", argvalues=[3, 4, 5]) def test_get_letters_chosen(num_vowels): """ Test that the letter chosen function returns an appropriate string of letters based upon the player's chosen number of vowels. """ letters_chosen = logic.get_letters_chosen(num_vowels) - vowels = ['A', 'E', 'O', 'I', 'U'] + vowels = ["A", "E", "O", "I", "U"] num_of_vowels, num_of_consontants = 0, 0 for letter in letters_chosen: for vowel in vowels: @@ -59,28 +59,28 @@ def test_get_words(): unique words by default. """ words = logic.get_words() - assert isinstance(words, set), 'Should be a set' - assert len(words) == 40_424, 'Should have 40,424 words' + assert isinstance(words, set), "Should be a set" + assert len(words) == 40_424, "Should have 40,424 words" def test_get_shortlisted_words(): - """ Asserts the shortlisted words dict has at least one key """ + """Asserts the shortlisted words dict has at least one key""" words = logic.get_words() - assert isinstance(words, set), 'First input to get_shortlisted_words() should be a set' - shortlisted_words = logic.get_shortlisted_words(words, 'AEIBCDFGH') + assert isinstance(words, set), "First input to get_shortlisted_words() should be a set" + shortlisted_words = logic.get_shortlisted_words(words, "AEIBCDFGH") assert isinstance(shortlisted_words, list) - assert len(shortlisted_words) >= 1, 'Should have at least one element' + assert len(shortlisted_words) >= 1, "Should have at least one element" @pytest.mark.vcr() def test_get_longest_possible_word(shortlisted_words: list): - """ Asserts one of the longest possible words is a string """ + """Asserts one of the longest possible words is a string""" longest_possible_word = logic.get_longest_possible_word(shortlisted_words) - assert isinstance(shortlisted_words, list), 'Fixture should be set up correctly as a list' - assert longest_possible_word, 'Should exist' - assert len(longest_possible_word) <= 9, 'Should be less than or equal to 9 characters' - assert isinstance(longest_possible_word, str), 'Should be a string' - assert longest_possible_word.isupper, 'Should be uppercase' + assert isinstance(shortlisted_words, list), "Fixture should be set up correctly as a list" + assert longest_possible_word, "Should exist" + assert len(longest_possible_word) <= 9, "Should be less than or equal to 9 characters" + assert isinstance(longest_possible_word, str), "Should be a string" + assert longest_possible_word.isupper, "Should be uppercase" @pytest.mark.vcr() @@ -90,13 +90,13 @@ def test_get_longest_possible_word_returns_none(false_shortlisted_words: list): the shortlisted words are in the Oxford Online API """ longest_possible_word = logic.get_longest_possible_word(false_shortlisted_words) - assert isinstance(false_shortlisted_words, list), 'Fixture should be a list' - assert longest_possible_word is None, 'Should not exist' + assert isinstance(false_shortlisted_words, list), "Fixture should be a list" + assert longest_possible_word is None, "Should not exist" @given(word_len=st.integers(min_value=1, max_value=9)) def test_get_game_score(word_len: int): - """ Asserts the correct game score is returned """ + """Asserts the correct game score is returned""" game_score = logic.get_game_score(word_len) if word_len == 9: assert game_score == 18 @@ -105,8 +105,8 @@ def test_get_game_score(word_len: int): assert isinstance(game_score, int) -@pytest.mark.slow(reason='Processing makes 2 calls to the Oxford Online API') -@pytest.mark.parametrize(argnames='word', argvalues=['strive', 'strove']) +@pytest.mark.slow(reason="Processing makes 2 calls to the Oxford Online API") +@pytest.mark.parametrize(argnames="word", argvalues=["strive", "strove"]) @pytest.mark.vcr() def test_get_lemmas_response_json(word: str): """ @@ -116,12 +116,12 @@ def test_get_lemmas_response_json(word: str): assert isinstance(word, str) lemmas_json = logic.get_lemmas_response_json(word) assert isinstance(lemmas_json, dict) - assert len(lemmas_json.keys()) == 2, 'Should be 2 keys in dict' + assert len(lemmas_json.keys()) == 2, "Should be 2 keys in dict" -@pytest.mark.slow(reason='Processing makes a call to the Oxford Dictionaries API') +@pytest.mark.slow(reason="Processing makes a call to the Oxford Dictionaries API") @pytest.mark.vcr() -def test_lookup_definition_data_valid_word(word: str = 'strove'): +def test_lookup_definition_data_valid_word(word: str = "strove"): """ Asserts that given an example of a word in its alternative form (i.e. past tense form or plural verbs form), tests the definitions @@ -136,13 +136,13 @@ def test_lookup_definition_data_valid_word(word: str = 'strove'): """ definition = logic.lookup_definition_data(word) assert isinstance(definition, dict) - assert len(definition.keys()) == 2, 'Set up to return 2 key/value pairs' - assert definition['definition'] == 'Make great efforts to achieve or obtain something' - assert definition['word_class'] == 'Verb' + assert len(definition.keys()) == 2, "Set up to return 2 key/value pairs" + assert definition["definition"] == "Make great efforts to achieve or obtain something" + assert definition["word_class"] == "Verb" -@pytest.mark.slow(reason='Processing makes a call to the Oxford Dictionaries API') -def test_lookup_definition_data_invalid_word(word: str = 'bonjourno'): +@pytest.mark.slow(reason="Processing makes a call to the Oxford Dictionaries API") +def test_lookup_definition_data_invalid_word(word: str = "bonjourno"): """ Asserts that the definition dictionary is not instantiated when given an example of a word that isn't within the Dictionary API. @@ -157,11 +157,11 @@ def test_get_result(): of the achieved word lengths for the player and the computer. At this stage, the words have been validated for eligibility. """ - result = logic.get_result(player_word='this', comp_word='the') - assert result == 'You win' + result = logic.get_result(player_word="this", comp_word="the") + assert result == "You win" - result = logic.get_result(player_word='the', comp_word='this') - assert result == 'Susie wins' + result = logic.get_result(player_word="the", comp_word="this") + assert result == "Susie wins" - result = logic.get_result(player_word='the', comp_word='the') - assert result == 'Draw' + result = logic.get_result(player_word="the", comp_word="the") + assert result == "Draw" diff --git a/apps/countdown_letters/tests/test_models.py b/apps/countdown_letters/tests/test_models.py index 47eb3fe4..f879e477 100644 --- a/apps/countdown_letters/tests/test_models.py +++ b/apps/countdown_letters/tests/test_models.py @@ -18,71 +18,73 @@ class TestLettersGame: of the attributes within the `LettersGame` class. This has been generated by the `mixer` package. """ + def test_single_letters_game_saves(self, letters_game): - assert letters_game.pk == 1, 'Should create a `Letters letters_game` instance' + assert letters_game.pk == 1, "Should create a `Letters letters_game` instance" def test_multi_letters_game_saves(self): games = mixer.cycle(10).blend(LettersGame) - assert games[9].pk == 10, '10th instance should have a PK of 10' - assert LettersGame.objects.count() == 10, 'Should have 10 objects in the database' + assert games[9].pk == 10, "10th instance should have a PK of 10" + assert LettersGame.objects.count() == 10, "Should have 10 objects in the database" def test_can_delete_letters_game(self): games = mixer.cycle(10).blend(LettersGame) games[4].delete() - assert LettersGame.objects.count() == 9, 'Should have 9 objects remaining in the database' + assert LettersGame.objects.count() == 9, "Should have 9 objects remaining in the database" def test_letters_chosen_is_charfield(self, letters_game): field = letters_game._meta.get_field("letters_chosen") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_players_word_is_charfield(self, letters_game): field = letters_game._meta.get_field("players_word") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_comp_word_is_charfield(self, letters_game): field = letters_game._meta.get_field("comp_word") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_eligible_answer_is_charfield(self, letters_game): field = letters_game._meta.get_field("eligible_answer") - assert isinstance(field, models.BooleanField), 'Should be a boolean field' + assert isinstance(field, models.BooleanField), "Should be a boolean field" def test_winning_word_is_charfield(self, letters_game): field = letters_game._meta.get_field("winning_word") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_player_word_len_is_integerfield(self, letters_game): field = letters_game._meta.get_field("player_word_len") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_comp_word_len_is_integerfield(self, letters_game): field = letters_game._meta.get_field("comp_word_len") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_player_score_is_integerfield(self, letters_game): field = letters_game._meta.get_field("player_score") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_comp_score_is_integerfield(self, letters_game): field = letters_game._meta.get_field("comp_score") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_definition_is_textfield(self, letters_game): field = letters_game._meta.get_field("definition") - assert isinstance(field, models.TextField), 'Should be a text field' + assert isinstance(field, models.TextField), "Should be a text field" def test_word_class_is_charfield(self, letters_game): field = letters_game._meta.get_field("word_class") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_result_is_charfield(self, letters_game): field = letters_game._meta.get_field("result") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_entry_date_is_datefield(self, letters_game): field = letters_game._meta.get_field("entry_date") - assert isinstance(field, models.DateField), 'Should be a date field' + assert isinstance(field, models.DateField), "Should be a date field" def test_entry_year(self, letters_game): - assert letters_game.entry_year == letters_game.entry_date.year, \ - "Year should be the same as the entry date's year property" + assert ( + letters_game.entry_year == letters_game.entry_date.year + ), "Year should be the same as the entry date's year property" diff --git a/apps/countdown_letters/tests/test_urls.py b/apps/countdown_letters/tests/test_urls.py index eb9a2cd7..c023738e 100644 --- a/apps/countdown_letters/tests/test_urls.py +++ b/apps/countdown_letters/tests/test_urls.py @@ -2,16 +2,18 @@ def test_selection_screen(): - """ Verify that the `selection` url invokes intended view """ - resolver = resolve(reverse('countdown_letters:selection')) - assert resolver.view_name, 'selection_screen' + """Verify that the `selection` url invokes intended view""" + resolver = resolve(reverse("countdown_letters:selection")) + assert resolver.view_name, "selection_screen" + def test_game_screen(): - """ Verify that the `game` url invokes intended view """ - resolver = resolve(reverse('countdown_letters:game')) - assert resolver.view_name, 'game_screen' + """Verify that the `game` url invokes intended view""" + resolver = resolve(reverse("countdown_letters:game")) + assert resolver.view_name, "game_screen" + def test_results_screen(): - """ Verify that the `results` url invokes intended view """ - resolver = resolve(reverse('countdown_letters:results')) - assert resolver.view_name, 'results_screen' + """Verify that the `results` url invokes intended view""" + resolver = resolve(reverse("countdown_letters:results")) + assert resolver.view_name, "results_screen" diff --git a/apps/countdown_letters/tests/test_utils.py b/apps/countdown_letters/tests/test_utils.py index 243165d0..2ea5308f 100644 --- a/apps/countdown_letters/tests/test_utils.py +++ b/apps/countdown_letters/tests/test_utils.py @@ -3,21 +3,23 @@ class TestURL: def test_build_game_screen_url(self): - """ Tests that the game screens' URL is built correctly """ + """Tests that the game screens' URL is built correctly""" game_screen_url = utils.build_game_screen_url(num_vowels_selected=3) - path = 'countdown-letters/game/?letters_chosen=' - assert path in game_screen_url, \ - 'specified path including query param should be within the URL' + path = "countdown-letters/game/?letters_chosen=" + assert ( + path in game_screen_url + ), "specified path including query param should be within the URL" letters_chosen_in_url = game_screen_url[-9:] full_url = f"{path}{letters_chosen_in_url}" assert full_url in game_screen_url def test_build_results_screen_url(self): - """ Tests that the results screens' URL is built correctly """ + """Tests that the results screens' URL is built correctly""" results_screen_url = utils.build_results_screen_url( - letters_chosen='ABCDEFGHI', players_word='BAD') - path = 'countdown-letters/results/?letters_chosen=' + letters_chosen="ABCDEFGHI", players_word="BAD" + ) + path = "countdown-letters/results/?letters_chosen=" assert path in results_screen_url - assert 'players_word=' in results_screen_url - assert 'ABCDEFGHI' in results_screen_url - assert 'BAD' in results_screen_url + assert "players_word=" in results_screen_url + assert "ABCDEFGHI" in results_screen_url + assert "BAD" in results_screen_url diff --git a/apps/countdown_letters/tests/test_validations.py b/apps/countdown_letters/tests/test_validations.py index 94f847e8..1b14c63e 100644 --- a/apps/countdown_letters/tests/test_validations.py +++ b/apps/countdown_letters/tests/test_validations.py @@ -3,19 +3,19 @@ from apps.countdown_letters import validations -@pytest.mark.slow(reason='Processing makes 2 calls to the Oxford Online API') -@pytest.mark.parametrize(argnames='test_word', argvalues=['random', 'radnom']) +@pytest.mark.slow(reason="Processing makes 2 calls to the Oxford Online API") +@pytest.mark.parametrize(argnames="test_word", argvalues=["random", "radnom"]) @pytest.mark.vcr() def test_is_in_oxford_api(test_word: str): - """ Asserts that given a valid word, `True` is returned else `False` """ + """Asserts that given a valid word, `True` is returned else `False`""" in_api = validations.is_in_oxford_api(test_word) - if test_word == 'random': + if test_word == "random": assert in_api - if test_word == 'radnom': # Misspelt on purpose + if test_word == "radnom": # Misspelt on purpose assert not in_api -def test_is_eligible_answer(given_answers_list, letters: str = 'SVEODRETR'): +def test_is_eligible_answer(given_answers_list, letters: str = "SVEODRETR"): """ Asserts that given a list fixture containing 8 words, the first 4 words pass the eligibility tests since all of their letters are diff --git a/apps/countdown_letters/tests/test_views.py b/apps/countdown_letters/tests/test_views.py index 11b09641..70a09b69 100644 --- a/apps/countdown_letters/tests/test_views.py +++ b/apps/countdown_letters/tests/test_views.py @@ -5,50 +5,51 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) + def test_get_selection_screen(client): - """ Asserts a site visitor can GET the `selection` screen """ - path = reverse('countdown_letters:selection') + """Asserts a site visitor can GET the `selection` screen""" + path = reverse("countdown_letters:selection") response = client.get(path) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" + -@pytest.mark.parametrize(argnames='num_vowels_selected', argvalues=[3, 4, 5]) +@pytest.mark.parametrize(argnames="num_vowels_selected", argvalues=[3, 4, 5]) def test_post_selection_screen(client, num_vowels_selected): - """ Asserts a site visitor can POST from the `selection` screen """ - path = reverse('countdown_letters:selection') - data = {'num_vowels_selected': num_vowels_selected} + """Asserts a site visitor can POST from the `selection` screen""" + path = reverse("countdown_letters:selection") + data = {"num_vowels_selected": num_vowels_selected} response = client.post(path, data) - assert response.status_code == 302, 'Should return a redirection status code' + assert response.status_code == 302, "Should return a redirection status code" + -@pytest.mark.parametrize(argnames='num_vowels', argvalues=[1, 2, 6, 7]) +@pytest.mark.parametrize(argnames="num_vowels", argvalues=[1, 2, 6, 7]) def test_form_not_valid(client, num_vowels): - """ Asserts a site visitor returns to the selection screen """ - path = reverse('countdown_letters:selection') - data = {'num_vowels': num_vowels} + """Asserts a site visitor returns to the selection screen""" + path = reverse("countdown_letters:selection") + data = {"num_vowels": num_vowels} response = client.post(path, data) - assert not response.context['form'].is_valid() - assert response.context['widget']['attrs']['min'] == 3 - assert response.context['widget']['attrs']['max'] == 5 - assert response.status_code == 200, 'Should return an `OK` status code' + assert not response.context["form"].is_valid() + assert response.context["widget"]["attrs"]["min"] == 3 + assert response.context["widget"]["attrs"]["max"] == 5 + assert response.status_code == 200, "Should return an `OK` status code" + def test_get_game_screen(client): - """ Asserts a site visitor can GET the `game` screen """ - base_path = reverse('countdown_letters:game') - params = { - 'letters_chosen': 'ABCDEFGHI' - } + """Asserts a site visitor can GET the `game` screen""" + base_path = reverse("countdown_letters:game") + params = {"letters_chosen": "ABCDEFGHI"} response = client.get(base_path, params) - assert response.status_code == 200, 'Should return an `OK` status code' - assert 'The letters selected' in response.content.decode('utf-8'), \ - 'Should contain specified text' + assert response.status_code == 200, "Should return an `OK` status code" + assert "The letters selected" in response.content.decode( + "utf-8" + ), "Should contain specified text" + @pytest.mark.vcr() def test_get_results_screen(client): - """ Asserts a site visitor can GET the `results` screen """ - path = reverse('countdown_letters:results') - params = { - 'letters_chosen': 'SWIMMINGS', - 'players_word': 'SWIMMING' - } + """Asserts a site visitor can GET the `results` screen""" + path = reverse("countdown_letters:results") + params = {"letters_chosen": "SWIMMINGS", "players_word": "SWIMMING"} response = client.get(path, params) - assert response.status_code == 200, 'Should return an `OK` status code' - assert 'You found a' in response.content.decode('utf-8'), 'Should contain specified text' + assert response.status_code == 200, "Should return an `OK` status code" + assert "You found a" in response.content.decode("utf-8"), "Should contain specified text" diff --git a/apps/countdown_letters/urls.py b/apps/countdown_letters/urls.py index f9019e36..cba7ea05 100644 --- a/apps/countdown_letters/urls.py +++ b/apps/countdown_letters/urls.py @@ -3,10 +3,10 @@ from apps.countdown_letters.views import game_screen, results_screen, selection_screen -app_name = 'countdown_letters' +app_name = "countdown_letters" urlpatterns = [ - path('selection/', selection_screen, name='selection'), - path('game/', game_screen, name='game'), - path('results/', results_screen, name='results'), + path("selection/", selection_screen, name="selection"), + path("game/", game_screen, name="game"), + path("results/", results_screen, name="results"), ] diff --git a/apps/countdown_letters/utils.py b/apps/countdown_letters/utils.py index 34d0cb87..c3287b01 100644 --- a/apps/countdown_letters/utils.py +++ b/apps/countdown_letters/utils.py @@ -12,8 +12,8 @@ def build_game_screen_url(num_vowels_selected: int) -> str: choosing the letters for the game. """ letters_chosen = logic.get_letters_chosen(num_vowels=num_vowels_selected) - base_url = reverse('countdown_letters:game') - letters_chosen_url = urlencode({'letters_chosen': letters_chosen}) + base_url = reverse("countdown_letters:game") + letters_chosen_url = urlencode({"letters_chosen": letters_chosen}) return f"{base_url}?{letters_chosen_url}" @@ -23,9 +23,9 @@ def build_results_screen_url(letters_chosen: str, players_word: str) -> str: Builds the results screen's URL based upon the game's chosen letters and the player's selected word. """ - base_url = reverse('countdown_letters:results') - letters_chosen_url = urlencode({'letters_chosen': letters_chosen}) - players_word_url = urlencode({'players_word': players_word}) + base_url = reverse("countdown_letters:results") + letters_chosen_url = urlencode({"letters_chosen": letters_chosen}) + players_word_url = urlencode({"players_word": players_word}) return f"{base_url}?{letters_chosen_url}&{players_word_url}" @@ -36,16 +36,16 @@ def create_record(context: dict): posts the results to the database for reference and later retrieval. """ LettersGame.objects.create( - letters_chosen=context['letters_chosen'], - players_word=context['players_word'], - comp_word=context['comp_word'], - eligible_answer=context['eligible_answer'], - winning_word=context['winning_word'], - player_word_len=context['player_word_len'], - comp_word_len=context['comp_word_len'], - player_score=context['player_score'], - comp_score=context['comp_score'], - definition=context['definition_data']['definition'], - word_class=context['definition_data']['word_class'], - result=context['result'], + letters_chosen=context["letters_chosen"], + players_word=context["players_word"], + comp_word=context["comp_word"], + eligible_answer=context["eligible_answer"], + winning_word=context["winning_word"], + player_word_len=context["player_word_len"], + comp_word_len=context["comp_word_len"], + player_score=context["player_score"], + comp_score=context["comp_score"], + definition=context["definition_data"]["definition"], + word_class=context["definition_data"]["word_class"], + result=context["result"], ) diff --git a/apps/countdown_letters/views.py b/apps/countdown_letters/views.py index 5c02ff7f..879bdde5 100644 --- a/apps/countdown_letters/views.py +++ b/apps/countdown_letters/views.py @@ -6,39 +6,39 @@ def selection_screen(request): form = LetterSelectionForm() - if request.method == 'POST': + if request.method == "POST": form = LetterSelectionForm(request.POST) if form.is_valid(): - num_vowels_selected = form.cleaned_data.get('num_vowels_selected') + num_vowels_selected = form.cleaned_data.get("num_vowels_selected") full_url = utils.build_game_screen_url(num_vowels_selected) return redirect(full_url) else: form = LetterSelectionForm() - context = {'form': form} + context = {"form": form} - return render(request, 'countdown_letters/selection.html', context) + return render(request, "countdown_letters/selection.html", context) def game_screen(request): form = SelectedLettersForm() - if request.method == 'POST': + if request.method == "POST": form = SelectedLettersForm(request.POST) if form.is_valid(): - players_word = form.cleaned_data.get('players_word').upper() - letters_chosen = request.META['HTTP_REFERER'][-logic.GameSetup.MAX_GAME_LETTERS:] + players_word = form.cleaned_data.get("players_word").upper() + letters_chosen = request.META["HTTP_REFERER"][-logic.GameSetup.MAX_GAME_LETTERS :] full_url = utils.build_results_screen_url(letters_chosen, players_word) return redirect(full_url) - context = {'form': form} + context = {"form": form} - return render(request, 'countdown_letters/game.html', context) + return render(request, "countdown_letters/game.html", context) def results_screen(request): - letters_chosen: str = request.GET['letters_chosen'] - players_word: str = request.GET['players_word'] + letters_chosen: str = request.GET["letters_chosen"] + players_word: str = request.GET["players_word"] valid_word = validations.is_in_oxford_api(players_word) eligible_answer = validations.is_eligible_answer(players_word, letters_chosen) @@ -49,34 +49,29 @@ def results_screen(request): else: player_word_len, player_score = 0, 0 - if shortlisted_words := logic.get_shortlisted_words( - logic.get_words(), letters_chosen - ): + if shortlisted_words := logic.get_shortlisted_words(logic.get_words(), letters_chosen): comp_word = logic.get_longest_possible_word(shortlisted_words) winning_word = comp_word if len(comp_word) > player_word_len else players_word definition_data = logic.lookup_definition_data(winning_word) else: - players_word, comp_word, winning_word = 'N/A', 'N/A', 'N/A' + players_word, comp_word, winning_word = "N/A", "N/A", "N/A" eligible_answer = False - definition_data = { - 'definition': 'N/A', - 'word_class': 'N/A' - } + definition_data = {"definition": "N/A", "word_class": "N/A"} context = { - 'letters_chosen': letters_chosen, - 'players_word': players_word, - 'eligible_answer': eligible_answer, - 'player_word_len': player_word_len, - 'player_score': player_score, - 'comp_word': comp_word, - 'comp_word_len': len(comp_word) if comp_word else 0, - 'comp_score': logic.get_game_score(len(comp_word)) if comp_word else 0, - 'winning_word': comp_word if len(comp_word) > player_word_len else players_word, - 'definition_data': definition_data, - 'result': logic.get_result(players_word, comp_word), + "letters_chosen": letters_chosen, + "players_word": players_word, + "eligible_answer": eligible_answer, + "player_word_len": player_word_len, + "player_score": player_score, + "comp_word": comp_word, + "comp_word_len": len(comp_word) if comp_word else 0, + "comp_score": logic.get_game_score(len(comp_word)) if comp_word else 0, + "winning_word": comp_word if len(comp_word) > player_word_len else players_word, + "definition_data": definition_data, + "result": logic.get_result(players_word, comp_word), } utils.create_record(context) - return render(request, 'countdown_letters/results.html', context) + return render(request, "countdown_letters/results.html", context) diff --git a/apps/countdown_numbers/admin.py b/apps/countdown_numbers/admin.py index d5433cf8..80707360 100644 --- a/apps/countdown_numbers/admin.py +++ b/apps/countdown_numbers/admin.py @@ -8,32 +8,36 @@ class NumbersGameAdmin(admin.ModelAdmin): model = NumbersGame list_display = ( - 'game_nums', 'target_number', 'player_num_achieved', - 'comp_num_achieved', 'game_result', 'entry_date' + "game_nums", + "target_number", + "player_num_achieved", + "comp_num_achieved", + "game_result", + "entry_date", ) - ordering = ('-entry_date',) + ordering = ("-entry_date",) readonly_fields = [field.name for field in NumbersGame._meta.get_fields()] formfield_overrides = { - models.CharField: {'widget': TextInput(attrs={'size': '20'})}, - models.IntegerField: {'widget': TextInput(attrs={'size': '2'})}, + models.CharField: {"widget": TextInput(attrs={"size": "20"})}, + models.IntegerField: {"widget": TextInput(attrs={"size": "2"})}, } fieldsets = ( - ('Selection', { - 'fields': ['game_nums', 'target_number'] - }), - ('Results', { - 'fields': ( - ('valid_calc', 'player_num_achieved', 'player_score'), - ('comp_num_achieved', 'comp_score'), - 'solution_str', - 'game_result' - ) - }), - ('Dates', { - 'fields': ('entry_date', ) - }), + ("Selection", {"fields": ["game_nums", "target_number"]}), + ( + "Results", + { + "fields": ( + ("valid_calc", "player_num_achieved", "player_score"), + ("comp_num_achieved", "comp_score"), + "solution_str", + "game_result", + ) + }, + ), + ("Dates", {"fields": ("entry_date",)}), ) + admin.site.register(NumbersGame, NumbersGameAdmin) diff --git a/apps/countdown_numbers/apps.py b/apps/countdown_numbers/apps.py index 0d58b188..765666fe 100644 --- a/apps/countdown_numbers/apps.py +++ b/apps/countdown_numbers/apps.py @@ -2,5 +2,5 @@ class CountdownNumbersConfig(AppConfig): - name = 'apps.countdown_numbers' - verbose_name = 'Countdown Numbers' + name = "apps.countdown_numbers" + verbose_name = "Countdown Numbers" diff --git a/apps/countdown_numbers/forms.py b/apps/countdown_numbers/forms.py index c293a151..6b145d23 100644 --- a/apps/countdown_numbers/forms.py +++ b/apps/countdown_numbers/forms.py @@ -2,16 +2,17 @@ class NumberSelectionForm(forms.Form): - num_from_top = forms.IntegerField(required=True, label='', min_value=0, max_value=4) + num_from_top = forms.IntegerField(required=True, label="", min_value=0, max_value=4) class SelectedNumbersForm(forms.Form): players_calculation = forms.CharField( required=True, - label='', + label="", strip=True, min_length=3, max_length=50, - widget=forms.TextInput(attrs={ - 'placeholder': 'Enter your calculation closest to the target number...'}), + widget=forms.TextInput( + attrs={"placeholder": "Enter your calculation closest to the target number..."} + ), ) diff --git a/apps/countdown_numbers/logic.py b/apps/countdown_numbers/logic.py index 2588144f..6b47a261 100644 --- a/apps/countdown_numbers/logic.py +++ b/apps/countdown_numbers/logic.py @@ -6,12 +6,13 @@ from collections import defaultdict, deque from random import choices, randint +from typing import DefaultDict, Dict, List from django.urls import reverse from django.utils.http import urlencode -def get_numbers_chosen(num_from_top: int) -> list: +def get_numbers_chosen(num_from_top: int) -> List: """ Returns an appropriate proportion of numbers from the top and bottom row within the randomised game selection according to their @@ -40,34 +41,35 @@ def get_numbers_chosen(num_from_top: int) -> list: def get_target_number() -> int: - """ Generates a random number between 100 and 999 """ + """Generates a random number between 100 and 999""" return randint(100, 999) def build_game_url(num_from_top: int) -> str: - """ Generates the URL for the `game` screen """ - base_url = reverse('countdown_numbers:game') - target_number_url = urlencode({'target_number': get_target_number()}) + """Generates the URL for the `game` screen""" + base_url = reverse("countdown_numbers:game") + target_number_url = urlencode({"target_number": get_target_number()}) numbers_chosen_url = urlencode( - {'numbers_chosen': get_numbers_chosen(num_from_top=num_from_top)}) + {"numbers_chosen": get_numbers_chosen(num_from_top=num_from_top)} + ) return f"{base_url}?{target_number_url}&{numbers_chosen_url}" -def get_game_nums(number_chosen: list) -> list: - """ Performs cleanup to get the game numbers as a list """ - return number_chosen.strip('[').strip(']').replace(' ', '').split(',') +def get_game_nums(number_chosen: list) -> List: + """Performs cleanup to get the game numbers as a list""" + return number_chosen.strip("[").strip("]").replace(" ", "").split(",") def get_player_num_achieved(players_calc: str) -> int: - """ Calculates number calculated according to the input answer """ + """Calculates number calculated according to the input answer""" return int(eval(players_calc)) -def get_game_calcs(game_nums: list, stop_on=None) -> defaultdict: - """ Calculates the possible calculations to the game """ +def get_game_calcs(game_nums: list, stop_on=None) -> DefaultDict: + """Calculates the possible calculations to the game""" operator_symbols = { - '+': operator.add, - '-': operator.sub, + "+": operator.add, + "-": operator.sub, chr(215): operator.mul, chr(247): operator.truediv, } @@ -77,8 +79,8 @@ def get_game_calcs(game_nums: list, stop_on=None) -> defaultdict: possibilities = itertools.product(game_nums_permutations, operator_combinations) game_calcs = defaultdict(list) - calc_string = u'(((({0} {6} {1}) {7} {2}) {8} {3}) {9} {4}) {10} {5}' - for (game_nums, operators) in possibilities: + calc_string = "(((({0} {6} {1}) {7} {2}) {8} {3}) {9} {4}) {10} {5}" + for game_nums, operators in possibilities: calc = calc_string.format(*(game_nums + operators)) value_queue = deque(game_nums) @@ -99,8 +101,8 @@ def get_game_calcs(game_nums: list, stop_on=None) -> defaultdict: return game_calcs -def get_best_solution(game_nums: list, target: int) -> str: - """ Calculates a solution closest to the game's target number """ +def get_best_solution(game_nums: List, target: int) -> str: + """Calculates a solution closest to the game's target number""" game_calcs = get_game_calcs(game_nums, stop_on=target) if int(target) in game_calcs: @@ -128,15 +130,15 @@ def get_score_awarded(target_number: int, num_achieved: int) -> int: return points_awarded -def get_game_result(target: int, answers: dict) -> str: - """ Returns the game's result as a string for template rendering """ - comp_ans_variance = abs(answers['comp_num_achieved'] - target) - player_ans_variance = abs(answers['player_num_achieved'] - target) +def get_game_result(target: int, answers: Dict) -> str: + """Returns the game's result as a string for template rendering""" + comp_ans_variance = abs(answers["comp_num_achieved"] - target) + player_ans_variance = abs(answers["player_num_achieved"] - target) if comp_ans_variance == player_ans_variance: - result = 'Draw' + result = "Draw" else: if player_ans_variance < comp_ans_variance: - result = 'Player wins' + result = "Player wins" else: - result = 'Rachel wins' + result = "Rachel wins" return result diff --git a/apps/countdown_numbers/migrations/0001_initial.py b/apps/countdown_numbers/migrations/0001_initial.py index 79dac459..337cd389 100644 --- a/apps/countdown_numbers/migrations/0001_initial.py +++ b/apps/countdown_numbers/migrations/0001_initial.py @@ -4,30 +4,33 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='NumbersGame', + name="NumbersGame", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('game_nums', models.CharField(max_length=255)), - ('target_number', models.IntegerField()), - ('player_num_achieved', models.IntegerField()), - ('valid_calc', models.BooleanField(default=False)), - ('comp_num_achieved', models.IntegerField()), - ('player_score', models.IntegerField(default=0)), - ('comp_score', models.IntegerField(default=0)), - ('solution_str', models.CharField(max_length=255)), - ('game_result', models.CharField(max_length=255)), - ('entry_date', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("game_nums", models.CharField(max_length=255)), + ("target_number", models.IntegerField()), + ("player_num_achieved", models.IntegerField()), + ("valid_calc", models.BooleanField(default=False)), + ("comp_num_achieved", models.IntegerField()), + ("player_score", models.IntegerField(default=0)), + ("comp_score", models.IntegerField(default=0)), + ("solution_str", models.CharField(max_length=255)), + ("game_result", models.CharField(max_length=255)), + ("entry_date", models.DateTimeField(auto_now_add=True)), ], options={ - 'ordering': ('-entry_date',), + "ordering": ("-entry_date",), }, ), ] diff --git a/apps/countdown_numbers/models.py b/apps/countdown_numbers/models.py index de16df26..245997a2 100644 --- a/apps/countdown_numbers/models.py +++ b/apps/countdown_numbers/models.py @@ -14,7 +14,7 @@ class NumbersGame(models.Model): entry_date = models.DateTimeField(auto_now_add=True) class Meta: - ordering = ('-entry_date', ) + ordering = ("-entry_date",) @property def entry_year(self) -> int: diff --git a/apps/countdown_numbers/templatetags/template_helpers.py b/apps/countdown_numbers/templatetags/template_helpers.py index e61ac6ce..3302e052 100644 --- a/apps/countdown_numbers/templatetags/template_helpers.py +++ b/apps/countdown_numbers/templatetags/template_helpers.py @@ -12,18 +12,14 @@ @register.filter def remove_brackets(value): - """ Removes any squared brackets """ + """Removes any squared brackets""" return value.replace("[", "").replace("]", "") + @register.filter def add_spacing(value): - """ Adds spacing around the calculation's operators """ - replacements = { - "+": " + ", - "-": " - ", - "*": " * ", - "/": " / " - } + """Adds spacing around the calculation's operators""" + replacements = {"+": " + ", "-": " - ", "*": " * ", "/": " / "} return "".join([replacements.get(c, c) for c in value]) diff --git a/apps/countdown_numbers/tests/conftest.py b/apps/countdown_numbers/tests/conftest.py index bd05942b..986b8c36 100644 --- a/apps/countdown_numbers/tests/conftest.py +++ b/apps/countdown_numbers/tests/conftest.py @@ -7,6 +7,6 @@ from apps.countdown_numbers.models import NumbersGame -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def numbers_game(): return mixer.blend(NumbersGame, pk=1) diff --git a/apps/countdown_numbers/tests/test_logic.py b/apps/countdown_numbers/tests/test_logic.py index c9261624..ca27cd9f 100644 --- a/apps/countdown_numbers/tests/test_logic.py +++ b/apps/countdown_numbers/tests/test_logic.py @@ -13,15 +13,17 @@ def test_get_numbers_chosen(num_from_top): row, the correct game numbers are returned accordingly. """ numbers_chosen = logic.get_numbers_chosen(num_from_top) - assert len(numbers_chosen) == 6, 'Should return 6 chosen game numbers' + assert len(numbers_chosen) == 6, "Should return 6 chosen game numbers" - assert numbers_chosen.count(all([25, 50, 75, 100])) <= 1, \ - 'Should only return 1 instance of any of the numbers from the top' + assert ( + numbers_chosen.count(all([25, 50, 75, 100])) <= 1 + ), "Should only return 1 instance of any of the numbers from the top" - assert numbers_chosen.count(all([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) <= 2, \ - 'Should only return a max of 2 instances of any of the numbers from the bottom' + assert ( + numbers_chosen.count(all([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) <= 2 + ), "Should only return a max of 2 instances of any of the numbers from the bottom" - assert isinstance(numbers_chosen, list), 'Should be a `list` object' + assert isinstance(numbers_chosen, list), "Should be a `list` object" def test_get_target_number(): @@ -29,16 +31,16 @@ def test_get_target_number(): Asserts that the number generated is an integer between 100 and 999 """ random_num = logic.get_target_number() - assert 100 <= random_num <= 999, 'Should be between 100 and 999' - assert isinstance(random_num, int), 'Should be an `int` object' + assert 100 <= random_num <= 999, "Should be between 100 and 999" + assert isinstance(random_num, int), "Should be an `int` object" -@pytest.mark.parametrize(argnames='num_from_top', argvalues=[0, 1, 2, 3, 4]) +@pytest.mark.parametrize(argnames="num_from_top", argvalues=[0, 1, 2, 3, 4]) def test_build_game_url(num_from_top): - """ Asserts that the correct game URL is built """ + """Asserts that the correct game URL is built""" game_url = logic.build_game_url(num_from_top) - assert '/countdown-numbers/game/?target_number=' in game_url - assert '&numbers_chosen=' in game_url + assert "/countdown-numbers/game/?target_number=" in game_url + assert "&numbers_chosen=" in game_url assert isinstance(game_url, str) @@ -50,7 +52,7 @@ def test_score_awarded_for_achieving_target_number(target_number): """ num_achieved = target_number score_awarded = logic.get_score_awarded(target_number, num_achieved) - assert score_awarded == 10, 'Should be 10 points for achieving the target number' + assert score_awarded == 10, "Should be 10 points for achieving the target number" @given(num_achieved_var=st.integers(min_value=-5, max_value=5).filter(lambda x: x != 0)) @@ -62,10 +64,12 @@ def test_score_awarded_for_being_within_5_of_target_number(num_achieved_var): TARGET_NUMBER = 500 num_achieved = TARGET_NUMBER + num_achieved_var score_awarded = logic.get_score_awarded(TARGET_NUMBER, num_achieved) - assert score_awarded == 7, 'Should score 7 points for being within 1-5 either side' + assert score_awarded == 7, "Should score 7 points for being within 1-5 either side" -@given(num_achieved_var=st.integers(min_value=-10, max_value=10).filter(lambda x: not -5 <= x <= 5)) +@given( + num_achieved_var=st.integers(min_value=-10, max_value=10).filter(lambda x: not -5 <= x <= 5) +) def test_score_awarded_for_being_within_10_of_target_number(num_achieved_var): """ Asserts that a player is awarded 5 points for being within a @@ -74,10 +78,14 @@ def test_score_awarded_for_being_within_10_of_target_number(num_achieved_var): TARGET_NUMBER = 500 num_achieved = TARGET_NUMBER + num_achieved_var score_awarded = logic.get_score_awarded(TARGET_NUMBER, num_achieved) - assert score_awarded == 5, 'Should score 5 points for being within 6-10 either side' + assert score_awarded == 5, "Should score 5 points for being within 6-10 either side" -@given(num_achieved_var=st.integers(min_value=-400, max_value=499).filter(lambda x: not -10 <= x <= 10)) +@given( + num_achieved_var=st.integers(min_value=-400, max_value=499).filter( + lambda x: not -10 <= x <= 10 + ) +) def test_score_awarded_for_being_more_than_10_away_from_target_number(num_achieved_var): """ Asserts that a player is awarded zero points for being more than 10 @@ -86,7 +94,7 @@ def test_score_awarded_for_being_more_than_10_away_from_target_number(num_achiev TARGET_NUMBER = 500 num_achieved = TARGET_NUMBER + num_achieved_var score_awarded = logic.get_score_awarded(TARGET_NUMBER, num_achieved) - assert score_awarded == 0, 'Should score 0 points for being more than 10 away either side' + assert score_awarded == 0, "Should score 0 points for being more than 10 away either side" def test_get_game_result_is_draw_1(): @@ -96,11 +104,11 @@ def test_get_game_result_is_draw_1(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 500, - 'player_num_achieved': 500, + "comp_num_achieved": 500, + "player_num_achieved": 500, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Draw' + assert game_result == "Draw" def test_get_game_result_is_draw_2(): @@ -110,11 +118,11 @@ def test_get_game_result_is_draw_2(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 505, - 'player_num_achieved': 505, + "comp_num_achieved": 505, + "player_num_achieved": 505, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Draw' + assert game_result == "Draw" def test_get_game_result_is_draw_3(): @@ -124,11 +132,11 @@ def test_get_game_result_is_draw_3(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 495, - 'player_num_achieved': 495, + "comp_num_achieved": 495, + "player_num_achieved": 495, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Draw' + assert game_result == "Draw" def test_get_game_result_is_draw_4(): @@ -138,11 +146,11 @@ def test_get_game_result_is_draw_4(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 495, - 'player_num_achieved': 505, + "comp_num_achieved": 495, + "player_num_achieved": 505, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Draw' + assert game_result == "Draw" def test_rachel_wins_when_closer_but_above_target_number(): @@ -152,11 +160,11 @@ def test_rachel_wins_when_closer_but_above_target_number(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 501, - 'player_num_achieved': 502, + "comp_num_achieved": 501, + "player_num_achieved": 502, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Rachel wins' + assert game_result == "Rachel wins" def test_rachel_wins_when_closer_but_below_target_number(): @@ -166,11 +174,11 @@ def test_rachel_wins_when_closer_but_below_target_number(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 499, - 'player_num_achieved': 498, + "comp_num_achieved": 499, + "player_num_achieved": 498, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Rachel wins' + assert game_result == "Rachel wins" def test_player_wins_when_closer_but_above_target_number(): @@ -180,11 +188,11 @@ def test_player_wins_when_closer_but_above_target_number(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 502, - 'player_num_achieved': 501, + "comp_num_achieved": 502, + "player_num_achieved": 501, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Player wins' + assert game_result == "Player wins" def test_player_wins_when_closer_but_below_target_number(): @@ -194,8 +202,8 @@ def test_player_wins_when_closer_but_below_target_number(): """ TARGET_NUMBER = 500 answers = { - 'comp_num_achieved': 498, - 'player_num_achieved': 499, + "comp_num_achieved": 498, + "player_num_achieved": 499, } game_result = logic.get_game_result(TARGET_NUMBER, answers) - assert game_result == 'Player wins' + assert game_result == "Player wins" diff --git a/apps/countdown_numbers/tests/test_models.py b/apps/countdown_numbers/tests/test_models.py index dd820ac7..5cf08502 100644 --- a/apps/countdown_numbers/tests/test_models.py +++ b/apps/countdown_numbers/tests/test_models.py @@ -20,58 +20,59 @@ class TestNumbersGame: """ def test_single_numbers_game_saves(self, numbers_game): - assert numbers_game.pk == 1, 'Should create a `Numbers numbers_game` instance' + assert numbers_game.pk == 1, "Should create a `Numbers numbers_game` instance" def test_multi_numbers_game_saves(self): games = mixer.cycle(10).blend(NumbersGame) - assert games[9].pk == 10, '10th instance should have a PK of 10' - assert NumbersGame.objects.count() == 10, 'Should have 10 objects in the database' + assert games[9].pk == 10, "10th instance should have a PK of 10" + assert NumbersGame.objects.count() == 10, "Should have 10 objects in the database" def test_can_delete_numbers_game(self): games = mixer.cycle(10).blend(NumbersGame) games[4].delete() - assert NumbersGame.objects.count() == 9, 'Should have 9 objects remaining in the database' + assert NumbersGame.objects.count() == 9, "Should have 9 objects remaining in the database" def test_game_nums_is_charfield(self, numbers_game): field = numbers_game._meta.get_field("game_nums") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_target_number_is_integerfield(self, numbers_game): field = numbers_game._meta.get_field("target_number") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_player_num_achieved_is_integerfield(self, numbers_game): field = numbers_game._meta.get_field("player_num_achieved") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_valid_calc_is_booleanfield(self, numbers_game): field = numbers_game._meta.get_field("valid_calc") - assert isinstance(field, models.BooleanField), 'Should be a boolean field' + assert isinstance(field, models.BooleanField), "Should be a boolean field" def test_comp_num_achieved_is_integerfield(self, numbers_game): field = numbers_game._meta.get_field("comp_num_achieved") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_player_score_is_integerfield(self, numbers_game): field = numbers_game._meta.get_field("player_score") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_comp_score_is_integerfield(self, numbers_game): field = numbers_game._meta.get_field("comp_score") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_solution_str_is_charfield(self, numbers_game): field = numbers_game._meta.get_field("solution_str") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_game_result_is_charfield(self, numbers_game): field = numbers_game._meta.get_field("game_result") - assert isinstance(field, models.CharField), 'Should be a char field' + assert isinstance(field, models.CharField), "Should be a char field" def test_entry_date_is_datefield(self, numbers_game): field = numbers_game._meta.get_field("entry_date") - assert isinstance(field, models.DateField), 'Should be a date field' + assert isinstance(field, models.DateField), "Should be a date field" def test_entry_year(self, numbers_game): - assert numbers_game.entry_year == numbers_game.entry_date.year, \ - "Year should be the same as the entry date's year property" + assert ( + numbers_game.entry_year == numbers_game.entry_date.year + ), "Year should be the same as the entry date's year property" diff --git a/apps/countdown_numbers/tests/test_template_helpers.py b/apps/countdown_numbers/tests/test_template_helpers.py index 97aacc99..45fd27bf 100644 --- a/apps/countdown_numbers/tests/test_template_helpers.py +++ b/apps/countdown_numbers/tests/test_template_helpers.py @@ -2,29 +2,32 @@ def test_remove_brackets(): - test_value = '[100*9]' + test_value = "[100*9]" outcome = template_helpers.remove_brackets(test_value) - assert '[' and ']' not in outcome, 'Square brackets should not be returned' + assert "[" and "]" not in outcome, "Square brackets should not be returned" + def test_add_spacing(): - test_value = '[(100*9)/(4+5)-1]' + test_value = "[(100*9)/(4+5)-1]" outcome = template_helpers.add_spacing(test_value) - assert outcome == '[(100 * 9) / (4 + 5) - 1]' + assert outcome == "[(100 * 9) / (4 + 5) - 1]" + def test_change_symbols(): - test_value = '(100 * 9) / (4 + 5) - 1' + test_value = "(100 * 9) / (4 + 5) - 1" outcome = template_helpers.change_symbols(test_value) - expected_outcome = '(100 ' + chr(215) +' 9) ' + chr(247) + ' (4 + 5) - 1' - assert outcome == expected_outcome, \ - "Should be reformatted with mathematical multiplication and division symbols" + expected_outcome = "(100 " + chr(215) + " 9) " + chr(247) + " (4 + 5) - 1" + assert ( + outcome == expected_outcome + ), "Should be reformatted with mathematical multiplication and division symbols" + def test_humanise_calculation(): - test_value = '[(100*9)/(4+5)-1]' + test_value = "[(100*9)/(4+5)-1]" removed_brackets = template_helpers.remove_brackets(test_value) spacing_added = template_helpers.add_spacing(removed_brackets) overall_outcome = template_helpers.change_symbols(spacing_added) - expected_outcome = '(100 ' + chr(215) +' 9) ' + chr(247) + ' (4 + 5) - 1' - assert overall_outcome == expected_outcome, \ - """ Should have brackets removed, spacing added, and be + expected_outcome = "(100 " + chr(215) + " 9) " + chr(247) + " (4 + 5) - 1" + assert overall_outcome == expected_outcome, """ Should have brackets removed, spacing added, and be reformatted with humanised mathematical multiplication and division symbols""" diff --git a/apps/countdown_numbers/tests/test_urls.py b/apps/countdown_numbers/tests/test_urls.py index 5fa6ee8a..25ed059b 100644 --- a/apps/countdown_numbers/tests/test_urls.py +++ b/apps/countdown_numbers/tests/test_urls.py @@ -2,16 +2,18 @@ def test_selection_screen(): - """ Verify that the `selection` url invokes intended view """ - resolver = resolve(reverse('countdown_numbers:selection')) - assert resolver.view_name, 'selection_screen' + """Verify that the `selection` url invokes intended view""" + resolver = resolve(reverse("countdown_numbers:selection")) + assert resolver.view_name, "selection_screen" + def test_game_screen(): - """ Verify that the `game` url invokes intended view """ - resolver = resolve(reverse('countdown_numbers:game')) - assert resolver.view_name, 'game_screen' + """Verify that the `game` url invokes intended view""" + resolver = resolve(reverse("countdown_numbers:game")) + assert resolver.view_name, "game_screen" + def test_results_screen(): - """ Verify that the `results` url invokes intended view """ - resolver = resolve(reverse('countdown_numbers:results')) - assert resolver.view_name, 'results_screen' + """Verify that the `results` url invokes intended view""" + resolver = resolve(reverse("countdown_numbers:results")) + assert resolver.view_name, "results_screen" diff --git a/apps/countdown_numbers/tests/test_validations.py b/apps/countdown_numbers/tests/test_validations.py index 52f78434..4dff971e 100644 --- a/apps/countdown_numbers/tests/test_validations.py +++ b/apps/countdown_numbers/tests/test_validations.py @@ -4,84 +4,83 @@ @pytest.mark.django_db - class TestCheckChars: def test_valid_calc_string(self): - """ Checks a calc without illegal characters passes """ - calc = '(((25+75)*4)/4)+1' + """Checks a calc without illegal characters passes""" + calc = "(((25+75)*4)/4)+1" check_passes = validations.check_chars(calc) - assert check_passes, 'Should return `True`' + assert check_passes, "Should return `True`" def test_invalid_calc_string(self): - """ Checks a calc with illegal characters doesn't pass """ - calc = '(((25+75)*4)^4)+1' # Includes caret symbol + """Checks a calc with illegal characters doesn't pass""" + calc = "(((25+75)*4)^4)+1" # Includes caret symbol check_passes = validations.check_chars(calc) - assert not check_passes, 'Should return `False`' + assert not check_passes, "Should return `False`" class TestCheckBrackets: def test_valid_calc_string(self): - """ Checks a calc with a valid bracket pairing passes """ - calc = '(((25+75)*4)/4)+1' + """Checks a calc with a valid bracket pairing passes""" + calc = "(((25+75)*4)/4)+1" check_passes = validations.check_brackets(calc) - assert check_passes, 'Should return `True`' + assert check_passes, "Should return `True`" def test_invalid_calc_string(self): - """ Checks a calc with an invalid bracket pairing doesn't pass """ - calc = '(((25+75)*4)/4))+1' # 3 opening; 4 closing brackets + """Checks a calc with an invalid bracket pairing doesn't pass""" + calc = "(((25+75)*4)/4))+1" # 3 opening; 4 closing brackets check_passes = validations.check_brackets(calc) - assert not check_passes, 'Should return `False`' + assert not check_passes, "Should return `False`" class TestCheckLegalChars: def test_valid_calc_string(self): - """ Checks a calc with a valid characters passes """ - calc = '(((25+75)*4)/4)+1' + """Checks a calc with a valid characters passes""" + calc = "(((25+75)*4)/4)+1" check_passes = validations.check_legal_chars_seq(calc) - assert check_passes, 'Should return `True`' + assert check_passes, "Should return `True`" def test_invalid_calc_string(self): - """ Checks a calc with invalid characters doesn't pass """ - calc = '(((25+75)*4)/4)+1/)' # Includes '/)' + """Checks a calc with invalid characters doesn't pass""" + calc = "(((25+75)*4)/4)+1/)" # Includes '/)' check_passes = validations.check_legal_chars_seq(calc) - assert not check_passes, 'Should return `False`' + assert not check_passes, "Should return `False`" class TestStripSpaces: def test_stripped(self): - """ Checks spaces are stripped from calc """ - calc = ' (((25 + 75) * 4 )/ 4 ) + 1 ' # Includes spaces + """Checks spaces are stripped from calc""" + calc = " (((25 + 75) * 4 )/ 4 ) + 1 " # Includes spaces stripped = validations.strip_spaces(calc) - assert ' ' not in stripped - assert stripped == '(((25+75)*4)/4)+1' + assert " " not in stripped + assert stripped == "(((25+75)*4)/4)+1" class TestValidCalc: def test_valid_calc_passes_checks(self): - """ Checks that a valid calc string passes the checks """ - players_calc = '50 * 10' + """Checks that a valid calc string passes the checks""" + players_calc = "50 * 10" checks = validations.calc_entered_is_valid(players_calc) assert checks.has_valid_brackets assert checks.has_valid_chars assert checks.has_valid_sequences def test_invalid_brackets_fails_check(self): - """ Asserts a calc string with invalid brackets fails the checks """ - players_calc = '(50 * 10))' + """Asserts a calc string with invalid brackets fails the checks""" + players_calc = "(50 * 10))" checks = validations.calc_entered_is_valid(players_calc) assert isinstance(checks, tuple) assert not checks.has_valid_brackets def test_invalid_chars_fails_check(self): - """ Asserts a calc string with invalid chars fails the checks """ - players_calc = '50 * 10z' + """Asserts a calc string with invalid chars fails the checks""" + players_calc = "50 * 10z" checks = validations.calc_entered_is_valid(players_calc) assert isinstance(checks, tuple) assert not checks.has_valid_chars def test_invalid_sequences_fails_check(self): - """ Asserts a calc string with invalid sequences fails the checks """ - players_calc = '(50 * 10/)' + """Asserts a calc string with invalid sequences fails the checks""" + players_calc = "(50 * 10/)" checks = validations.calc_entered_is_valid(players_calc) assert isinstance(checks, tuple) assert not checks.has_valid_sequences diff --git a/apps/countdown_numbers/tests/test_views.py b/apps/countdown_numbers/tests/test_views.py index f7479d43..e5a70b9b 100644 --- a/apps/countdown_numbers/tests/test_views.py +++ b/apps/countdown_numbers/tests/test_views.py @@ -5,63 +5,67 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) + def test_get_selection_screen(client): - """ Asserts a site visitor can GET the `selection` screen """ - path = reverse('countdown_numbers:selection') + """Asserts a site visitor can GET the `selection` screen""" + path = reverse("countdown_numbers:selection") response = client.get(path) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" + -@pytest.mark.parametrize(argnames='num_from_top', argvalues=[0, 1, 2, 3, 4]) +@pytest.mark.parametrize(argnames="num_from_top", argvalues=[0, 1, 2, 3, 4]) def test_post_selection_screen(client, num_from_top): - """ Asserts a site visitor can POST from the `selection` screen """ - path = reverse('countdown_numbers:selection') - data = {'num_from_top': num_from_top} + """Asserts a site visitor can POST from the `selection` screen""" + path = reverse("countdown_numbers:selection") + data = {"num_from_top": num_from_top} response = client.post(path, data) - assert response.status_code == 302, 'Should return a redirection status code' + assert response.status_code == 302, "Should return a redirection status code" + def test_form_not_valid(client): - """ Asserts a site visitor returns to the selection screen """ - path = reverse('countdown_numbers:selection') - data = {'num_from_top': 5} + """Asserts a site visitor returns to the selection screen""" + path = reverse("countdown_numbers:selection") + data = {"num_from_top": 5} response = client.post(path, data) - assert not response.context['form'].is_valid() - assert response.context['widget']['attrs']['min'] == 0 - assert response.context['widget']['attrs']['max'] == 4 - assert response.status_code == 200, 'Should return an `OK` status code' + assert not response.context["form"].is_valid() + assert response.context["widget"]["attrs"]["min"] == 0 + assert response.context["widget"]["attrs"]["max"] == 4 + assert response.status_code == 200, "Should return an `OK` status code" + def test_get_game_screen(client): - """ Asserts a site visitor can GET the `game` screen """ - base_path = reverse('countdown_numbers:game') - get_params = { - 'target_number': '869', - 'numbers_chosen': '[100, 1, 2, 3, 4, 5]' - } + """Asserts a site visitor can GET the `game` screen""" + base_path = reverse("countdown_numbers:game") + get_params = {"target_number": "869", "numbers_chosen": "[100, 1, 2, 3, 4, 5]"} response = client.get(base_path, get_params) - assert response.status_code == 200, 'Should return an `OK` status code' - assert 'The target number' in response.content.decode('utf-8'), 'Should contain specified text' + assert response.status_code == 200, "Should return an `OK` status code" + assert "The target number" in response.content.decode("utf-8"), "Should contain specified text" + def test_post_game_screen(client): - """ Asserts a site visitor can POST from the `game` screen """ - base_path = reverse('countdown_numbers:game') + """Asserts a site visitor can POST from the `game` screen""" + base_path = reverse("countdown_numbers:game") full_path = base_path + "?target_number=869&numbers_chosen=%5B100%2C+5%1C+8%2C+1%3C+5%4C+2%5D" data = { - 'target_number': '869', - 'numbers_chosen': '[100, 1, 2, 3, 4, 5]', - 'players_calculation': '100 * (5+3)' + "target_number": "869", + "numbers_chosen": "[100, 1, 2, 3, 4, 5]", + "players_calculation": "100 * (5+3)", } response = client.post(full_path, data, HTTP_REFERER=base_path) - assert response.status_code == 302, 'Should return a redirection status code' + assert response.status_code == 302, "Should return a redirection status code" + @pytest.mark.slow( - reason='Processing the view also encapsulates game logic, validations, and calculations') + reason="Processing the view also encapsulates game logic, validations, and calculations" +) def test_results_screen(client): - """ Asserts a site visitor can GET the `results` screen """ - path = reverse('countdown_numbers:results') + """Asserts a site visitor can GET the `results` screen""" + path = reverse("countdown_numbers:results") get_params = { - 'target_number': '869', - 'numbers_chosen': '[100, 1, 2, 3, 4, 5]', - 'players_calculation': '100*(5+4)', + "target_number": "869", + "numbers_chosen": "[100, 1, 2, 3, 4, 5]", + "players_calculation": "100*(5+4)", } response = client.get(path, get_params) - assert response.status_code == 200, 'Should return an `OK` status code' - assert 'Your Calculation' in response.content.decode('utf-8'), 'Should contain specified text' + assert response.status_code == 200, "Should return an `OK` status code" + assert "Your Calculation" in response.content.decode("utf-8"), "Should contain specified text" diff --git a/apps/countdown_numbers/urls.py b/apps/countdown_numbers/urls.py index 8ce0572b..1e8aefc9 100644 --- a/apps/countdown_numbers/urls.py +++ b/apps/countdown_numbers/urls.py @@ -3,10 +3,10 @@ from apps.countdown_numbers.views import game_screen, results_screen, selection_screen -app_name = 'countdown_numbers' +app_name = "countdown_numbers" urlpatterns = [ - path('selection/', selection_screen, name='selection'), - path('game/', game_screen, name='game'), - path('results/', results_screen, name='results'), + path("selection/", selection_screen, name="selection"), + path("game/", game_screen, name="game"), + path("results/", results_screen, name="results"), ] diff --git a/apps/countdown_numbers/utils.py b/apps/countdown_numbers/utils.py index b26cd6ea..54f58e4e 100644 --- a/apps/countdown_numbers/utils.py +++ b/apps/countdown_numbers/utils.py @@ -9,13 +9,13 @@ def create_record(context: dict): posts the results to the database for reference and later retrieval. """ NumbersGame.objects.create( - game_nums=context['game_nums'], - target_number=context['target_number'], - player_num_achieved=context['player_num_achieved'], - valid_calc=context['valid_calc'], - comp_num_achieved=context['comp_num_achieved'], - player_score=context['player_score'], - comp_score=context['comp_score'], - solution_str=context['solution_str'], - game_result=context['game_result'], + game_nums=context["game_nums"], + target_number=context["target_number"], + player_num_achieved=context["player_num_achieved"], + valid_calc=context["valid_calc"], + comp_num_achieved=context["comp_num_achieved"], + player_score=context["player_score"], + comp_score=context["comp_score"], + solution_str=context["solution_str"], + game_result=context["game_result"], ) diff --git a/apps/countdown_numbers/validations.py b/apps/countdown_numbers/validations.py index 6a5781a9..d82cb130 100644 --- a/apps/countdown_numbers/validations.py +++ b/apps/countdown_numbers/validations.py @@ -4,6 +4,7 @@ import re from collections import namedtuple +from typing import List, NamedTuple from django.contrib import messages @@ -14,14 +15,14 @@ def check_chars(players_calc: str) -> bool: - If valid, the game's processing logic continues. - If invalid, help message is displayed to the player. """ - pattern = r'^[0-9()\+\-\*\/]*$' + pattern = r"^[0-9()\+\-\*\/]*$" match_set = re.search(pattern, players_calc) return match_set is not None def check_brackets(players_calc: str) -> bool: - """ Checks there's a matching number of opening/closing brackets """ - return players_calc.count('(') == players_calc.count(')') + """Checks there's a matching number of opening/closing brackets""" + return players_calc.count("(") == players_calc.count(")") def check_legal_chars_seq(players_calc: str) -> bool: @@ -30,57 +31,57 @@ def check_legal_chars_seq(players_calc: str) -> bool: - If valid, the game's processing logic continues. - If invalid, help message is displayed to the player. """ - patterns = ['+)', '-)', '*)', '/)'] + patterns = ["+)", "-)", "*)", "/)"] return all(pattern not in players_calc for pattern in patterns) def strip_spaces(players_calc: str) -> str: - """ Removes unncessary spaces within a player's answer. """ - return players_calc.replace(' ', '') + """Removes unncessary spaces within a player's answer.""" + return players_calc.replace(" ", "") -def calc_entered_is_valid(players_calc: str) -> namedtuple: - """ Validates the calc entered is in a valid format. """ +def calc_entered_is_valid(players_calc: str) -> NamedTuple: + """Validates the calc entered is in a valid format.""" players_calc = strip_spaces(players_calc) has_valid_chars = check_chars(players_calc) has_valid_brackets = check_brackets(players_calc) has_valid_sequences = check_legal_chars_seq(players_calc) ValidCalc = namedtuple( - 'ValidCalc', ['has_valid_chars', 'has_valid_brackets', 'has_valid_sequences']) + "ValidCalc", ["has_valid_chars", "has_valid_brackets", "has_valid_sequences"] + ) return ValidCalc(has_valid_chars, has_valid_brackets, has_valid_sequences) -def output_message(request, checks: namedtuple): - """ When checks do not pass, displays a message to the player """ +def output_message(request, checks: NamedTuple): + """When checks do not pass, displays a message to the player""" msg: str = "" if not checks.has_valid_brackets: msg = "There is a mismatch in the number of opening and closing brackets used." - + if not checks.has_valid_chars: msg = "Only arithmetic operators, digits, and rounded brackets are permitted characters." - + if not checks.has_valid_sequences: - msg = (f"The string sequence is an invalid one. " + - "Please check the calculation string and resubmit.") + msg = "The string sequence is invalid. Please check the calculation string and resubmit." messages.add_message(request, messages.INFO, msg) -def get_permissible_nums(request) -> list: - """ Returns list of numbers used to form a valid calc """ - return ast.literal_eval(request.GET.get('numbers_chosen')) +def get_permissible_nums(request) -> List: + """Returns list of numbers used to form a valid calc""" + return ast.literal_eval(request.GET.get("numbers_chosen")) -def get_nums_used(players_calc: str) -> list: - """ Returns list of numbers used to form player's calc """ - nums_used = re.split(r'; |, |\*|\/|\+|\-|\(|\)', players_calc) - nums_used[:] = (int(item) for item in nums_used if item != '') +def get_nums_used(players_calc: str) -> List: + """Returns list of numbers used to form player's calc""" + nums_used = re.split(r"; |, |\*|\/|\+|\-|\(|\)", players_calc) + nums_used[:] = (int(item) for item in nums_used if item != "") return nums_used def is_calc_valid(request, players_calc) -> bool: - """ Validates numbers used for player's calc are permissible """ + """Validates numbers used for player's calc are permissible""" players_calc = strip_spaces(players_calc) nums_used = get_nums_used(players_calc) permissible_nums = get_permissible_nums(request) diff --git a/apps/countdown_numbers/views.py b/apps/countdown_numbers/views.py index 564d50ae..d5141147 100644 --- a/apps/countdown_numbers/views.py +++ b/apps/countdown_numbers/views.py @@ -10,84 +10,82 @@ def selection_screen(request): - if request.method == 'POST': + if request.method == "POST": form = NumberSelectionForm(request.POST) if form.is_valid(): - num_from_top = form.cleaned_data.get('num_from_top') + num_from_top = form.cleaned_data.get("num_from_top") game_screen_url = logic.build_game_url(num_from_top) return redirect(game_screen_url) else: form = NumberSelectionForm() - return render(request, 'countdown_numbers/selection.html', {'form': form}) + return render(request, "countdown_numbers/selection.html", {"form": form}) def game_screen(request): - if request.method == 'POST': + if request.method == "POST": form = SelectedNumbersForm(request.POST) - calc_entered = form['players_calculation'].data + calc_entered = form["players_calculation"].data checks = validations.calc_entered_is_valid(calc_entered) if not all(checks): validations.output_message(request, checks) - return redirect(request.META['HTTP_REFERER']) + return redirect(request.META["HTTP_REFERER"]) if form.is_valid(): - base_url = reverse('countdown_numbers:results') - referer_url = request.META['HTTP_REFERER'].split('?')[-1] + base_url = reverse("countdown_numbers:results") + referer_url = request.META["HTTP_REFERER"].split("?")[-1] players_calc_url = urlencode( - {'players_calculation': form.cleaned_data.get('players_calculation')}) + {"players_calculation": form.cleaned_data.get("players_calculation")} + ) results_screen_url = f"{base_url}?{referer_url}&{players_calc_url}" return redirect(results_screen_url) else: - numbers_chosen = request.GET['numbers_chosen'] - context = { - 'form': SelectedNumbersForm(), - 'game_nums': logic.get_game_nums(numbers_chosen) - } + numbers_chosen = request.GET["numbers_chosen"] + context = {"form": SelectedNumbersForm(), "game_nums": logic.get_game_nums(numbers_chosen)} - return render(request, 'countdown_numbers/game.html', context) + return render(request, "countdown_numbers/game.html", context) def results_screen(request): - players_calc = request.GET.get('players_calculation') + players_calc = request.GET.get("players_calculation") valid_calc = validations.is_calc_valid(request, players_calc) player_num_achieved = logic.get_player_num_achieved(players_calc) - target_number = int(request.GET.get('target_number')) + target_number = int(request.GET.get("target_number")) player_score, comp_score = 0, 0 if valid_calc: player_score = logic.get_score_awarded(target_number, player_num_achieved) game_nums = validations.get_permissible_nums(request) best_solution = logic.get_best_solution(game_nums, target_number) - best_solution = best_solution.replace(chr(215), '*').replace(chr(247), '/') + best_solution = best_solution.replace(chr(215), "*").replace(chr(247), "/") comp_num_achieved = int(eval(best_solution)) solution_str = f""" {best_solution.replace( '*', chr(215)).replace('/', chr(247))} = {comp_num_achieved}""".strip() answers = { - 'player_num_achieved': player_num_achieved, - 'comp_num_achieved': comp_num_achieved, + "player_num_achieved": player_num_achieved, + "comp_num_achieved": comp_num_achieved, } game_result = logic.get_game_result(target_number, answers) - if valid_calc and game_result != 'comp_num_achieved': + if valid_calc and game_result != "comp_num_achieved": player_score = logic.get_score_awarded(target_number, player_num_achieved) - if game_result != 'player_num_achieved': + if game_result != "player_num_achieved": comp_score = logic.get_score_awarded(target_number, comp_num_achieved) context = { - 'game_nums': game_nums, - 'valid_calc': valid_calc, - 'target_number': target_number, - 'player_num_achieved': player_num_achieved, - 'comp_num_achieved': comp_num_achieved, - 'solution_str': solution_str, - 'player_score': player_score, - 'comp_score': comp_score, - 'game_result': game_result, + "game_nums": game_nums, + "valid_calc": valid_calc, + "target_number": target_number, + "player_num_achieved": player_num_achieved, + "comp_num_achieved": comp_num_achieved, + "solution_str": solution_str, + "player_score": player_score, + "comp_score": comp_score, + "game_result": game_result, } utils.create_record(context) - return render(request, 'countdown_numbers/results.html', context) + return render(request, "countdown_numbers/results.html", context) diff --git a/apps/cv/apps.py b/apps/cv/apps.py index be073c3d..43caabac 100644 --- a/apps/cv/apps.py +++ b/apps/cv/apps.py @@ -2,4 +2,4 @@ class CvConfig(AppConfig): - name = 'apps.cv' + name = "apps.cv" diff --git a/apps/cv/tests/test_urls.py b/apps/cv/tests/test_urls.py index b61ea013..2e5bebe5 100644 --- a/apps/cv/tests/test_urls.py +++ b/apps/cv/tests/test_urls.py @@ -4,7 +4,6 @@ class TestUrls: - def test_cv(self): - path = reverse('cv:cv') + path = reverse("cv:cv") assert path, CVView.as_view().__name__ diff --git a/apps/cv/tests/test_views.py b/apps/cv/tests/test_views.py index 71fe80b7..e7772835 100644 --- a/apps/cv/tests/test_views.py +++ b/apps/cv/tests/test_views.py @@ -3,8 +3,8 @@ def test_cv(rf): - """ Asserts any user can access the `cv` page """ - path = reverse('cv:cv') + """Asserts any user can access the `cv` page""" + path = reverse("cv:cv") request = rf.get(path) response = CVView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" diff --git a/apps/cv/urls.py b/apps/cv/urls.py index b5b3e979..b7f4c506 100644 --- a/apps/cv/urls.py +++ b/apps/cv/urls.py @@ -3,8 +3,8 @@ from apps.cv.views import CVView -app_name = 'cv' +app_name = "cv" urlpatterns = [ - path('', CVView.as_view(), name='cv'), + path("", CVView.as_view(), name="cv"), ] diff --git a/apps/cv/views.py b/apps/cv/views.py index 278310f4..3bd02b36 100644 --- a/apps/cv/views.py +++ b/apps/cv/views.py @@ -2,4 +2,4 @@ class CVView(TemplateView): - template_name = 'cv.html' + template_name = "cv.html" diff --git a/apps/helpers.py b/apps/helpers.py index 0905053c..39698b4d 100644 --- a/apps/helpers.py +++ b/apps/helpers.py @@ -14,8 +14,9 @@ def add_middleware_to_request(request, middleware_class): request.session.save() return request + def add_middlewares(request): - """ Supports adding session/messages middleware to views testing """ + """Supports adding session/messages middleware to views testing""" middlewares = (SessionMiddleware, MessageMiddleware) for middleware in middlewares: add_middleware_to_request(request, middleware) diff --git a/apps/pages/apps.py b/apps/pages/apps.py index eeb47b34..de4b4058 100644 --- a/apps/pages/apps.py +++ b/apps/pages/apps.py @@ -2,4 +2,4 @@ class PagesConfig(AppConfig): - name = 'apps.pages' + name = "apps.pages" diff --git a/apps/pages/templatetags/ext_links.py b/apps/pages/templatetags/ext_links.py index f2c14750..55cddef0 100644 --- a/apps/pages/templatetags/ext_links.py +++ b/apps/pages/templatetags/ext_links.py @@ -53,10 +53,11 @@ class LinkGenerator: Creates either a GitHub source code URL or the query string URL for a list of filtered issues for the given app. """ + @staticmethod @register.simple_tag def github_url(type: str, app: str) -> str: - if type == 'code': + if type == "code": query_str = f"tree/main/apps/{app}" else: query_str = f"issues?q=is%3Aissue+label%3A%22app%3A+{app}%22" @@ -64,71 +65,76 @@ def github_url(type: str, app: str) -> str: class Contacts: - @register.simple_tag(name='google_maps_embed_link') + @register.simple_tag(name="google_maps_embed_link") def google_maps_embed_link(): return "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2430.5527282918692!2d-1.940316583728355!3d52.46912729774982!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x4870bda93e3bf027%3A0x8f4a61d2fb6a1d3f!2sAugustus%20Rd%2C%20Birmingham%20B15%203PA!5e0!3m2!1sen!2suk!4v1630401139728!5m2!1sen!2suk" + class CountdownLetters: - @register.simple_tag(name='countdown_letters_game_rules_link') + @register.simple_tag(name="countdown_letters_game_rules_link") def game_rules(): return "http://wiki.apterous.org/Letters_game" - @register.simple_tag(name='countdown_letters_views_source_code_link') + @register.simple_tag(name="countdown_letters_views_source_code_link") def views_source_code(): - return "https://github.com/WayneLambert/portfolio/blob/main/apps/countdown_letters/views.py" + return ( + "https://github.com/WayneLambert/portfolio/blob/main/apps/countdown_letters/views.py" + ) class CountdownNumbers: - @register.simple_tag(name='countdown_numbers_game_rules_link') + @register.simple_tag(name="countdown_numbers_game_rules_link") def game_rules(): # pragma: no cover return "http://datagenetics.com/blog/august32014/index.html" - @register.simple_tag(name='countdown_numbers_views_source_code_link') + @register.simple_tag(name="countdown_numbers_views_source_code_link") def views_source_code(): - return "https://github.com/WayneLambert/portfolio/blob/main/apps/countdown_numbers/views.py" + return ( + "https://github.com/WayneLambert/portfolio/blob/main/apps/countdown_numbers/views.py" + ) class Scraping: - @register.simple_tag(name='churchill_speech_link') + @register.simple_tag(name="churchill_speech_link") def churchill_speech(): return "https://www.goodreads.com/quotes/55276-i-have-nothing-to-offer-but-blood-toil-tears-and" - @register.simple_tag(name='gettysburg_speech_link') + @register.simple_tag(name="gettysburg_speech_link") def gettysburg_speech(): return "https://www.goodreads.com/work/quotes/4694-the-illustrated-gettysburg-address" - @register.simple_tag(name='scraping_gettysburg_source_code_link') + @register.simple_tag(name="scraping_gettysburg_source_code_link") def gettysburg_source_code(): - scraping_code_url = LinkGenerator.github_url(type='code', app='scraping') + scraping_code_url = LinkGenerator.github_url(type="code", app="scraping") return f"{scraping_code_url}/gettysburg.py" - @register.simple_tag(name='scraping_churchill_source_code_link') + @register.simple_tag(name="scraping_churchill_source_code_link") def churchill_source_code(): - scraping_code_url = LinkGenerator.github_url(type='code', app='scraping') + scraping_code_url = LinkGenerator.github_url(type="code", app="scraping") return f"{scraping_code_url}/churchill.py" - @register.simple_tag(name='scraping_referendum_source_code_link') + @register.simple_tag(name="scraping_referendum_source_code_link") def referendum_source_code(): - scraping_code_url = LinkGenerator.github_url(type='code', app='scraping') + scraping_code_url = LinkGenerator.github_url(type="code", app="scraping") return f"{scraping_code_url}/referendum.py" - @register.simple_tag(name='scraping_sample_ref_results_link') + @register.simple_tag(name="scraping_sample_ref_results_link") def sample_referendum_results(): return "https://www.bbc.co.uk/news/politics/eu_referendum/results/local/a" class TextAnalysis: - @register.simple_tag(name='text_analysis_views_source_code_link') + @register.simple_tag(name="text_analysis_views_source_code_link") def views_source_code(): - scraping_code_url = LinkGenerator.github_url(type='code', app='text_analysis') + scraping_code_url = LinkGenerator.github_url(type="code", app="text_analysis") return f"{scraping_code_url}/views.py" class DataScience: - @register.simple_tag(name='data_science_portfolio_notebooks') + @register.simple_tag(name="data_science_portfolio_notebooks") def notebooks(): return "https://github.com/WayneLambert/data-science-portfolio/tree/main/notebooks" - @register.simple_tag(name='data_science_github_issues_link') + @register.simple_tag(name="data_science_github_issues_link") def github_issues(): return "https://github.com/WayneLambert/portfolio/issues?q=is%3Aissue+label%3A%22data+science%22+is%3Aclosed" diff --git a/apps/pages/tests/helpers.py b/apps/pages/tests/helpers.py index e4445f20..28fda953 100644 --- a/apps/pages/tests/helpers.py +++ b/apps/pages/tests/helpers.py @@ -6,5 +6,5 @@ def app_names(): long_form_apps, short_form_apps = base.PROJECT_APPS, [] for app in long_form_apps: - short_form_apps.append(app.split('.')[1]) + short_form_apps.append(app.split(".")[1]) return short_form_apps diff --git a/apps/pages/tests/test_ext_links.py b/apps/pages/tests/test_ext_links.py index 14885ee4..f2e1a0a8 100644 --- a/apps/pages/tests/test_ext_links.py +++ b/apps/pages/tests/test_ext_links.py @@ -26,132 +26,138 @@ import pytest import requests -from apps.pages.templatetags.ext_links import (Contacts, CountdownLetters, - CountdownNumbers, DataScience, - LinkGenerator, Scraping, SocialMedia, - TextAnalysis,) +from apps.pages.templatetags.ext_links import ( + Contacts, + CountdownLetters, + CountdownNumbers, + DataScience, + LinkGenerator, + Scraping, + SocialMedia, + TextAnalysis, +) from .helpers import app_names -@pytest.mark.slow(reason='Sends a GET request to each link') +@pytest.mark.slow(reason="Sends a GET request to each link") class TestSocialMedia: @staticmethod def test_github_profile(): link = requests.get(SocialMedia.github_profile_link()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_github_portfolio(): link = requests.get(SocialMedia.github_portfolio_link()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_github_portfolio_issues_link(): link = requests.get(SocialMedia.github_portfolio_issues_link()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_stack_overflow_profile(): link = requests.get(SocialMedia.stack_overflow_profile_link()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_google_maps_location(): link = requests.get(SocialMedia.google_maps_location_link()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" -@pytest.mark.slow(reason='Sends a GET request to each link') -@pytest.mark.parametrize(argnames='type', argvalues=['code', 'issues']) -@pytest.mark.parametrize(argnames='app', argvalues=app_names()) +@pytest.mark.slow(reason="Sends a GET request to each link") +@pytest.mark.parametrize(argnames="type", argvalues=["code", "issues"]) +@pytest.mark.parametrize(argnames="app", argvalues=app_names()) class TestLinkGenerator: @staticmethod def test_github_url(type, app): link = requests.get(LinkGenerator.github_url(type, app)) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" -@pytest.mark.slow(reason='Sends a GET request') +@pytest.mark.slow(reason="Sends a GET request") class TestContacts: @staticmethod def test_google_maps_embed_link(): link = requests.get(Contacts.google_maps_embed_link()) - assert link.status_code == 200, 'Should return an `OK` status' - assert b'B15 3PA' in link.content, 'Should contain the postcode of B15 3PA' + assert link.status_code == 200, "Should return an `OK` status" + assert b"B15 3PA" in link.content, "Should contain the postcode of B15 3PA" -@pytest.mark.skipif('GITHUB_RUN_ID' in os.environ, reason='Times out in GitHub Actions') -@pytest.mark.slow(reason='Sends a GET request to each link') +@pytest.mark.skipif("GITHUB_RUN_ID" in os.environ, reason="Times out in GitHub Actions") +@pytest.mark.slow(reason="Sends a GET request to each link") class TestCountdownLetters: @staticmethod def test_game_rules(): link = requests.get(CountdownLetters.game_rules(), timeout=10) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_views_source_code(): link = requests.get(CountdownLetters.views_source_code()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" -@pytest.mark.slow(reason='Sends a GET request to each link') +@pytest.mark.slow(reason="Sends a GET request to each link") class TestCountdownNumbers: @staticmethod def test_views_source_code(): link = requests.get(CountdownNumbers.views_source_code()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" -@pytest.mark.slow(reason='Sends a GET request to each link') +@pytest.mark.slow(reason="Sends a GET request to each link") class TestScraping: @staticmethod def test_churchill_speech(): link = requests.get(Scraping.churchill_speech()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_gettysburg_speech(): link = requests.get(Scraping.gettysburg_speech()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_gettysburg_source_code(): link = requests.get(Scraping.gettysburg_source_code()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_churchill_source_code(): link = requests.get(Scraping.churchill_source_code()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_referendum_source_code(): link = requests.get(Scraping.referendum_source_code()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_sample_referendum_results(): link = requests.get(Scraping.sample_referendum_results()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" -@pytest.mark.slow(reason='Sends a GET request') +@pytest.mark.slow(reason="Sends a GET request") class TestTextAnalysis: @staticmethod def test_views_source_code(): link = requests.get(TextAnalysis.views_source_code()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" -@pytest.mark.slow(reason='Sends a GET request to each link') +@pytest.mark.slow(reason="Sends a GET request to each link") class TestDataScience: @staticmethod def test_notebooks(): link = requests.get(DataScience.notebooks()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" @staticmethod def test_github_issues(): link = requests.get(DataScience.github_issues()) - assert link.status_code == 200, 'Should return an `OK` status' + assert link.status_code == 200, "Should return an `OK` status" diff --git a/apps/pages/tests/test_urls.py b/apps/pages/tests/test_urls.py index 6f34de29..7e9df222 100644 --- a/apps/pages/tests/test_urls.py +++ b/apps/pages/tests/test_urls.py @@ -1,97 +1,108 @@ from django.urls import reverse -from apps.pages.views import (AboutMeView, APIReviewView, BackEndSkillsView, - BlogReviewView, CountdownLettersReviewView, - CountdownNumbersReviewView, DataScienceReviewView, - FrontEndSkillsView, InfrastructureSkillsView, PortfolioView, - PrivacyPolicyView, ReadingListView, RouletteReviewView, - ScrapingReviewView, SiteHomeView, SoftwareSkillsView, - TextAnalysisReviewView,) +from apps.pages.views import ( + AboutMeView, + APIReviewView, + BackEndSkillsView, + BlogReviewView, + CountdownLettersReviewView, + CountdownNumbersReviewView, + DataScienceReviewView, + FrontEndSkillsView, + InfrastructureSkillsView, + PortfolioView, + PrivacyPolicyView, + ReadingListView, + RouletteReviewView, + ScrapingReviewView, + SiteHomeView, + SoftwareSkillsView, + TextAnalysisReviewView, +) class TestUrls: - def test_home(self): - """ Verify that the `home` url invokes intended view """ - path = reverse('pages:home') + """Verify that the `home` url invokes intended view""" + path = reverse("pages:home") assert path, SiteHomeView.as_view().__name__ def test_portfolio(self): - """ Verify that the `portfolio` url invokes intended view """ - path = reverse('pages:portfolio') + """Verify that the `portfolio` url invokes intended view""" + path = reverse("pages:portfolio") assert path, PortfolioView.as_view().__name__ def test_blog_review(self): - """ Verify that the `blog_review` url invokes intended view """ - path = reverse('pages:blog_review') + """Verify that the `blog_review` url invokes intended view""" + path = reverse("pages:blog_review") assert path, BlogReviewView.as_view().__name__ def test_api_review(self): - """ Verify that the `api_review` url invokes intended view """ - path = reverse('pages:api_review') + """Verify that the `api_review` url invokes intended view""" + path = reverse("pages:api_review") assert path, APIReviewView.as_view().__name__ def test_countdown_letters_review(self): - """ Verify that the `countdown_letters_review` url invokes intended view """ - path = reverse('pages:countdown_letters_review') + """Verify that the `countdown_letters_review` url invokes intended view""" + path = reverse("pages:countdown_letters_review") assert path, CountdownLettersReviewView.as_view().__name__ def test_countdown_numbers_review(self): - """ Verify that the `countdown_numbers_review` url invokes intended view """ - path = reverse('pages:countdown_numbers_review') + """Verify that the `countdown_numbers_review` url invokes intended view""" + path = reverse("pages:countdown_numbers_review") assert path, CountdownNumbersReviewView.as_view().__name__ def test_roulette_review(self): - """ Verify that the `roulette_review` url invokes intended view """ - path = reverse('pages:roulette_review') + """Verify that the `roulette_review` url invokes intended view""" + path = reverse("pages:roulette_review") assert path, RouletteReviewView.as_view().__name__ def test_scraping_review(self): - """ Verify that the `scraping_review` url invokes intended view """ - path = reverse('pages:scraping_review') + """Verify that the `scraping_review` url invokes intended view""" + path = reverse("pages:scraping_review") assert path, ScrapingReviewView.as_view().__name__ def test_text_analysis_review(self): - """ Verify that the `text_analysis_review` url invokes intended view """ - path = reverse('pages:text_analysis_review') + """Verify that the `text_analysis_review` url invokes intended view""" + path = reverse("pages:text_analysis_review") assert path, TextAnalysisReviewView.as_view().__name__ def test_data_science_review(self): - """ Verify that the `data_science_review` url invokes intended view """ - path = reverse('pages:data_science_review') + """Verify that the `data_science_review` url invokes intended view""" + path = reverse("pages:data_science_review") assert path, DataScienceReviewView.as_view().__name__ def test_back_end_skills(self): - """ Verify that the `back_end_skills` url invokes intended view """ - path = reverse('pages:back_end_skills') + """Verify that the `back_end_skills` url invokes intended view""" + path = reverse("pages:back_end_skills") assert path, BackEndSkillsView.as_view().__name__ def test_front_end_skills(self): - """ Verify that the `front_end_skills` url invokes intended view """ - path = reverse('pages:front_end_skills') + """Verify that the `front_end_skills` url invokes intended view""" + path = reverse("pages:front_end_skills") assert path, FrontEndSkillsView.as_view().__name__ def test_infrastructure_skills(self): - """ Verify that the `infrastructure_skills` url invokes intended view """ - path = reverse('pages:infrastructure_skills') + """Verify that the `infrastructure_skills` url invokes intended view""" + path = reverse("pages:infrastructure_skills") assert path, InfrastructureSkillsView.as_view().__name__ def test_software_skills(self): - """ Verify that the `software_skills` url invokes intended view """ - path = reverse('pages:software_skills') + """Verify that the `software_skills` url invokes intended view""" + path = reverse("pages:software_skills") assert path, SoftwareSkillsView.as_view().__name__ def test_about_me(self): - """ Verify that the `about_me` url invokes intended view """ - path = reverse('pages:about_me') + """Verify that the `about_me` url invokes intended view""" + path = reverse("pages:about_me") assert path, AboutMeView.as_view().__name__ def test_privacy(self): - """ Verify that the `privacy` url invokes intended view """ - path = reverse('pages:privacy') + """Verify that the `privacy` url invokes intended view""" + path = reverse("pages:privacy") assert path, PrivacyPolicyView.as_view().__name__ def test_reading_list(self): - """ Verify that the `reading_list` url invokes intended view """ - path = reverse('pages:reading_list') + """Verify that the `reading_list` url invokes intended view""" + path = reverse("pages:reading_list") assert path, ReadingListView.as_view().__name__ diff --git a/apps/pages/tests/test_views.py b/apps/pages/tests/test_views.py index 484aa3c0..ae48f18e 100644 --- a/apps/pages/tests/test_views.py +++ b/apps/pages/tests/test_views.py @@ -2,203 +2,237 @@ import pytest -from apps.pages.views import (AboutMeView, APIReviewView, BackEndSkillsView, - BadRequestView, BlogReviewView, CountdownLettersReviewView, - CountdownNumbersReviewView, DataScienceReviewView, - FrontEndSkillsView, InfrastructureSkillsView, - PageNotFoundView, PermissionDeniedView, PortfolioView, - PrivacyPolicyView, ReadingListView, RouletteReviewView, - ScrapingReviewView, SiteHomeView, SoftwareSkillsView, - TextAnalysisReviewView, handler500,) +from apps.pages.views import ( + AboutMeView, + APIReviewView, + BackEndSkillsView, + BadRequestView, + BlogReviewView, + CountdownLettersReviewView, + CountdownNumbersReviewView, + DataScienceReviewView, + FrontEndSkillsView, + InfrastructureSkillsView, + PageNotFoundView, + PermissionDeniedView, + PortfolioView, + PrivacyPolicyView, + ReadingListView, + RouletteReviewView, + ScrapingReviewView, + SiteHomeView, + SoftwareSkillsView, + TextAnalysisReviewView, + handler500, +) pytestmark = pytest.mark.django_db(reset_sequences=True) -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestStaticPagesViews: def test_home_view(self, rf, all_users): - """ Asserts any user can GET the site's `home` page """ - path = reverse('pages:home') + """Asserts any user can GET the site's `home` page""" + path = reverse("pages:home") request = rf.get(path) request.user = all_users response = SiteHomeView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_portfolio_view(self, rf, all_users): - """ Asserts any user can GET the `portfolio` page """ - path = reverse('pages:portfolio') + """Asserts any user can GET the `portfolio` page""" + path = reverse("pages:portfolio") request = rf.get(path) request.user = all_users response = PortfolioView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_reading_list_view(self, rf, all_users): - """ Asserts any user can GET the `reading list` page """ - path = reverse('pages:reading_list') + """Asserts any user can GET the `reading list` page""" + path = reverse("pages:reading_list") request = rf.get(path) request.user = all_users response = ReadingListView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_about_me_view(self, rf, all_users): - """ Asserts any user can GET the `about me` page """ - path = reverse('pages:about_me') + """Asserts any user can GET the `about me` page""" + path = reverse("pages:about_me") request = rf.get(path) request.user = all_users response = AboutMeView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_privacy_policy_view(self, rf, all_users): - """ Asserts any user can GET the site's `privacy policy` page """ - path = reverse('pages:privacy') + """Asserts any user can GET the site's `privacy policy` page""" + path = reverse("pages:privacy") request = rf.get(path) request.user = all_users response = PrivacyPolicyView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestSkillsPagesViews: def test_back_end_skills_view(self, rf, all_users): - """ Asserts any user can GET the `back end skills` page """ - path = reverse('pages:back_end_skills') + """Asserts any user can GET the `back end skills` page""" + path = reverse("pages:back_end_skills") request = rf.get(path) request.user = all_users response = BackEndSkillsView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_front_end_skills_view(self, rf, all_users): - """ Asserts any user can GET the `front end skills` page """ - path = reverse('pages:front_end_skills') + """Asserts any user can GET the `front end skills` page""" + path = reverse("pages:front_end_skills") request = rf.get(path) request.user = all_users response = FrontEndSkillsView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_infrastructure_skills_view(self, rf, all_users): - """ Asserts any user can GET the `infrastructure skills` page """ - path = reverse('pages:infrastructure_skills') + """Asserts any user can GET the `infrastructure skills` page""" + path = reverse("pages:infrastructure_skills") request = rf.get(path) request.user = all_users response = InfrastructureSkillsView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_software_skills_view(self, rf, all_users): - """ Asserts any user can GET the site's `software skills` page """ - path = reverse('pages:software_skills') + """Asserts any user can GET the site's `software skills` page""" + path = reverse("pages:software_skills") request = rf.get(path) request.user = all_users response = SoftwareSkillsView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestReviewsPagesViews: def test_blog_review_view(self, rf, all_users): - """ Asserts any user can GET the `blog review` page """ - path = reverse('pages:blog_review') + """Asserts any user can GET the `blog review` page""" + path = reverse("pages:blog_review") request = rf.get(path) request.user = all_users response = BlogReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_api_review_view(self, rf, all_users): - """ Asserts any user can GET the `API review` page """ - path = reverse('pages:api_review') + """Asserts any user can GET the `API review` page""" + path = reverse("pages:api_review") request = rf.get(path) request.user = all_users response = APIReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_countdown_letters_review_view(self, rf, all_users): - """ Asserts any user can GET the `Countdown Letters review` page """ - path = reverse('pages:countdown_letters_review') + """Asserts any user can GET the `Countdown Letters review` page""" + path = reverse("pages:countdown_letters_review") request = rf.get(path) request.user = all_users response = CountdownLettersReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_countdown_numbers_review_view(self, rf, all_users): - """ Asserts any user can GET the `Countdown Numbers review` page """ - path = reverse('pages:countdown_numbers_review') + """Asserts any user can GET the `Countdown Numbers review` page""" + path = reverse("pages:countdown_numbers_review") request = rf.get(path) request.user = all_users response = CountdownNumbersReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_roulette_review_view(self, rf, all_users): - """ Asserts any user can GET the `Roulette review` page """ - path = reverse('pages:roulette_review') + """Asserts any user can GET the `Roulette review` page""" + path = reverse("pages:roulette_review") request = rf.get(path) request.user = all_users response = RouletteReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_scraping_review_view(self, request, rf, all_users): - """ Asserts any user can GET the `Scraping review` page """ - path = reverse('pages:scraping_review') + """Asserts any user can GET the `Scraping review` page""" + path = reverse("pages:scraping_review") request = rf.get(path) request.user = all_users response = ScrapingReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_text_analysis_review_view(self, rf, all_users): - """ Asserts any user can GET the `Text Analysis review` page """ - path = reverse('pages:text_analysis_review') + """Asserts any user can GET the `Text Analysis review` page""" + path = reverse("pages:text_analysis_review") request = rf.get(path) request.user = all_users response = TextAnalysisReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" def test_data_science_review_view(self, rf, all_users): - """ Asserts any user can GET the `Data Science review` page """ - path = reverse('pages:data_science_review') + """Asserts any user can GET the `Data Science review` page""" + path = reverse("pages:data_science_review") request = rf.get(path) request.user = all_users response = DataScienceReviewView.as_view()(request) - assert response.status_code == 200, 'Should be callable by anyone' + assert response.status_code == 200, "Should be callable by anyone" + class TestCustomErrorPages: - @pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) + @pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, + ) def test_400_page(self, rf, all_users): - """ Asserts a user can GET a 400 page """ - path = reverse('pages:home') + """Asserts a user can GET a 400 page""" + path = reverse("pages:home") request = rf.get(path) request.user = all_users response = BadRequestView.as_view()(request) - assert response.status_code == 200, 'the custom 400 template should GET `OK` response' + assert response.status_code == 200, "the custom 400 template should GET `OK` response" def test_403_page(self, rf, pub_post, auth_user): - """ Asserts a user can GET a 403 page """ - kwargs = {'slug': pub_post.slug} - path = reverse('blog:post_update', kwargs=kwargs) + """Asserts a user can GET a 403 page""" + kwargs = {"slug": pub_post.slug} + path = reverse("blog:post_update", kwargs=kwargs) request = rf.get(path) request.user = auth_user response = PermissionDeniedView.as_view()(request) - assert response.status_code == 200, 'the custom 403 template should GET an `OK` response' + assert response.status_code == 200, "the custom 403 template should GET an `OK` response" - @pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) + @pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, + ) def test_404_page(self, rf, all_users): - """ Asserts a user can GET a 404 page """ - kwargs = {'slug': 'post-slug-that-does-not-exist'} - path = reverse('blog:post_update', kwargs=kwargs) + """Asserts a user can GET a 404 page""" + kwargs = {"slug": "post-slug-that-does-not-exist"} + path = reverse("blog:post_update", kwargs=kwargs) request = rf.get(path) request.user = all_users response = PageNotFoundView.as_view()(request) - assert response.status_code == 200, 'the custom 404 template should GET `OK` response' + assert response.status_code == 200, "the custom 404 template should GET `OK` response" - @pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) + @pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, + ) def test_500_page(self, rf, all_users): - """ Asserts a user can GET a 500 page """ - path = reverse('pages:home') + """Asserts a user can GET a 500 page""" + path = reverse("pages:home") request = rf.get(path) request.user = all_users response = handler500(request) - assert response.status_code == 500, 'the user should GET a 500 status code' + assert response.status_code == 500, "the user should GET a 500 status code" diff --git a/apps/pages/urls.py b/apps/pages/urls.py index 5dcf78e2..a0a8b1cb 100644 --- a/apps/pages/urls.py +++ b/apps/pages/urls.py @@ -1,38 +1,64 @@ from django.urls import path -from apps.pages.views import (AboutMeView, APIReviewView, BackEndSkillsView, - BlogReviewView, CountdownLettersReviewView, - CountdownNumbersReviewView, DataScienceReviewView, - FrontEndSkillsView, InfrastructureSkillsView, PortfolioView, - PrivacyPolicyView, ReadingListView, RouletteReviewView, - ScrapingReviewView, SiteHomeView, SkillsView, SoftwareSkillsView, - TextAnalysisReviewView,) +from apps.pages.views import ( + AboutMeView, + APIReviewView, + BackEndSkillsView, + BlogReviewView, + CountdownLettersReviewView, + CountdownNumbersReviewView, + DataScienceReviewView, + FrontEndSkillsView, + InfrastructureSkillsView, + PortfolioView, + PrivacyPolicyView, + ReadingListView, + RouletteReviewView, + ScrapingReviewView, + SiteHomeView, + SkillsView, + SoftwareSkillsView, + TextAnalysisReviewView, +) -app_name = 'pages' +app_name = "pages" urlpatterns = [ - path('', SiteHomeView.as_view(), name='home'), - path('portfolio/', PortfolioView.as_view(), name='portfolio'), - path('portfolio/reviews/blog/', BlogReviewView.as_view(), name='blog_review'), - path('portfolio/reviews/api/', APIReviewView.as_view(), name='api_review'), - path('portfolio/reviews/countdown-letters/', - CountdownLettersReviewView.as_view(), name='countdown_letters_review'), - path('portfolio/reviews/countdown-numbers/', - CountdownNumbersReviewView.as_view(), name='countdown_numbers_review'), - path('portfolio/reviews/roulette/', RouletteReviewView.as_view(), name='roulette_review'), - path('portfolio/reviews/scraping/', ScrapingReviewView.as_view(), name='scraping_review'), - path('portfolio/reviews/text-analysis/', - TextAnalysisReviewView.as_view(), name='text_analysis_review'), - path('portfolio/reviews/data-science/', - DataScienceReviewView.as_view(), name='data_science_review'), - path('skills/', SkillsView.as_view(), name='skills'), - path('skills/back-end/', BackEndSkillsView.as_view(), name='back_end_skills'), - path('skills/front-end/', FrontEndSkillsView.as_view(), name='front_end_skills'), - path('skills/infrastructure/', - InfrastructureSkillsView.as_view(), name='infrastructure_skills'), - path('skills/software/', SoftwareSkillsView.as_view(), name='software_skills'), - path('about-me/', AboutMeView.as_view(), name='about_me'), - path('privacy/', PrivacyPolicyView.as_view(), name='privacy'), - path('reading-list/', ReadingListView.as_view(), name='reading_list'), + path("", SiteHomeView.as_view(), name="home"), + path("portfolio/", PortfolioView.as_view(), name="portfolio"), + path("portfolio/reviews/blog/", BlogReviewView.as_view(), name="blog_review"), + path("portfolio/reviews/api/", APIReviewView.as_view(), name="api_review"), + path( + "portfolio/reviews/countdown-letters/", + CountdownLettersReviewView.as_view(), + name="countdown_letters_review", + ), + path( + "portfolio/reviews/countdown-numbers/", + CountdownNumbersReviewView.as_view(), + name="countdown_numbers_review", + ), + path("portfolio/reviews/roulette/", RouletteReviewView.as_view(), name="roulette_review"), + path("portfolio/reviews/scraping/", ScrapingReviewView.as_view(), name="scraping_review"), + path( + "portfolio/reviews/text-analysis/", + TextAnalysisReviewView.as_view(), + name="text_analysis_review", + ), + path( + "portfolio/reviews/data-science/", + DataScienceReviewView.as_view(), + name="data_science_review", + ), + path("skills/", SkillsView.as_view(), name="skills"), + path("skills/back-end/", BackEndSkillsView.as_view(), name="back_end_skills"), + path("skills/front-end/", FrontEndSkillsView.as_view(), name="front_end_skills"), + path( + "skills/infrastructure/", InfrastructureSkillsView.as_view(), name="infrastructure_skills" + ), + path("skills/software/", SoftwareSkillsView.as_view(), name="software_skills"), + path("about-me/", AboutMeView.as_view(), name="about_me"), + path("privacy/", PrivacyPolicyView.as_view(), name="privacy"), + path("reading-list/", ReadingListView.as_view(), name="reading_list"), ] diff --git a/apps/pages/views.py b/apps/pages/views.py index 6517c56c..8ed2528f 100644 --- a/apps/pages/views.py +++ b/apps/pages/views.py @@ -10,98 +10,99 @@ class SiteHomeView(ListView): Limits published posts dataset to three most recently updated posts for the site's home page. """ + model = Post - template_name = 'home.html' - context_object_name = 'posts' + template_name = "home.html" + context_object_name = "posts" queryset = Post.published.all()[:3] # Static Pages class PortfolioView(TemplateView): - template_name = 'portfolio.html' + template_name = "portfolio.html" class ReadingListView(TemplateView): - template_name = 'reading_list.html' + template_name = "reading_list.html" class AboutMeView(TemplateView): - template_name = 'about_me.html' + template_name = "about_me.html" class PrivacyPolicyView(TemplateView): - template_name = 'privacy.html' + template_name = "privacy.html" # Skills class SkillsView(RedirectView): permanent = True - pattern_name = 'pages:back_end_skills' + pattern_name = "pages:back_end_skills" class BackEndSkillsView(TemplateView): - template_name = 'skills/back_end.html' + template_name = "skills/back_end.html" class FrontEndSkillsView(TemplateView): - template_name = 'skills/front_end.html' + template_name = "skills/front_end.html" class InfrastructureSkillsView(TemplateView): - template_name = 'skills/infrastructure.html' + template_name = "skills/infrastructure.html" class SoftwareSkillsView(TemplateView): - template_name = 'skills/software.html' + template_name = "skills/software.html" # Reviews class BlogReviewView(TemplateView): - template_name = 'reviews/blog.html' + template_name = "reviews/blog.html" class APIReviewView(TemplateView): - template_name = 'reviews/api.html' + template_name = "reviews/api.html" class CountdownLettersReviewView(TemplateView): - template_name = 'reviews/countdown_letters.html' + template_name = "reviews/countdown_letters.html" class CountdownNumbersReviewView(TemplateView): - template_name = 'reviews/countdown_numbers.html' + template_name = "reviews/countdown_numbers.html" class RouletteReviewView(TemplateView): - template_name = 'reviews/roulette.html' + template_name = "reviews/roulette.html" class ScrapingReviewView(TemplateView): - template_name = 'reviews/scraping.html' + template_name = "reviews/scraping.html" class TextAnalysisReviewView(TemplateView): - template_name = 'reviews/text_analysis.html' + template_name = "reviews/text_analysis.html" class DataScienceReviewView(TemplateView): - template_name = 'reviews/data_science.html' + template_name = "reviews/data_science.html" # Custom Error Templates class BadRequestView(TemplateView): - template_name = 'errors/400.html' + template_name = "errors/400.html" class PermissionDeniedView(TemplateView): - template_name = 'errors/403.html' + template_name = "errors/403.html" class PageNotFoundView(TemplateView): - template_name = 'errors/404.html' + template_name = "errors/404.html" def handler500(request): # pragma: no cover - response = render(request, template_name='errors/500.html', context={}) + response = render(request, template_name="errors/500.html", context={}) response.status_code = 500 return response diff --git a/apps/roulette/apps.py b/apps/roulette/apps.py index 9ffcc27a..cb2cdb92 100644 --- a/apps/roulette/apps.py +++ b/apps/roulette/apps.py @@ -2,4 +2,4 @@ class RouletteConfig(AppConfig): - name = 'apps.roulette' + name = "apps.roulette" diff --git a/apps/roulette/logging.py b/apps/roulette/logging.py index db23fa4b..8eddcc6b 100644 --- a/apps/roulette/logging.py +++ b/apps/roulette/logging.py @@ -4,12 +4,12 @@ from django.conf import settings -log_file = os.path.join(settings.APPS_DIR, 'roulette/holiday_roulette.log') +log_file = os.path.join(settings.APPS_DIR, "roulette/holiday_roulette.log") logging.basicConfig( filename=log_file, level=logging.INFO, - encoding='utf-8', + encoding="utf-8", format="%(asctime)s.%(msecs)03d %(filename)s: %(lineno)d - %(funcName)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S" + datefmt="%Y-%m-%d %H:%M:%S", ) diff --git a/apps/roulette/logic.py b/apps/roulette/logic.py index 31af8b38..4ef876e2 100644 --- a/apps/roulette/logic.py +++ b/apps/roulette/logic.py @@ -3,7 +3,7 @@ import random import time -from typing import List, Tuple +from typing import Dict, List, Tuple from django.conf import settings @@ -12,34 +12,34 @@ class Game: places_to_go = { - 'Aruba': 0, - 'Barbados': 0, - 'Bora Bora': 0, - 'Fiji': 0, - 'Hawaii': 0, - 'Koh Samui': 0, - 'Langkawi': 0, - 'Maldives': 0, - 'Palawan': 0, - 'Santorini': 0, - 'Seychelles': 0, - 'St. Lucia': 0, + "Aruba": 0, + "Barbados": 0, + "Bora Bora": 0, + "Fiji": 0, + "Hawaii": 0, + "Koh Samui": 0, + "Langkawi": 0, + "Maldives": 0, + "Palawan": 0, + "Santorini": 0, + "Seychelles": 0, + "St. Lucia": 0, } def clear_down_log_file(): - """ Clears down the contents of the log file """ - with open(log_file, 'w'): + """Clears down the contents of the log file""" + with open(log_file, "w"): pass def reset_places_to_go(): - """ Resets the dictionary count to zero for each destination """ + """Resets the dictionary count to zero for each destination""" for key in Game.places_to_go: Game.places_to_go[key] = 0 -def get_game_result() -> Tuple[dict, str, int, list]: +def get_game_result() -> Tuple[Dict[str, int], List]: """ Gets the result of the roulette game and introduces suspense in the game due to a time.sleep() function. @@ -60,26 +60,26 @@ def get_game_result() -> Tuple[dict, str, int, list]: def get_picture_url(destination: str) -> str: - """ Builds the URL for the destinations' image from an S3 bucket """ + """Builds the URL for the destinations' image from an S3 bucket""" aws_folder = f"{settings.AWS_BASE_BUCKET_ADDRESS}/post_images/holiday-roulette/" locations = { - 'Aruba': f"{aws_folder}aruba.jpg", - 'Barbados': f"{aws_folder}barbados.jpg", - 'Bora Bora': f"{aws_folder}bora-bora.jpg", - 'Fiji': f"{aws_folder}fiji.jpg", - 'Hawaii': f"{aws_folder}hawaii.jpg", - 'Koh Samui': f"{aws_folder}koh-samui.jpg", - 'Langkawi': f"{aws_folder}langkawi.jpg", - 'Maldives': f"{aws_folder}maldives.jpg", - 'Palawan': f"{aws_folder}palawan.jpg", - 'Santorini': f"{aws_folder}santorini.jpg", - 'Seychelles': f"{aws_folder}seychelles.jpg", - 'St. Lucia': f"{aws_folder}st-lucia.jpg", + "Aruba": f"{aws_folder}aruba.jpg", + "Barbados": f"{aws_folder}barbados.jpg", + "Bora Bora": f"{aws_folder}bora-bora.jpg", + "Fiji": f"{aws_folder}fiji.jpg", + "Hawaii": f"{aws_folder}hawaii.jpg", + "Koh Samui": f"{aws_folder}koh-samui.jpg", + "Langkawi": f"{aws_folder}langkawi.jpg", + "Maldives": f"{aws_folder}maldives.jpg", + "Palawan": f"{aws_folder}palawan.jpg", + "Santorini": f"{aws_folder}santorini.jpg", + "Seychelles": f"{aws_folder}seychelles.jpg", + "St. Lucia": f"{aws_folder}st-lucia.jpg", } return locations.get(destination, "Invalid destination") def read_log_file() -> List: - """ Reads log file to a list for later rendering to results page """ - with open(log_file, 'r') as file: + """Reads log file to a list for later rendering to results page""" + with open(log_file, "r") as file: return [line for line in file.readlines()] diff --git a/apps/roulette/tests/helpers.py b/apps/roulette/tests/helpers.py index 400ba4ba..189b7949 100644 --- a/apps/roulette/tests/helpers.py +++ b/apps/roulette/tests/helpers.py @@ -2,6 +2,16 @@ destinations = { - 'Aruba', 'Barbados', 'Bora Bora', 'Fiji', 'Hawaii', 'Koh Samui', - 'Langkawi', 'Maldives', 'Palawan', 'Santorini', 'Seychelles', 'St. Lucia' + "Aruba", + "Barbados", + "Bora Bora", + "Fiji", + "Hawaii", + "Koh Samui", + "Langkawi", + "Maldives", + "Palawan", + "Santorini", + "Seychelles", + "St. Lucia", } diff --git a/apps/roulette/tests/test_logging.py b/apps/roulette/tests/test_logging.py index 59ef6226..a155bfc2 100644 --- a/apps/roulette/tests/test_logging.py +++ b/apps/roulette/tests/test_logging.py @@ -5,4 +5,4 @@ def test_log_file_setup(): - assert os.path.join(base.APPS_DIR, 'roulette/holiday_roulette.log') in log_file + assert os.path.join(base.APPS_DIR, "roulette/holiday_roulette.log") in log_file diff --git a/apps/roulette/tests/test_logic.py b/apps/roulette/tests/test_logic.py index f8faa2f5..89ecb1c6 100644 --- a/apps/roulette/tests/test_logic.py +++ b/apps/roulette/tests/test_logic.py @@ -6,30 +6,32 @@ def test_places_to_go_dict(): - assert len(logic.Game.places_to_go.keys()) == 12, 'Should be 12 keys in dict' + assert len(logic.Game.places_to_go.keys()) == 12, "Should be 12 keys in dict" assert sum(logic.Game.places_to_go.values()) == 0, "All key's values should be set to zero" def test_reset_places_to_go(): - logic.Game.places_to_go['Aruba'] = 10 - logic.Game.places_to_go['Barbados'] = 10 - assert logic.Game.places_to_go['Aruba'] == 10 and logic.Game.places_to_go['Barbados'] == 10, \ - '2 keys should have positive integer values' + logic.Game.places_to_go["Aruba"] = 10 + logic.Game.places_to_go["Barbados"] = 10 + assert ( + logic.Game.places_to_go["Aruba"] == 10 and logic.Game.places_to_go["Barbados"] == 10 + ), "2 keys should have positive integer values" for key in logic.Game.places_to_go: logic.Game.places_to_go[key] = 0 - assert logic.Game.places_to_go['Aruba'] == 0 and logic.Game.places_to_go['Barbados'] == 0, \ - 'The keys should now be back to zero' + assert ( + logic.Game.places_to_go["Aruba"] == 0 and logic.Game.places_to_go["Barbados"] == 0 + ), "The keys should now be back to zero" assert sum(logic.Game.places_to_go.values()) == 0, "All key's values should be set to zero" def test_get_game_result(mocker): - mocker.patch('time.sleep', return_value=None) + mocker.patch("time.sleep", return_value=None) logic.get_game_result() - assert len(logic.Game.places_to_go.keys()) == 12, 'Should still be 12 keys in dict' + assert len(logic.Game.places_to_go.keys()) == 12, "Should still be 12 keys in dict" assert sum(logic.Game.places_to_go.values()) == 1_000, "Sum of key's values should be 1,000" -@pytest.mark.parametrize(argnames='destinations', argvalues=sorted(destinations)) +@pytest.mark.parametrize(argnames="destinations", argvalues=sorted(destinations)) def test_get_picture_url(destinations): pic_url = logic.get_picture_url(destinations) - assert requests.get(pic_url).status_code == 200, 'Should return an `OK` status' + assert requests.get(pic_url).status_code == 200, "Should return an `OK` status" diff --git a/apps/roulette/tests/test_urls.py b/apps/roulette/tests/test_urls.py index b7b02312..54c495b9 100644 --- a/apps/roulette/tests/test_urls.py +++ b/apps/roulette/tests/test_urls.py @@ -2,16 +2,18 @@ def test_game_screen(): - """ Verify that the `game` url invokes intended view """ - resolver = resolve(reverse('roulette:game')) - assert resolver.view_name, 'game' + """Verify that the `game` url invokes intended view""" + resolver = resolve(reverse("roulette:game")) + assert resolver.view_name, "game" + def test_destination_screen(): - """ Verify that the `destination` url invokes intended view """ - resolver = resolve(reverse('roulette:destination')) - assert resolver.view_name, 'destination' + """Verify that the `destination` url invokes intended view""" + resolver = resolve(reverse("roulette:destination")) + assert resolver.view_name, "destination" + def test_results_screen(): - """ Verify that the `destination-log-file` url invokes intended view """ - resolver = resolve(reverse('roulette:log_file')) - assert resolver.view_name, 'log_file' + """Verify that the `destination-log-file` url invokes intended view""" + resolver = resolve(reverse("roulette:log_file")) + assert resolver.view_name, "log_file" diff --git a/apps/roulette/tests/test_views.py b/apps/roulette/tests/test_views.py index 531fbc27..b1087037 100644 --- a/apps/roulette/tests/test_views.py +++ b/apps/roulette/tests/test_views.py @@ -2,22 +2,22 @@ def test_game_screen(client): - """ Asserts a site visitor can access the `game` screen """ - path = reverse('roulette:game') + """Asserts a site visitor can access the `game` screen""" + path = reverse("roulette:game") response = client.get(path) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_destination_screen(client, mocker): - """ Asserts a site visitor can access the `destination` screen """ - mocker.patch('time.sleep', return_value=None) - path = reverse('roulette:destination') + """Asserts a site visitor can access the `destination` screen""" + mocker.patch("time.sleep", return_value=None) + path = reverse("roulette:destination") response = client.get(path) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_view_log_file_contents(client): - """ Asserts a site visitor can access the `log file contents` screen """ - path = reverse('roulette:log_file') + """Asserts a site visitor can access the `log file contents` screen""" + path = reverse("roulette:log_file") response = client.get(path) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" diff --git a/apps/roulette/urls.py b/apps/roulette/urls.py index 6636ecba..df3d2599 100644 --- a/apps/roulette/urls.py +++ b/apps/roulette/urls.py @@ -3,10 +3,10 @@ from apps.roulette.views import destination_screen, game_screen, view_log_file_contents -app_name = 'roulette' +app_name = "roulette" urlpatterns = [ - path('game/', game_screen, name='game'), - path('destination/', destination_screen, name='destination'), - path('destination-log-file/', view_log_file_contents, name='log_file'), + path("game/", game_screen, name="game"), + path("destination/", destination_screen, name="destination"), + path("destination-log-file/", view_log_file_contents, name="log_file"), ] diff --git a/apps/roulette/views.py b/apps/roulette/views.py index 7eec5614..6fb44405 100644 --- a/apps/roulette/views.py +++ b/apps/roulette/views.py @@ -5,7 +5,7 @@ def game_screen(request): logic.clear_down_log_file() - return render(request, 'roulette/game.html') + return render(request, "roulette/game.html") def destination_screen(request): @@ -14,18 +14,16 @@ def destination_screen(request): roulette_result = logic.get_game_result() destination_image_url = logic.get_picture_url(roulette_result[1]) context = { - 'places_to_go': roulette_result[0], - 'most_selected_place': roulette_result[1], - 'number_of_times_selected': roulette_result[2], - 'detailed_choices': roulette_result[3], - 'destination_image_url': destination_image_url, + "places_to_go": roulette_result[0], + "most_selected_place": roulette_result[1], + "number_of_times_selected": roulette_result[2], + "detailed_choices": roulette_result[3], + "destination_image_url": destination_image_url, } - return render(request, 'roulette/destination.html', context) + return render(request, "roulette/destination.html", context) def view_log_file_contents(request): - context = { - 'log_file_contents': logic.read_log_file() - } - return render(request, 'roulette/log_file_contents.html', context) + context = {"log_file_contents": logic.read_log_file()} + return render(request, "roulette/log_file_contents.html", context) diff --git a/apps/scraping/apps.py b/apps/scraping/apps.py index f1f219fe..f57c9423 100644 --- a/apps/scraping/apps.py +++ b/apps/scraping/apps.py @@ -2,4 +2,4 @@ class ScrapingConfig(AppConfig): - name = 'apps.scraping' + name = "apps.scraping" diff --git a/apps/scraping/churchill.py b/apps/scraping/churchill.py index 0a3cda90..f6e9034e 100644 --- a/apps/scraping/churchill.py +++ b/apps/scraping/churchill.py @@ -15,9 +15,10 @@ def get_churchill_speech(request): URL = "https://www.goodreads.com/quotes/55276-i-have-nothing-to-offer-but-blood-toil-tears-and" page_response = requests.get(URL, timeout=5) page_content = BeautifulSoup(page_response.content, "html.parser") - churchill_speech = str(page_content.findChildren('h1')).split('\n')\ - [1].split(';
')[0].strip() + '."' + churchill_speech = ( + str(page_content.findChildren("h1")).split("\n")[1].split(";
")[0].strip() + '."' + ) - context = {'churchill_speech': churchill_speech} + context = {"churchill_speech": churchill_speech} - return render(request, 'churchill_speech.html', context) + return render(request, "churchill_speech.html", context) diff --git a/apps/scraping/gettysburg.py b/apps/scraping/gettysburg.py index b8dc93f0..55854965 100644 --- a/apps/scraping/gettysburg.py +++ b/apps/scraping/gettysburg.py @@ -17,11 +17,11 @@ def get_gettysburg_speech(request): page_content = BeautifulSoup(page_response.content, "html.parser") text_content, split_string = [], [] for num in range(2): - paragraphs = page_content.find_all('div', attrs={"class": "quoteText"})[num].text + paragraphs = page_content.find_all("div", attrs={"class": "quoteText"})[num].text text_content.append(paragraphs) - split_string = text_content[1].strip('\n').strip().split('\n') + split_string = text_content[1].strip("\n").strip().split("\n") gettysburg_speech = split_string[0] - context = {'gettysburg': gettysburg_speech} + context = {"gettysburg": gettysburg_speech} - return render(request, 'gettysburg_speech.html', context) + return render(request, "gettysburg_speech.html", context) diff --git a/apps/scraping/referendum.py b/apps/scraping/referendum.py index 758589f9..f119cac8 100644 --- a/apps/scraping/referendum.py +++ b/apps/scraping/referendum.py @@ -18,15 +18,15 @@ class ScrapingError(Exception): def get_area_results(results: dict) -> List[Tuple[Any]]: - """ Assembles each area's results into an iterable for rendering """ + """Assembles each area's results into an iterable for rendering""" area_results = zip( - results['area_name'], - results['leave_votes'], - results['leave_percent'], - results['remain_votes'], - results['remain_percent'], - results['area_votes'], - results['turnout'], + results["area_name"], + results["leave_votes"], + results["leave_percent"], + results["remain_votes"], + results["remain_percent"], + results["area_votes"], + results["turnout"], ) results = [] for area in area_results: @@ -36,32 +36,42 @@ def get_area_results(results: dict) -> List[Tuple[Any]]: def scrape_content() -> List[List[int]]: - """ Scrapes the results of the EU Referendum from the BBC website """ + """Scrapes the results of the EU Referendum from the BBC website""" ALPHABET = string.ascii_lowercase - BASE_URL = 'https://www.bbc.co.uk/news/politics/eu_referendum/results/local/' + BASE_URL = "https://www.bbc.co.uk/news/politics/eu_referendum/results/local/" results = defaultdict(list) for letter in ALPHABET: page_response = requests.get(f"{BASE_URL}{letter}", timeout=5) try: page_content = BeautifulSoup(page_response.content, "html.parser") - areas = page_content.find_all('div', attrs={'class': 'eu-ref-result-bar'}) + areas = page_content.find_all("div", attrs={"class": "eu-ref-result-bar"}) for area in areas: - results['area_name'].append(area.find('h3').getText()) - cleaned_leave_votes = int(area.find_all( - 'div', {'class': 'eu-ref-result-bar__votes'}) - [0].string.strip().split('\n')[0].strip().replace(',', '')) - results['leave_votes'].append(cleaned_leave_votes) - cleaned_remain_votes = int(area.find_all( - 'div', {'class': 'eu-ref-result-bar__votes'}) - [1].string.strip().split('\n')[0].strip().replace(',', '')) - results['remain_votes'].append(cleaned_remain_votes) + results["area_name"].append(area.find("h3").getText()) + cleaned_leave_votes = int( + area.find_all("div", {"class": "eu-ref-result-bar__votes"})[0] + .string.strip() + .split("\n")[0] + .strip() + .replace(",", "") + ) + results["leave_votes"].append(cleaned_leave_votes) + cleaned_remain_votes = int( + area.find_all("div", {"class": "eu-ref-result-bar__votes"})[1] + .string.strip() + .split("\n")[0] + .strip() + .replace(",", "") + ) + results["remain_votes"].append(cleaned_remain_votes) area_votes = cleaned_leave_votes + cleaned_remain_votes - results['area_votes'].append(area_votes) - results['leave_percent'].append(f"{cleaned_leave_votes / area_votes:.1%}") - results['remain_percent'].append(f"{cleaned_remain_votes / area_votes:.1%}") - results['turnout'].append( - area.find('div', {'class': 'eu-ref-result-bar__turnout'}) - .getText().replace('Turnout: ', '')) + results["area_votes"].append(area_votes) + results["leave_percent"].append(f"{cleaned_leave_votes / area_votes:.1%}") + results["remain_percent"].append(f"{cleaned_remain_votes / area_votes:.1%}") + results["turnout"].append( + area.find("div", {"class": "eu-ref-result-bar__turnout"}) + .getText() + .replace("Turnout: ", "") + ) except: # pragma: no cover raise ScrapingError @@ -69,17 +79,17 @@ def scrape_content() -> List[List[int]]: def calc_leave_votes(results: list) -> int: - """ Calculates total number of leave votes from scraped data """ + """Calculates total number of leave votes from scraped data""" return sum(result[1] for result in results) def calc_remain_votes(results: list) -> int: - """ Calculates total number of remain votes from scraped data """ + """Calculates total number of remain votes from scraped data""" return sum(result[3] for result in results) def get_referendum_results(request): - """ Retrieves 2016 Brexit Referendum results from BBC website """ + """Retrieves 2016 Brexit Referendum results from BBC website""" results = scrape_content() results = get_area_results(dict(results)) leave_votes = calc_leave_votes(results) @@ -89,12 +99,12 @@ def get_referendum_results(request): total_remain_percent = f"{remain_votes / total_votes:.1%}" context = { - 'results': results, - 'leave_votes': leave_votes, - 'remain_votes': remain_votes, - 'total_votes': total_votes, - 'total_leave_percent': total_leave_percent, - 'total_remain_percent': total_remain_percent, + "results": results, + "leave_votes": leave_votes, + "remain_votes": remain_votes, + "total_votes": total_votes, + "total_leave_percent": total_leave_percent, + "total_remain_percent": total_remain_percent, } - return render(request, 'referendum.html', {'context': context}) + return render(request, "referendum.html", {"context": context}) diff --git a/apps/scraping/tests/test_churchill.py b/apps/scraping/tests/test_churchill.py index 948ba8cb..9e7cac96 100644 --- a/apps/scraping/tests/test_churchill.py +++ b/apps/scraping/tests/test_churchill.py @@ -5,7 +5,7 @@ @pytest.mark.vcr() def test_get_churchill_speech_view(client): - """ Asserts a site visitor can GET the `Churchill Speech` screen """ - path = reverse('scraping:churchill_speech') + """Asserts a site visitor can GET the `Churchill Speech` screen""" + path = reverse("scraping:churchill_speech") response = client.get(path) - assert response.status_code == 200, 'Should return with an `OK` status code' + assert response.status_code == 200, "Should return with an `OK` status code" diff --git a/apps/scraping/tests/test_gettysburg.py b/apps/scraping/tests/test_gettysburg.py index adb65bc5..bedb36ba 100644 --- a/apps/scraping/tests/test_gettysburg.py +++ b/apps/scraping/tests/test_gettysburg.py @@ -5,7 +5,7 @@ @pytest.mark.vcr() def test_get_gettysburg_speech_view(client): - """ Asserts a site visitor can GET the `Gettysburg Speech` screen """ - path = reverse('scraping:gettysburg_speech') + """Asserts a site visitor can GET the `Gettysburg Speech` screen""" + path = reverse("scraping:gettysburg_speech") response = client.get(path) - assert response.status_code == 200, 'Should return with an `OK` status code' + assert response.status_code == 200, "Should return with an `OK` status code" diff --git a/apps/scraping/tests/test_referendum.py b/apps/scraping/tests/test_referendum.py index e8206af1..9fe779c1 100644 --- a/apps/scraping/tests/test_referendum.py +++ b/apps/scraping/tests/test_referendum.py @@ -3,10 +3,10 @@ import pytest -@pytest.mark.slow(reason='Uses a large cassette storage to test the EU Referendum Results') +@pytest.mark.slow(reason="Uses a large cassette storage to test the EU Referendum Results") @pytest.mark.vcr() def test_get_referendum_results(client): - """ Asserts a site visitor can GET the `Referendum Results` screen """ - path = reverse('scraping:referendum') + """Asserts a site visitor can GET the `Referendum Results` screen""" + path = reverse("scraping:referendum") response = client.get(path) - assert response.status_code == 200, 'Should return with an `OK` status code' + assert response.status_code == 200, "Should return with an `OK` status code" diff --git a/apps/scraping/tests/test_urls.py b/apps/scraping/tests/test_urls.py index c649acc4..5b07146d 100644 --- a/apps/scraping/tests/test_urls.py +++ b/apps/scraping/tests/test_urls.py @@ -2,21 +2,24 @@ def test_scraping_options_screen(): - """ Verify that the `scraping-options` url invokes intended view """ - resolver = resolve(reverse('scraping:scraping_options')) - assert resolver.view_name, 'scraping_options' + """Verify that the `scraping-options` url invokes intended view""" + resolver = resolve(reverse("scraping:scraping_options")) + assert resolver.view_name, "scraping_options" + def test_churchill_speech_screen(): - """ Verify that the `churchill-speech` url invokes intended view """ - resolver = resolve(reverse('scraping:churchill_speech')) - assert resolver.view_name, 'churchill_speech' + """Verify that the `churchill-speech` url invokes intended view""" + resolver = resolve(reverse("scraping:churchill_speech")) + assert resolver.view_name, "churchill_speech" + def test_gettysburg_speech_screen(): - """ Verify that the `gettysburg-speech` url invokes intended view """ - resolver = resolve(reverse('scraping:gettysburg_speech')) - assert resolver.view_name, 'gettysburg_speech' + """Verify that the `gettysburg-speech` url invokes intended view""" + resolver = resolve(reverse("scraping:gettysburg_speech")) + assert resolver.view_name, "gettysburg_speech" + def test_eu_referendum_results_screen(): - """ Verify that the `eu-referendum-results` url invokes intended view """ - resolver = resolve(reverse('scraping:referendum')) - assert resolver.view_name, 'referendum' + """Verify that the `eu-referendum-results` url invokes intended view""" + resolver = resolve(reverse("scraping:referendum")) + assert resolver.view_name, "referendum" diff --git a/apps/scraping/tests/test_views.py b/apps/scraping/tests/test_views.py index 4f5397d5..0f9c0d52 100644 --- a/apps/scraping/tests/test_views.py +++ b/apps/scraping/tests/test_views.py @@ -5,8 +5,8 @@ class TestScrapingOptionsView: def test_scraping_options_view(self, rf): - """ Asserts any user can access `scraping options` page """ - path = reverse('scraping:scraping_options') + """Asserts any user can access `scraping options` page""" + path = reverse("scraping:scraping_options") request = rf.get(path) response = ScrapingOptionsView.as_view()(request) - assert response.status_code == 200, 'Should be callable by logged in/out user' + assert response.status_code == 200, "Should be callable by logged in/out user" diff --git a/apps/scraping/urls.py b/apps/scraping/urls.py index 7c6a48f0..8f87a2ff 100644 --- a/apps/scraping/urls.py +++ b/apps/scraping/urls.py @@ -6,11 +6,11 @@ from apps.scraping.views import ScrapingOptionsView -app_name = 'scraping' +app_name = "scraping" urlpatterns = [ - path('scraping-options/', ScrapingOptionsView.as_view(), name='scraping_options'), - path('churchill-speech/', get_churchill_speech, name='churchill_speech'), - path('gettysburg-speech/', get_gettysburg_speech, name='gettysburg_speech'), - path('eu-referendum-results/', get_referendum_results, name='referendum'), + path("scraping-options/", ScrapingOptionsView.as_view(), name="scraping_options"), + path("churchill-speech/", get_churchill_speech, name="churchill_speech"), + path("gettysburg-speech/", get_gettysburg_speech, name="gettysburg_speech"), + path("eu-referendum-results/", get_referendum_results, name="referendum"), ] diff --git a/apps/scraping/views.py b/apps/scraping/views.py index 17c5d0c3..f24c19f2 100644 --- a/apps/scraping/views.py +++ b/apps/scraping/views.py @@ -2,4 +2,4 @@ class ScrapingOptionsView(TemplateView): - template_name = 'scraping_options.html' + template_name = "scraping_options.html" diff --git a/apps/text_analysis/apps.py b/apps/text_analysis/apps.py index c66fec22..7292e6b9 100644 --- a/apps/text_analysis/apps.py +++ b/apps/text_analysis/apps.py @@ -2,4 +2,4 @@ class TextAnalysisConfig(AppConfig): - name = 'apps.text_analysis' + name = "apps.text_analysis" diff --git a/apps/text_analysis/tests/conftest.py b/apps/text_analysis/tests/conftest.py index d420f0d4..91b77858 100644 --- a/apps/text_analysis/tests/conftest.py +++ b/apps/text_analysis/tests/conftest.py @@ -1,12 +1,12 @@ import pytest -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def text_to_analyse(): - s = 'lorem ipsum ' * 50 + s = "lorem ipsum " * 50 return s.strip() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def dirty_text(): - return 'This is %;some di!ty to @na#.]se' + return "This is %;some di!ty to @na#.]se" diff --git a/apps/text_analysis/tests/test_urls.py b/apps/text_analysis/tests/test_urls.py index e888ee0c..3bfab8ca 100644 --- a/apps/text_analysis/tests/test_urls.py +++ b/apps/text_analysis/tests/test_urls.py @@ -2,11 +2,12 @@ def test_analyse_screen(): - """ Verify that the `analyse` url invokes intended view """ - resolver = resolve(reverse('text_analysis:analyse')) - assert resolver.view_name, 'analyse' + """Verify that the `analyse` url invokes intended view""" + resolver = resolve(reverse("text_analysis:analyse")) + assert resolver.view_name, "analyse" + def test_analysis_screen(): - """ Verify that the `analysis` url invokes intended view """ - resolver = resolve(reverse('text_analysis:analysis')) - assert resolver.view_name, 'analysis' + """Verify that the `analysis` url invokes intended view""" + resolver = resolve(reverse("text_analysis:analysis")) + assert resolver.view_name, "analysis" diff --git a/apps/text_analysis/tests/test_utils.py b/apps/text_analysis/tests/test_utils.py index 0db48d54..4f449d74 100644 --- a/apps/text_analysis/tests/test_utils.py +++ b/apps/text_analysis/tests/test_utils.py @@ -8,20 +8,21 @@ def test_get_word_list(text_to_analyse): word_list = utils.get_word_list(text_to_analyse) - assert isinstance(word_list, list), 'Should be a list' - assert len(word_list) == 100, 'Should be 100 words in length' + assert isinstance(word_list, list), "Should be a list" + assert len(word_list) == 100, "Should be 100 words in length" def test_get_sorted_words(text_to_analyse): words_to_sort_list = text_to_analyse.split() sorted_words = utils.get_sorted_words(words_to_sort_list) - assert isinstance(sorted_words, List), 'Should be a list' - assert isinstance(sorted_words[0], Tuple), 'Elements within the list should be tuples' - assert len(sorted_words) == 2, 'Should return 2 words. 1 for `lorem` and 1 for `ipsum`' + assert isinstance(sorted_words, List), "Should be a list" + assert isinstance(sorted_words[0], Tuple), "Elements within the list should be tuples" + assert len(sorted_words) == 2, "Should return 2 words. 1 for `lorem` and 1 for `ipsum`" def test_get_letter_counts(text_to_analyse): letter_counts = utils.get_letter_counts(text_to_analyse) - assert isinstance(letter_counts, list), 'Should be a list' - assert len(letter_counts) == 26, \ - 'Should be 26 items in the list - 1 for each letter of the alphabet' + assert isinstance(letter_counts, list), "Should be a list" + assert ( + len(letter_counts) == 26 + ), "Should be 26 items in the list - 1 for each letter of the alphabet" diff --git a/apps/text_analysis/tests/test_views.py b/apps/text_analysis/tests/test_views.py index bf826f72..36e25cc4 100644 --- a/apps/text_analysis/tests/test_views.py +++ b/apps/text_analysis/tests/test_views.py @@ -5,19 +5,21 @@ def test_analyse_screen(client): - """ Asserts a site visitor can GET the `analyse` screen """ - path = reverse('text_analysis:analyse') + """Asserts a site visitor can GET the `analyse` screen""" + path = reverse("text_analysis:analyse") response = client.get(path) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" + def test_analysis_screen_with_clean_text(client, text_to_analyse): - """ Asserts a site visitor can GET the `analysis` screen """ - path = reverse('text_analysis:analysis') - response = client.get(path, {'fulltext': text_to_analyse}) - assert response.status_code == 200, 'Should return an `OK` status code' + """Asserts a site visitor can GET the `analysis` screen""" + path = reverse("text_analysis:analysis") + response = client.get(path, {"fulltext": text_to_analyse}) + assert response.status_code == 200, "Should return an `OK` status code" + def test_analysis_screen_with_dirty_text(client, dirty_text): - """ Asserts a site visitor can GET the `analysis` screen """ - path = reverse('text_analysis:analysis') - response = client.get(path, {'fulltext': dirty_text}) - assert response.status_code == 200, 'Should return an `OK` status code' + """Asserts a site visitor can GET the `analysis` screen""" + path = reverse("text_analysis:analysis") + response = client.get(path, {"fulltext": dirty_text}) + assert response.status_code == 200, "Should return an `OK` status code" diff --git a/apps/text_analysis/urls.py b/apps/text_analysis/urls.py index 32249d59..9996e06d 100644 --- a/apps/text_analysis/urls.py +++ b/apps/text_analysis/urls.py @@ -3,9 +3,9 @@ from apps.text_analysis.views import analyse_screen, analysis_screen -app_name = 'text_analysis' +app_name = "text_analysis" urlpatterns = [ - path('analyse/', analyse_screen, name='analyse'), - path('analysis/', analysis_screen, name='analysis'), + path("analyse/", analyse_screen, name="analyse"), + path("analysis/", analysis_screen, name="analysis"), ] diff --git a/apps/text_analysis/utils.py b/apps/text_analysis/utils.py index c0718009..df47fc0e 100644 --- a/apps/text_analysis/utils.py +++ b/apps/text_analysis/utils.py @@ -11,11 +11,11 @@ def get_orig_full_text(request) -> str: - return request.GET['fulltext'].strip() + return request.GET["fulltext"].strip() def get_cleaned_full_text(request) -> str: - return re.sub('[!@#.,/\';]', '', get_orig_full_text(request).lower()) + return re.sub("[!@#.,/';]", "", get_orig_full_text(request).lower()) def get_word_list(cleaned_full_text: str) -> List[str]: @@ -41,6 +41,6 @@ def get_letter_counts(cleaned_full_text: str) -> List[str]: char_count = cleaned_full_text.count(letter) perc = char_count / total_num_chars * 100 letter_count_str = f"{letter}: {char_count} ({round(perc, 1)}%)" - letter_counts.append(letter_count_str.strip('\n')) + letter_counts.append(letter_count_str.strip("\n")) return letter_counts diff --git a/apps/text_analysis/views.py b/apps/text_analysis/views.py index de883d73..03208477 100644 --- a/apps/text_analysis/views.py +++ b/apps/text_analysis/views.py @@ -4,7 +4,7 @@ def analyse_screen(request): - return render(request, 'text_analysis/analyse.html') + return render(request, "text_analysis/analyse.html") def analysis_screen(request): @@ -12,11 +12,11 @@ def analysis_screen(request): word_list = utils.get_word_list(cleaned_full_text) context = { - 'orig_full_text': utils.get_orig_full_text(request), - 'cleaned_full_text': cleaned_full_text, - 'word_count': len(word_list), - 'sorted_words': utils.get_sorted_words(word_list), - 'letter_count': utils.get_letter_counts(cleaned_full_text), + "orig_full_text": utils.get_orig_full_text(request), + "cleaned_full_text": cleaned_full_text, + "word_count": len(word_list), + "sorted_words": utils.get_sorted_words(word_list), + "letter_count": utils.get_letter_counts(cleaned_full_text), } - return render(request, 'text_analysis/analysis.html', context) + return render(request, "text_analysis/analysis.html", context) diff --git a/apps/users/admin.py b/apps/users/admin.py index 5856a421..67a15210 100644 --- a/apps/users/admin.py +++ b/apps/users/admin.py @@ -10,8 +10,8 @@ class ProfileAdmin(admin.ModelAdmin): - list_display = ('user', 'author_view', 'created_date', 'updated_date') - readonly_fields = ('profile_picture_image', 'created_date', 'updated_date') + list_display = ("user", "author_view", "created_date", "updated_date") + readonly_fields = ("profile_picture_image", "created_date", "updated_date") SCALE_FACTOR = 0.3 def profile_picture_image(self, obj): @@ -19,23 +19,22 @@ def profile_picture_image(self, obj): img_width = obj.profile_picture.width * self.SCALE_FACTOR img_height = obj.profile_picture.height * self.SCALE_FACTOR url = obj.profile_picture.url - return format_html( - f"") + return format_html(f"") class EmailTokenAdmin(admin.ModelAdmin): list_display = ( - 'challenge_email_address', - 'challenge_generation_timestamp', - 'challenge_expiration_timestamp', - 'challenge_completed', - 'token_expiration_timestamp', - 'user_id', + "challenge_email_address", + "challenge_generation_timestamp", + "challenge_expiration_timestamp", + "challenge_completed", + "token_expiration_timestamp", + "user_id", ) - exclude = ('challenge_token', ) + exclude = ("challenge_token",) def get_readonly_fields(self, request, obj=None): - return [field.name for field in obj._meta.fields if field.name != 'challenge_token'] + return [field.name for field in obj._meta.fields if field.name != "challenge_token"] admin.site.register(Profile, ProfileAdmin) diff --git a/apps/users/apps.py b/apps/users/apps.py index 7e3439a2..f1c9ed30 100644 --- a/apps/users/apps.py +++ b/apps/users/apps.py @@ -2,7 +2,7 @@ class UsersConfig(AppConfig): - name = 'apps.users' + name = "apps.users" def ready(self): import apps.users.signals diff --git a/apps/users/forms.py b/apps/users/forms.py index 1d6ae955..ecf69758 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -15,16 +15,16 @@ class UserRegisterForm(UserCreationForm): class Meta: model = get_user_model() - fields = ['username', 'email', 'first_name', 'last_name', 'password1', 'password2'] + fields = ["username", "email", "first_name", "last_name", "password1", "password2"] def clean_username(self): - return self.cleaned_data['username'].casefold() + return self.cleaned_data["username"].casefold() def clean_first_name(self): - return self.cleaned_data['first_name'].title() + return self.cleaned_data["first_name"].title() def clean_last_name(self): - return self.cleaned_data['last_name'].title() + return self.cleaned_data["last_name"].title() class UserUpdateForm(forms.ModelForm): @@ -32,29 +32,31 @@ class UserUpdateForm(forms.ModelForm): class Meta: model = get_user_model() - fields = ['username', 'email', 'first_name', 'last_name'] + fields = ["username", "email", "first_name", "last_name"] def clean_username(self): - return self.cleaned_data['username'].casefold().strip() + return self.cleaned_data["username"].casefold().strip() def clean_email(self): - return self.cleaned_data['email'].casefold().strip() + return self.cleaned_data["email"].casefold().strip() def clean_first_name(self): - return self.cleaned_data['first_name'].title().strip() + return self.cleaned_data["first_name"].title().strip() def clean_last_name(self): - return self.cleaned_data['last_name'].title().strip() + return self.cleaned_data["last_name"].title().strip() class ProfileUpdateForm(forms.ModelForm): class Meta: model = Profile - fields = ['profile_picture', 'author_view'] + fields = ["profile_picture", "author_view"] widgets = { - 'author_view': forms.RadioSelect(attrs={ - 'class': 'form-check-inline', - }), + "author_view": forms.RadioSelect( + attrs={ + "class": "form-check-inline", + } + ), } def __init__(self, *args, **kwargs): @@ -62,7 +64,7 @@ def __init__(self, *args, **kwargs): class UserTOTPDeviceForm(TOTPDeviceForm): - """ Used during the setup of the QR Code """ + """Used during the setup of the QR Code""" def __init__(self, key, user, metadata=None, **kwargs): super().__init__(self, key, user, **kwargs) @@ -76,7 +78,7 @@ def __init__(self, key, user, metadata=None, **kwargs): self.metadata = metadata or {} self.helper = FormHelper() - self.fields['token'].label = False + self.fields["token"].label = False extra_attrs = { "class": "login100-form validate-form textinput textInput form-control", "title": "", @@ -86,7 +88,7 @@ def __init__(self, key, user, metadata=None, **kwargs): class EmailTokenSubmissionForm(forms.Form): - """ Used during the setup or submission of an email token """ + """Used during the setup or submission of an email token""" MIN_TOKEN_VALUE = 0 MAX_TOKEN_VALUE = 999_999 @@ -97,15 +99,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() - self.helper.form_id = 'email-token-submission' - self.fields['token'].label = False - self.fields['token'].widget.attrs={ - 'autofocus': 'autofocus', - 'inputmode': 'numeric', - 'autocomplete': 'one-time-code', - 'class': 'form-control', - 'title': '', - 'placeholder': 'Enter token from email...', - 'min': self.MIN_TOKEN_VALUE, - 'max': self.MAX_TOKEN_VALUE, + self.helper.form_id = "email-token-submission" + self.fields["token"].label = False + self.fields["token"].widget.attrs = { + "autofocus": "autofocus", + "inputmode": "numeric", + "autocomplete": "one-time-code", + "class": "form-control", + "title": "", + "placeholder": "Enter token from email...", + "min": self.MIN_TOKEN_VALUE, + "max": self.MAX_TOKEN_VALUE, } diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py index 2f3f1f65..7783a65c 100644 --- a/apps/users/migrations/0001_initial.py +++ b/apps/users/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,15 +14,35 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(max_length=255, unique=True)), - ('author_view', models.IntegerField(choices=[(0, 'Username'), (1, 'Full Name')], default=0)), - ('profile_picture', models.ImageField(default='default-user.jpg', max_length=200, upload_to='profile_pics')), - ('created_date', models.DateTimeField(auto_now_add=True)), - ('updated_date', models.DateTimeField(auto_now=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "author_view", + models.IntegerField(choices=[(0, "Username"), (1, "Full Name")], default=0), + ), + ( + "profile_picture", + models.ImageField( + default="default-user.jpg", max_length=200, upload_to="profile_pics" + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="user", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/apps/users/migrations/0002_auto_20200929_1338.py b/apps/users/migrations/0002_auto_20200929_1338.py index 1c0c0b89..67976d42 100644 --- a/apps/users/migrations/0002_auto_20200929_1338.py +++ b/apps/users/migrations/0002_auto_20200929_1338.py @@ -6,20 +6,25 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('users', '0001_initial'), + ("users", "0001_initial"), ] operations = [ migrations.RemoveField( - model_name='profile', - name='id', + model_name="profile", + name="id", ), migrations.AlterField( - model_name='profile', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='user', serialize=False, to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="user", + serialize=False, + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/apps/users/migrations/0003_auto_20201001_2136.py b/apps/users/migrations/0003_auto_20201001_2136.py index 59898e4d..96181622 100644 --- a/apps/users/migrations/0003_auto_20201001_2136.py +++ b/apps/users/migrations/0003_auto_20201001_2136.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('users', '0002_auto_20200929_1338'), + ("users", "0002_auto_20200929_1338"), ] operations = [ migrations.AlterField( - model_name='profile', - name='profile_picture', - field=models.ImageField(default='profile_pics/default-user.jpg', max_length=200, upload_to='profile_pics'), + model_name="profile", + name="profile_picture", + field=models.ImageField( + default="profile_pics/default-user.jpg", max_length=200, upload_to="profile_pics" + ), ), ] diff --git a/apps/users/migrations/0004_generate_email_token_model.py b/apps/users/migrations/0004_generate_email_token_model.py index d9df1be7..970184ec 100644 --- a/apps/users/migrations/0004_generate_email_token_model.py +++ b/apps/users/migrations/0004_generate_email_token_model.py @@ -9,30 +9,63 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('users', '0003_auto_20201001_2136'), + ("users", "0003_auto_20201001_2136"), ] operations = [ migrations.AlterField( - model_name='profile', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='profile', serialize=False, to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="profile", + serialize=False, + to=settings.AUTH_USER_MODEL, + ), ), migrations.CreateModel( - name='EmailToken', + name="EmailToken", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('challenge_email_address', models.EmailField(max_length=254)), - ('challenge_token', encrypted_model_fields.fields.EncryptedCharField(default=django_otp.util.random_hex, validators=[apps.users.utils.token_validator])), - ('challenge_generation_timestamp', models.DateTimeField(auto_now_add=True, null=True)), - ('challenge_expiration_timestamp', models.DateTimeField(blank=True, default=apps.users.utils.get_challenge_expiration_timestamp, null=True)), - ('challenge_completed', models.BooleanField(default=False)), - ('challenge_completed_timestamp', models.DateTimeField(blank=True, null=True)), - ('token_expiration_timestamp', models.DateTimeField(blank=True, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_email_tokens', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("challenge_email_address", models.EmailField(max_length=254)), + ( + "challenge_token", + encrypted_model_fields.fields.EncryptedCharField( + default=django_otp.util.random_hex, + validators=[apps.users.utils.token_validator], + ), + ), + ( + "challenge_generation_timestamp", + models.DateTimeField(auto_now_add=True, null=True), + ), + ( + "challenge_expiration_timestamp", + models.DateTimeField( + blank=True, + default=apps.users.utils.get_challenge_expiration_timestamp, + null=True, + ), + ), + ("challenge_completed", models.BooleanField(default=False)), + ("challenge_completed_timestamp", models.DateTimeField(blank=True, null=True)), + ("token_expiration_timestamp", models.DateTimeField(blank=True, null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_email_tokens", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/apps/users/mixins.py b/apps/users/mixins.py index 6ffe6513..c2bc1a36 100644 --- a/apps/users/mixins.py +++ b/apps/users/mixins.py @@ -6,7 +6,6 @@ class DeviceAuthUserMixin(LoginRequiredMixin, UserPassesTestMixin): - def test_func(self) -> bool: return self.request.user.profile.is_two_factor_auth_by_token @@ -15,7 +14,6 @@ def handle_no_permission(self): class EmailAuthUserMixin(LoginRequiredMixin, UserPassesTestMixin): - def test_func(self) -> bool: return self.request.user.profile.is_two_factor_auth_by_email @@ -24,14 +22,13 @@ def handle_no_permission(self): class TwoFactorAuthUserMixin(LoginRequiredMixin, UserPassesTestMixin): - def test_func(self) -> bool: return self.request.user.profile.is_two_factor_authenticated def handle_no_permission(self): html_msg = ( - "You have not verified by either device token or email.

" + - "Please follow the two-factor authentication process." + "You have not verified by either device token or email.

" + + "Please follow the two-factor authentication process." ) messages.add_message(self.request, messages.INFO, mark_safe(html_msg)) return redirect(settings.LOGIN_URL) diff --git a/apps/users/models.py b/apps/users/models.py index 9cd858c8..72480064 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -15,7 +15,6 @@ class Profile(models.Model): - class AuthorView(models.IntegerChoices): USERNAME = 0 FULL_NAME = 1 @@ -23,13 +22,15 @@ class AuthorView(models.IntegerChoices): slug = models.SlugField(max_length=255, unique=True) author_view = models.IntegerField(choices=AuthorView.choices, default=0) profile_picture = models.ImageField( - default='profile_pics/default-user.jpg', upload_to='profile_pics', max_length=200) + default="profile_pics/default-user.jpg", upload_to="profile_pics", max_length=200 + ) created_date = models.DateTimeField(auto_now_add=True, editable=False) updated_date = models.DateTimeField(auto_now=True) # Relationship Fields user = models.OneToOneField( - get_user_model(), primary_key=True, related_name='profile', on_delete=models.CASCADE) + get_user_model(), primary_key=True, related_name="profile", on_delete=models.CASCADE + ) def __str__(self): return f"{self.user.get_full_name()} ({self.user.get_username()})" @@ -51,7 +52,7 @@ def display_name(self): return self.user.get_full_name() def get_absolute_url(self): - return reverse('blog:users:profile', kwargs={'username': self.slug}) + return reverse("blog:users:profile", kwargs={"username": self.slug}) def save(self, *args, **kwargs): if not self.slug.strip(): @@ -60,14 +61,14 @@ def save(self, *args, **kwargs): @property def is_two_factor_auth_by_token(self) -> bool: - """" Returns whether user is authenticated by token """ + """ " Returns whether user is authenticated by token""" return self.user.totpdevice_set.exists() @property def is_two_factor_auth_by_email(self) -> bool: - """" Returns whether user is authenticated by email """ + """ " Returns whether user is authenticated by email""" try: - user_email_token = self.user.user_email_tokens.latest('id') + user_email_token = self.user.user_email_tokens.latest("id") except ObjectDoesNotExist: return False else: @@ -77,26 +78,29 @@ def is_two_factor_auth_by_email(self) -> bool: @property def is_two_factor_authenticated(self) -> bool: - """" Returns whether user is authenticated by either token or email """ + """ " Returns whether user is authenticated by either token or email""" return bool(self.is_two_factor_auth_by_token or self.is_two_factor_auth_by_email) class EmailToken(models.Model): - challenge_email_address = models.EmailField() challenge_token = EncryptedCharField( - max_length=255, default=random_hex, validators=[token_validator]) + max_length=255, default=random_hex, validators=[token_validator] + ) challenge_generation_timestamp = models.DateTimeField( - null=True, blank=True, auto_now_add=True, editable=False) + null=True, blank=True, auto_now_add=True, editable=False + ) challenge_expiration_timestamp = models.DateTimeField( - null=True, blank=True, default=get_challenge_expiration_timestamp) + null=True, blank=True, default=get_challenge_expiration_timestamp + ) challenge_completed = models.BooleanField(default=False) challenge_completed_timestamp = models.DateTimeField(null=True, blank=True) token_expiration_timestamp = models.DateTimeField(null=True, blank=True) # Relationship Fields user = models.ForeignKey( - get_user_model(), related_name='user_email_tokens', on_delete=models.CASCADE) + get_user_model(), related_name="user_email_tokens", on_delete=models.CASCADE + ) def __repr__(self): return f"" @@ -108,13 +112,14 @@ def save(self, *args, **kwargs): if not self._state.adding and self.challenge_completed: self.challenge_completed_timestamp = timezone.now() self.token_expiration_timestamp = timezone.now() + timedelta( - seconds=settings.EMAIL_TOKEN_EXPIRATION_IN_SECS) + seconds=settings.EMAIL_TOKEN_EXPIRATION_IN_SECS + ) self.challenge_expiration_timestamp = self.token_expiration_timestamp return super().save(*args, **kwargs) @property def is_challenge_within_expiry(self) -> bool: - """" + """ " Returns whether email token is within its challenge expiration date """ if not self.challenge_expiration_timestamp: @@ -123,7 +128,7 @@ def is_challenge_within_expiry(self) -> bool: @property def is_token_within_expiry(self) -> bool: - """" + """ " Returns whether email token is within its token expiration date """ if not self.token_expiration_timestamp: diff --git a/apps/users/tests/test_forms.py b/apps/users/tests/test_forms.py index c2140f06..7a261592 100644 --- a/apps/users/tests/test_forms.py +++ b/apps/users/tests/test_forms.py @@ -1,47 +1,51 @@ import pytest -from apps.users.forms import (EmailTokenSubmissionForm, ProfileUpdateForm, - UserRegisterForm, UserTOTPDeviceForm, UserUpdateForm,) +from apps.users.forms import ( + EmailTokenSubmissionForm, + ProfileUpdateForm, + UserRegisterForm, + UserTOTPDeviceForm, + UserUpdateForm, +) pytestmark = pytest.mark.django_db(reset_sequences=True) class TestUserRegisterForm: - - fields = ('username', 'email', 'first_name', 'last_name', 'password1', 'password2', 'validity') + fields = ("username", "email", "first_name", "last_name", "password1", "password2", "validity") good_data = { - 'username': 'wayne-lambert', - 'email': 'test-email@example.com', - 'first_name': 'Wayne', - 'last_name': 'Lambert', - 'password1': 's@mple-p@$$w0rd', - 'password2': 's@mple-p@$$w0rd', - 'validity': True, + "username": "wayne-lambert", + "email": "test-email@example.com", + "first_name": "Wayne", + "last_name": "Lambert", + "password1": "s@mple-p@$$w0rd", + "password2": "s@mple-p@$$w0rd", + "validity": True, } dirty_data = { - 'username': 'Wayne-lambert ', # Notice trailing spaces - 'email': 'test-email@example.com', - 'first_name': 'Wayne', - 'last_name': 'Lambert', - 'password1': 's@mple-p@$$w0rd', - 'password2': 's@mple-p@$$w0rd', - 'validity': True, + "username": "Wayne-lambert ", # Notice trailing spaces + "email": "test-email@example.com", + "first_name": "Wayne", + "last_name": "Lambert", + "password1": "s@mple-p@$$w0rd", + "password2": "s@mple-p@$$w0rd", + "validity": True, } def test_form_tests_for_all_fields(self): - """ Asserts all fields that need to be tested are present """ + """Asserts all fields that need to be tested are present""" form = UserRegisterForm() for field in self.fields: - if field != 'validity': + if field != "validity": assert field in form.fields def test_form_is_valid(self): - """ Asserts correctly filled in form is valid """ + """Asserts correctly filled in form is valid""" form = UserRegisterForm(data=self.good_data) - assert form.is_valid(), 'Should be valid' + assert form.is_valid(), "Should be valid" def test_empty_form_is_invalid(self): """ @@ -49,15 +53,15 @@ def test_empty_form_is_invalid(self): an invalid form """ form = UserRegisterForm(data={}) - assert not form.is_valid(), 'Should be invalid' + assert not form.is_valid(), "Should be invalid" def test_invalid_email_means_invalid_form(self): - """ Asserts an invalid email filled in form is valid """ + """Asserts an invalid email filled in form is valid""" form = UserRegisterForm(data=self.good_data) - form.data['email'] = 'test-emailexample.com' - assert not form.is_valid(), 'Should be invalid' + form.data["email"] = "test-emailexample.com" + assert not form.is_valid(), "Should be invalid" assert form.errors - assert form.errors['email'][0] == 'Enter a valid email address.' + assert form.errors["email"][0] == "Enter a valid email address." def test_username_is_cleaned(self): """ @@ -65,38 +69,38 @@ def test_username_is_cleaned(self): the trailing spaces """ form = UserRegisterForm(data=self.dirty_data) - assert len(form.data['username']) == 30, \ - 'With trailing spaces in form submission, the field is 30 chars in length.' + assert ( + len(form.data["username"]) == 30 + ), "With trailing spaces in form submission, the field is 30 chars in length." form.save(commit=False) - assert form.cleaned_data['username'] == 'wayne-lambert', 'Username has been trimmed' - assert len(form.cleaned_data['username']) == 13, "Example's username is 30 chars in length" - assert form.is_valid(), 'Should be valid' + assert form.cleaned_data["username"] == "wayne-lambert", "Username has been trimmed" + assert len(form.cleaned_data["username"]) == 13, "Example's username is 30 chars in length" + assert form.is_valid(), "Should be valid" class TestUserUpdateForm: - - fields = ('username', 'email', 'first_name', 'last_name', 'validity') + fields = ("username", "email", "first_name", "last_name", "validity") good_data = { - 'username': 'wayne-lambert', - 'email': 'test-email@example.com', - 'first_name': 'Wayne', - 'last_name': 'Lambert', - 'validity': True, + "username": "wayne-lambert", + "email": "test-email@example.com", + "first_name": "Wayne", + "last_name": "Lambert", + "validity": True, } dirty_data = { - 'username': 'Wayne-lambert ', # Notice trailing spaces - 'email': 'test-email@example.com', - 'first_name': 'Wayne', - 'last_name': 'Lambert', - 'validity': True, + "username": "Wayne-lambert ", # Notice trailing spaces + "email": "test-email@example.com", + "first_name": "Wayne", + "last_name": "Lambert", + "validity": True, } def test_form_tests_for_all_fields(self): - """ Asserts all fields that need to be tested are present """ + """Asserts all fields that need to be tested are present""" form = UserUpdateForm() for field in self.fields: - if field != 'validity': + if field != "validity": assert field in form.fields def test_empty_form_is_invalid(self): @@ -105,12 +109,12 @@ def test_empty_form_is_invalid(self): an invalid form """ form = UserUpdateForm(data={}) - assert not form.is_valid(), 'Should be invalid' + assert not form.is_valid(), "Should be invalid" def test_form_is_valid(self): - """ Asserts correctly filled in form is valid """ + """Asserts correctly filled in form is valid""" form = UserUpdateForm(data=self.good_data) - assert form.is_valid(), 'Should be valid' + assert form.is_valid(), "Should be valid" def test_username_is_cleaned(self): """ @@ -118,36 +122,36 @@ def test_username_is_cleaned(self): the trailing spaces """ form = UserUpdateForm(data=self.dirty_data) - assert len(form.data['username']) == 30, \ - 'With trailing spaces in form submission, the field is 30 chars in length.' + assert ( + len(form.data["username"]) == 30 + ), "With trailing spaces in form submission, the field is 30 chars in length." form.save(commit=False) - assert form.cleaned_data['username'] == 'wayne-lambert', 'Username has been trimmed' - assert form.cleaned_data['username'].islower(), 'Username is now in lowercase' - assert len(form.cleaned_data['username']) == 13, "Example's username is 30 chars in length" - assert form.is_valid(), 'Should be valid' + assert form.cleaned_data["username"] == "wayne-lambert", "Username has been trimmed" + assert form.cleaned_data["username"].islower(), "Username is now in lowercase" + assert len(form.cleaned_data["username"]) == 13, "Example's username is 30 chars in length" + assert form.is_valid(), "Should be valid" class TestProfileUpdateForm: - - fields = ('profile_picture', 'author_view', 'validity') + fields = ("profile_picture", "author_view", "validity") good_data_without_image = { - 'profile_picture': '', - 'author_view': 0, - 'validity': True, + "profile_picture": "", + "author_view": 0, + "validity": True, } def good_data_with_image(test_image): return { - 'profile_picture': test_image, - 'author_view': 0, - 'validity': True, - } + "profile_picture": test_image, + "author_view": 0, + "validity": True, + } def test_form_tests_for_all_fields(self): - """ Asserts all fields that need to be tested are present """ + """Asserts all fields that need to be tested are present""" form = ProfileUpdateForm() for field in self.fields: - if field != 'validity': + if field != "validity": assert field in form.fields def test_empty_form_is_invalid(self): @@ -156,57 +160,67 @@ def test_empty_form_is_invalid(self): an invalid form """ form = ProfileUpdateForm(data={}) - assert not form.is_valid(), 'Should be invalid' + assert not form.is_valid(), "Should be invalid" def test_form_without_image_is_valid(self): - """ Asserts correctly filled in form is valid """ + """Asserts correctly filled in form is valid""" form = ProfileUpdateForm(data=self.good_data_without_image) - assert form.is_valid(), 'Should be valid' + assert form.is_valid(), "Should be valid" def test_form_with_image_is_valid(self): - """ Asserts correctly filled in form is valid """ + """Asserts correctly filled in form is valid""" form = ProfileUpdateForm(data=self.good_data_with_image()) - assert form.is_valid(), 'Should be valid' + assert form.is_valid(), "Should be valid" class TestUserTOTPDeviceForm: - def test_token_field_contains_extra_attrs(self, device_auth_user): """ Asserts token field of the form includes the extra attrs. Other form functionality does not need to be tested because the form is a subclass of the Two Factor Auth package """ - key = device_auth_user.totpdevice_set.latest('id').key + key = device_auth_user.totpdevice_set.latest("id").key form = UserTOTPDeviceForm(key=key, user=device_auth_user) - extra_attrs = ("class", "title", "placeholder", ) + extra_attrs = ( + "class", + "title", + "placeholder", + ) token_field_attrs = form.fields["token"].widget.attrs for extra_attr in extra_attrs: - assert extra_attr in token_field_attrs, 'Each attr should be present' - assert len(token_field_attrs) == 8, 'Should be 8 attrs. 5 from package and 3 added' + assert extra_attr in token_field_attrs, "Each attr should be present" + assert len(token_field_attrs) == 8, "Should be 8 attrs. 5 from package and 3 added" class TestEmailTokenSubmissionForm: - - good_data = {'token': 123456} - bad_data = {'token': 'B@dT0k3n'} + good_data = {"token": 123456} + bad_data = {"token": "B@dT0k3n"} def test_form_is_valid(self): - """ Asserts correctly filled in form is valid """ + """Asserts correctly filled in form is valid""" form = EmailTokenSubmissionForm(data=self.good_data) - assert form.is_valid(), 'Should be valid' + assert form.is_valid(), "Should be valid" def test_form_is_invalid_with_string_submission(self): - """ Asserts incorrectly filled in form is invalid """ + """Asserts incorrectly filled in form is invalid""" form = EmailTokenSubmissionForm(data=self.bad_data) - assert not form.is_valid(), 'Should be invalid' + assert not form.is_valid(), "Should be invalid" def test_token_field_contains_desired_attrs(self): - """ Asserts token field of the form includes the set attrs. """ + """Asserts token field of the form includes the set attrs.""" form = EmailTokenSubmissionForm() set_attrs = ( - "autofocus", "inputmode", "autocomplete", "class", "title", "placeholder", "min", "max", ) + "autofocus", + "inputmode", + "autocomplete", + "class", + "title", + "placeholder", + "min", + "max", + ) token_field_attrs = form.fields["token"].widget.attrs for set_attr in set_attrs: - assert set_attr in token_field_attrs, 'Each attr should be present' - assert len(token_field_attrs) == 8, 'Should be 8 attrs set' + assert set_attr in token_field_attrs, "Each attr should be present" + assert len(token_field_attrs) == 8, "Should be 8 attrs set" diff --git a/apps/users/tests/test_mixins.py b/apps/users/tests/test_mixins.py index d3c72fce..78fbef8d 100644 --- a/apps/users/tests/test_mixins.py +++ b/apps/users/tests/test_mixins.py @@ -7,79 +7,76 @@ class TestDeviceAuthUserMixin: - class ExampleDeviceAuthView(DeviceAuthUserMixin, TemplateView): - template_name = 'random_template.html' + template_name = "random_template.html" def test_device_auth_user_passes_test_func(self, rf, device_auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = device_auth_user self.ExampleDeviceAuthView.as_view()(request) assert request.user.profile.is_two_factor_auth_by_token def test_email_auth_user_fails_test_func(self, rf, email_auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = email_auth_user self.ExampleDeviceAuthView.as_view()(request) assert not request.user.profile.is_two_factor_auth_by_token def test_handle_no_permission(self, rf, auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = auth_user response = self.ExampleDeviceAuthView.as_view()(request) assert isinstance(response, HttpResponseRedirect) - assert response.status_code == 302, 'Should redirect the user' - assert '/users/login/' in response.url, 'Should return the user to the login page' + assert response.status_code == 302, "Should redirect the user" + assert "/users/login/" in response.url, "Should return the user to the login page" class TestEmailAuthUserMixin: - class ExampleEmailAuthView(EmailAuthUserMixin, TemplateView): - template_name = 'random_template.html' + template_name = "random_template.html" def test_device_auth_user_fails_test_func(self, rf, device_auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = device_auth_user self.ExampleEmailAuthView.as_view()(request) assert not request.user.profile.is_two_factor_auth_by_email def test_email_auth_user_passes_test_func(self, rf, email_auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = email_auth_user self.ExampleEmailAuthView.as_view()(request) assert request.user.profile.is_two_factor_auth_by_email def test_handle_no_permission(self, rf, auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = auth_user response = self.ExampleEmailAuthView.as_view()(request) assert isinstance(response, HttpResponseRedirect) - assert response.status_code == 302, 'Should redirect the user' - assert '/users/login/' in response.url, 'Should return the user to the login page' + assert response.status_code == 302, "Should redirect the user" + assert "/users/login/" in response.url, "Should return the user to the login page" class TestTwoFactorAuthUserMixin: - class ExampleTwoFactorAuthView(TwoFactorAuthUserMixin, TemplateView): - template_name = 'random_template.html' + template_name = "random_template.html" def test_device_auth_user_passes_test_func(self, rf, device_auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = device_auth_user self.ExampleTwoFactorAuthView.as_view()(request) assert request.user.profile.is_two_factor_authenticated def test_email_auth_user_passes_test_func(self, rf, email_auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = email_auth_user self.ExampleTwoFactorAuthView.as_view()(request) assert request.user.profile.is_two_factor_authenticated def test_handle_no_permission(self, rf, auth_user): - request = rf.get('/any-random-url/') + request = rf.get("/any-random-url/") request.user = auth_user apps_helpers.add_middlewares(request) response = self.ExampleTwoFactorAuthView.as_view()(request) assert isinstance(response, HttpResponseRedirect) - assert response.status_code == 302, 'Should redirect the user' - assert '/users/login/' in response.url, 'Should return the user to the login page' + assert response.status_code == 302, "Should redirect the user" + assert "/users/login/" in response.url, "Should return the user to the login page" diff --git a/apps/users/tests/test_models.py b/apps/users/tests/test_models.py index 677389c6..84e0bd8c 100644 --- a/apps/users/tests/test_models.py +++ b/apps/users/tests/test_models.py @@ -10,59 +10,64 @@ class TestProfile: def test_user_is_onetoonefield(self, random_user): field = random_user.profile._meta.get_field("user") - assert isinstance(field, models.OneToOneField), 'Should be a one-to-one field' + assert isinstance(field, models.OneToOneField), "Should be a one-to-one field" def test_slug_is_slugfield(self, random_user): field = random_user.profile._meta.get_field("slug") - assert isinstance(field, models.SlugField), 'Should be a slug field' + assert isinstance(field, models.SlugField), "Should be a slug field" def test_slugification(self, random_user): random_user.profile.slug = slugify(random_user.username) - profile_slug_fragments = random_user.profile.slug.split('-') + profile_slug_fragments = random_user.profile.slug.split("-") for fragment in profile_slug_fragments: assert fragment.casefold() in random_user.profile.slug if len(profile_slug_fragments) > 1: - assert '-' in random_user.profile.slug, 'Should contain a hyphen' + assert "-" in random_user.profile.slug, "Should contain a hyphen" def test_author_view_is_integerfield(self, random_user): field = random_user.profile._meta.get_field("author_view") - assert isinstance(field, models.IntegerField), 'Should be an integer field' + assert isinstance(field, models.IntegerField), "Should be an integer field" def test_profile_picture_is_imagefield(self, random_user): field = random_user.profile._meta.get_field("profile_picture") - assert isinstance(field, models.ImageField), 'Should be an image date field' + assert isinstance(field, models.ImageField), "Should be an image date field" def test_created_date_is_datetimefield(self, random_user): field = random_user.profile._meta.get_field("created_date") - assert isinstance(field, models.DateTimeField), 'Should be an image date field' + assert isinstance(field, models.DateTimeField), "Should be an image date field" def test_updated_date_is_imagefield(self, random_user): field = random_user.profile._meta.get_field("updated_date") - assert isinstance(field, models.DateTimeField), 'Should be an image date field' + assert isinstance(field, models.DateTimeField), "Should be an image date field" def test_profile_str(self, fixed_user): - assert fixed_user.pk == 2, 'User instance should be set up' - assert fixed_user.profile.pk == 2, 'Profile instance with signal should be set up' - assert fixed_user.profile.__str__() == 'Wayne Lambert (wayne-lambert)', \ - '__str__ method should be formatted' + assert fixed_user.pk == 2, "User instance should be set up" + assert fixed_user.profile.pk == 2, "Profile instance with signal should be set up" + assert ( + fixed_user.profile.__str__() == "Wayne Lambert (wayne-lambert)" + ), "__str__ method should be formatted" def test_initials(self, fixed_user): - assert fixed_user.profile.initials == 'WL', 'Should be first letter of first and last name' + assert fixed_user.profile.initials == "WL", "Should be first letter of first and last name" def test_join_year(self, random_user): - assert random_user.profile.join_year == random_user.date_joined.year, \ - "Year should be the same as the `date_joined` field year property" + assert ( + random_user.profile.join_year == random_user.date_joined.year + ), "Year should be the same as the `date_joined` field year property" def test_display_name_is_username(self, random_user): random_user.profile.author_view = 0 - assert random_user.profile.display_name == random_user.get_username(), \ - "Should be the user's username" + assert ( + random_user.profile.display_name == random_user.get_username() + ), "Should be the user's username" def test_display_name_is_full_name(self, random_user): random_user.profile.author_view = 1 - assert random_user.profile.display_name == random_user.get_full_name(), \ - "Should be the user's full name" + assert ( + random_user.profile.display_name == random_user.get_full_name() + ), "Should be the user's full name" def test_get_absolute_url(self, random_user): assert random_user.profile.get_absolute_url() == reverse( - 'blog:users:profile', kwargs={'username': random_user.profile.slug}), 'Should resolve URL' + "blog:users:profile", kwargs={"username": random_user.profile.slug} + ), "Should resolve URL" diff --git a/apps/users/tests/test_urls.py b/apps/users/tests/test_urls.py index 7e689173..867bdc6d 100644 --- a/apps/users/tests/test_urls.py +++ b/apps/users/tests/test_urls.py @@ -3,18 +3,18 @@ class TestUserURLs: def test_register(self): - """ Verify that the `register` url invokes intended view """ - resolver = resolve(reverse('blog:users:register')) - assert resolver.view_name, 'register' + """Verify that the `register` url invokes intended view""" + resolver = resolve(reverse("blog:users:register")) + assert resolver.view_name, "register" def test_profile_screen(self): - """ Verify that the `profile` url invokes intended view """ - resolver = resolve(reverse( - 'blog:users:profile', kwargs={'username': 'wayne-lambert'})) - assert resolver.view_name, 'profile' + """Verify that the `profile` url invokes intended view""" + resolver = resolve(reverse("blog:users:profile", kwargs={"username": "wayne-lambert"})) + assert resolver.view_name, "profile" def test_profile_update_screen(self): - """ Verify that the `profile update` url invokes intended view """ - resolver = resolve(reverse( - 'blog:users:profile_update', kwargs={'username': 'wayne-lambert'})) - assert resolver.view_name, 'profile_update' + """Verify that the `profile update` url invokes intended view""" + resolver = resolve( + reverse("blog:users:profile_update", kwargs={"username": "wayne-lambert"}) + ) + assert resolver.view_name, "profile_update" diff --git a/apps/users/tests/test_views.py b/apps/users/tests/test_views.py index b9158813..a3c09adb 100644 --- a/apps/users/tests/test_views.py +++ b/apps/users/tests/test_views.py @@ -14,51 +14,50 @@ pytestmark = pytest.mark.django_db(reset_sequences=True) -class TestUserRegisterView: - path = reverse('blog:users:register') +class TestUserRegisterView: + path = reverse("blog:users:register") def test_auth_user_cannot_access(self, rf, auth_user): - """ Asserts authenticated user can't access `registration` iew """ + """Asserts authenticated user can't access `registration` iew""" request = rf.get(self.path) request.user = auth_user response = UserRegisterView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_unauth_user_can_access(self, rf, unauth_user): - """ Asserts unauthenticated user can access `registration` view """ + """Asserts unauthenticated user can access `registration` view""" request = rf.get(self.path) request.user = unauth_user response = UserRegisterView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_form_valid(self, rf, django_user_model, sample_user_data): """ Asserts that a form with valid data is considered valid and redirects the user accordingly """ - django_user_model.objects.create(username='wayne-lambert', id=2) + django_user_model.objects.create(username="wayne-lambert", id=2) kwargs = sample_user_data request = rf.post(self.path, kwargs) apps_helpers.add_middlewares(request) response = UserRegisterView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Should be redirected' - assert '/blog/' in response.url, 'Should redirect to `blog` home screen' - assert Profile.objects.count() == 2, 'Should have 2 objects in the database' + assert response.status_code == 302, "Should be redirected" + assert "/blog/" in response.url, "Should redirect to `blog` home screen" + assert Profile.objects.count() == 2, "Should have 2 objects in the database" class TestProfileView: - @pytest.mark.django_db(reset_sequences=True) def test_unauth_user_is_redirected_to_login(self, rf, unauth_user): - """ Asserts an unauth user cannot access a non-existent `profile` """ - kwargs = {'username': 'inexistent-user'} - path = reverse('blog:users:profile', kwargs=kwargs) + """Asserts an unauth user cannot access a non-existent `profile`""" + kwargs = {"username": "inexistent-user"} + path = reverse("blog:users:profile", kwargs=kwargs) request = rf.get(path) apps_helpers.add_middlewares(request) request.user = unauth_user response = ProfileView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Should be redirected' + assert response.status_code == 302, "Should be redirected" assert resolve_url(settings.LOGIN_URL) in response.url def test_auth_user_is_redirected_to_login(self, rf, auth_user): @@ -66,13 +65,13 @@ def test_auth_user_is_redirected_to_login(self, rf, auth_user): Asserts an authenticated user cannot access an inexistent profile and is redirected to the site's login page """ - kwargs = {'username': 'inexistent-user'} - path = reverse('blog:users:profile', kwargs=kwargs) + kwargs = {"username": "inexistent-user"} + path = reverse("blog:users:profile", kwargs=kwargs) request = rf.get(path) apps_helpers.add_middlewares(request) request.user = auth_user response = ProfileView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Should be redirected' + assert response.status_code == 302, "Should be redirected" assert resolve_url(settings.LOGIN_URL) in response.url def test_device_auth_user_cannot_access_inexistent_profile(self, rf, device_auth_user): @@ -81,8 +80,8 @@ def test_device_auth_user_cannot_access_inexistent_profile(self, rf, device_auth using their device cannot access a non-existent `profile`, therefore they're returned a 404 error """ - kwargs = {'username': 'inexistent-user'} - path = reverse('blog:users:profile', kwargs=kwargs) + kwargs = {"username": "inexistent-user"} + path = reverse("blog:users:profile", kwargs=kwargs) request = rf.get(path) request.user = device_auth_user assert request.user.profile.is_two_factor_auth_by_token @@ -95,8 +94,8 @@ def test_email_auth_user_cannot_access_inexistent_profile(self, rf, email_auth_u using their email cannot access a non-existent `profile`, therefore they're returned a 404 error """ - kwargs = {'username': 'inexistent-user'} - path = reverse('blog:users:profile', kwargs=kwargs) + kwargs = {"username": "inexistent-user"} + path = reverse("blog:users:profile", kwargs=kwargs) request = rf.get(path) request.user = email_auth_user assert request.user.profile.is_two_factor_auth_by_email @@ -110,76 +109,78 @@ def test_auth_user_can_access(self, rf, auth_user): Asserts the `profile update` view is accessible by an authenticated user """ - kwargs = {'username': auth_user.username} - path = reverse('blog:users:profile_update', kwargs=kwargs) + kwargs = {"username": auth_user.username} + path = reverse("blog:users:profile_update", kwargs=kwargs) request = rf.get(path) request.user = auth_user response = ProfileUpdateView.as_view()(request, **kwargs) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_unauth_user_cannot_access(self, rf, auth_user, unauth_user): """ Asserts `profile update` view inaccessible by unauthenticated user """ - kwargs = {'username': auth_user.username} - path = reverse('blog:users:profile_update', kwargs=kwargs) + kwargs = {"username": auth_user.username} + path = reverse("blog:users:profile_update", kwargs=kwargs) request = rf.get(path) request.user = unauth_user apps_helpers.add_middlewares(request) response = ProfileUpdateView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Should return with an `redirect` status code' - assert '/login/' in response.url, 'Should redirect to login page' + assert response.status_code == 302, "Should return with an `redirect` status code" + assert "/login/" in response.url, "Should redirect to login page" def test_another_user_cannot_access(self, rf, auth_user, li_sec_user): """ Asserts `profile update` view inaccessible by unauthenticated user """ - kwargs = {'username': auth_user.username} - path = reverse('blog:users:profile_update', kwargs=kwargs) + kwargs = {"username": auth_user.username} + path = reverse("blog:users:profile_update", kwargs=kwargs) request = rf.get(path) request.user = li_sec_user apps_helpers.add_middlewares(request) response = ProfileUpdateView.as_view()(request, **kwargs) - assert response.status_code == 302, 'Mixin should yield permanent redirect' - assert resolve_url(settings.LOGIN_URL) in response.url, 'Should redirect to login page' + assert response.status_code == 302, "Mixin should yield permanent redirect" + assert resolve_url(settings.LOGIN_URL) in response.url, "Should redirect to login page" -@pytest.mark.parametrize(argnames='all_users', - argvalues=[pytest.param('auth_user'), pytest.param('unauth_user')], indirect=True) +@pytest.mark.parametrize( + argnames="all_users", + argvalues=[pytest.param("auth_user"), pytest.param("unauth_user")], + indirect=True, +) class TestAuthViews: - def test_all_users_can_login(self, rf, all_users): - """ Asserts the `login` view is publicly accessible """ - path = reverse('blog:users:login') + """Asserts the `login` view is publicly accessible""" + path = reverse("blog:users:login") request = rf.get(path) request.user = all_users response = auth_views.LoginView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_all_users_can_password_reset(self, rf, all_users): - """ Asserts the `password reset` view is publicly accessible """ - path = reverse('blog:users:password_reset_form') + """Asserts the `password reset` view is publicly accessible""" + path = reverse("blog:users:password_reset_form") request = rf.get(path) request.user = all_users response = auth_views.LoginView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_all_users_can_access_password_reset_done(self, rf, all_users): """ Asserts the `password reset done` view is publicly accessible """ - path = reverse('blog:users:password_reset_done') + path = reverse("blog:users:password_reset_done") request = rf.get(path) request.user = all_users response = auth_views.LoginView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" def test_all_users_can_access_password_reset_complete(self, rf, all_users): """ Asserts `password reset complete` view is publicly accessible """ - path = reverse('blog:users:password_reset_complete') + path = reverse("blog:users:password_reset_complete") request = rf.get(path) request.user = all_users response = auth_views.LoginView.as_view()(request) - assert response.status_code == 200, 'Should return an `OK` status code' + assert response.status_code == 200, "Should return an `OK` status code" diff --git a/apps/users/urls.py b/apps/users/urls.py index 47f43a09..eedbd1ac 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -3,33 +3,51 @@ from two_factor.views import QRGeneratorView -from apps.users.views import (ProfileUpdateView, ProfileView, UserLoginView, - UserPasswordResetCompleteView, UserPasswordResetConfirmView, - UserPasswordResetDoneView, UserPasswordResetView, - UserRegisterView, UserSetupEmailTokenView, - UserSetupEmailView, UserSetupQRView,) +from apps.users.views import ( + ProfileUpdateView, + ProfileView, + UserLoginView, + UserPasswordResetCompleteView, + UserPasswordResetConfirmView, + UserPasswordResetDoneView, + UserPasswordResetView, + UserRegisterView, + UserSetupEmailTokenView, + UserSetupEmailView, + UserSetupQRView, +) -app_name = 'users' +app_name = "users" urlpatterns = [ - path('register/', UserRegisterView.as_view(), name='register'), - path('/profile/', ProfileView.as_view(), name='profile'), - path('/profile/update/', ProfileUpdateView.as_view(), name='profile_update'), + path("register/", UserRegisterView.as_view(), name="register"), + path("/profile/", ProfileView.as_view(), name="profile"), + path("/profile/update/", ProfileUpdateView.as_view(), name="profile_update"), ] # Custom Login, Two-Factor Authentication and Password Reset Processes urlpatterns += [ - path('login/', UserLoginView.as_view(), name='login'), - path('two-factor/setup/qr/', UserSetupQRView.as_view(), name='setup'), - path('two-factor/qrcode/', QRGeneratorView.as_view(), name='qr'), - path('two-factor/setup/email/', UserSetupEmailView.as_view(), name='setup_email'), - path('two-factor/setup/email/token/', UserSetupEmailTokenView.as_view(), name='setup_email_token'), - path('logout/', auth_views.LogoutView.as_view(), name='logout'), - path('password-reset/', UserPasswordResetView.as_view(), name='password_reset_form'), - path('password-reset/done/', UserPasswordResetDoneView.as_view(), name='password_reset_done'), - path('password-reset-confirm///', - UserPasswordResetConfirmView.as_view(), name='password_reset_confirm'), - path('password-reset-complete/', - UserPasswordResetCompleteView.as_view(), name='password_reset_complete'), + path("login/", UserLoginView.as_view(), name="login"), + path("two-factor/setup/qr/", UserSetupQRView.as_view(), name="setup"), + path("two-factor/qrcode/", QRGeneratorView.as_view(), name="qr"), + path("two-factor/setup/email/", UserSetupEmailView.as_view(), name="setup_email"), + path( + "two-factor/setup/email/token/", + UserSetupEmailTokenView.as_view(), + name="setup_email_token", + ), + path("logout/", auth_views.LogoutView.as_view(), name="logout"), + path("password-reset/", UserPasswordResetView.as_view(), name="password_reset_form"), + path("password-reset/done/", UserPasswordResetDoneView.as_view(), name="password_reset_done"), + path( + "password-reset-confirm///", + UserPasswordResetConfirmView.as_view(), + name="password_reset_confirm", + ), + path( + "password-reset-complete/", + UserPasswordResetCompleteView.as_view(), + name="password_reset_complete", + ), ] diff --git a/apps/users/utils.py b/apps/users/utils.py index b63fdbaf..32102e46 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -8,7 +8,7 @@ def generate_token() -> str: - """ Generates a 6 digit random number including any leading zeros """ + """Generates a 6 digit random number including any leading zeros""" return str(random.randint(0, 999_999)).zfill(6) @@ -29,5 +29,5 @@ def get_token_expiration_timestamp(): def token_validator(*args, **kwargs): - """ Wraps hex_validator generator satisfying `makemigrations` """ + """Wraps hex_validator generator satisfying `makemigrations`""" return hex_validator()(*args, **kwargs) diff --git a/apps/users/views.py b/apps/users/views.py index 27bbc748..4c59fe69 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -6,9 +6,12 @@ from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.hashers import check_password from django.contrib.auth.mixins import UserPassesTestMixin -from django.contrib.auth.views import (PasswordResetCompleteView, - PasswordResetConfirmView, PasswordResetDoneView, - PasswordResetView,) +from django.contrib.auth.views import ( + PasswordResetCompleteView, + PasswordResetConfirmView, + PasswordResetDoneView, + PasswordResetView, +) from django.core.mail.message import EmailMultiAlternatives from django.shortcuts import get_object_or_404, redirect from django.template.loader import render_to_string @@ -24,17 +27,22 @@ from users.mixins import TwoFactorAuthUserMixin from users.utils import generate_token, get_challenge_expiration_timestamp -from apps.users.forms import (EmailTokenSubmissionForm, ProfileUpdateForm, - UserRegisterForm, UserTOTPDeviceForm, UserUpdateForm,) +from apps.users.forms import ( + EmailTokenSubmissionForm, + ProfileUpdateForm, + UserRegisterForm, + UserTOTPDeviceForm, + UserUpdateForm, +) from apps.users.models import EmailToken, Profile class UserRegisterView(CreateView): model = Profile form_class = UserRegisterForm - template_name = 'users/register.html' - success_url = reverse_lazy('blog:users:setup') - register_url = reverse_lazy('blog:users:register') + template_name = "users/register.html" + success_url = reverse_lazy("blog:users:setup") + register_url = reverse_lazy("blog:users:register") html_msg = """ The username you've attempted to register with is already taken.

Perhaps you already have an account? If so, you can log in using @@ -42,7 +50,7 @@ class UserRegisterView(CreateView): """ def user_exists(self, form) -> bool: - username = form.data['username'] + username = form.data["username"] user = get_user_model().objects.filter(username=username) if user.exists(): return True @@ -50,10 +58,10 @@ def user_exists(self, form) -> bool: def form_valid(self, form): self.object = form.save() form_valid = super().form_valid(form) - username = form.cleaned_data['username'] - password = form.cleaned_data['password1'] + username = form.cleaned_data["username"] + password = form.cleaned_data["password1"] user = auth.authenticate(username=username, password=password) - backend = 'django.contrib.auth.backends.ModelBackend' + backend = "django.contrib.auth.backends.ModelBackend" auth.login(self.request, user=user, backend=backend) return form_valid @@ -64,46 +72,46 @@ def form_invalid(self, form): class UserLoginView(LoginView): - template_name = 'users/login.html' - two_factor_setup_url = reverse_lazy('blog:users:setup') - two_factor_setup_email_url = reverse_lazy('blog:users:setup_email') + template_name = "users/login.html" + two_factor_setup_url = reverse_lazy("blog:users:setup") + two_factor_setup_email_url = reverse_lazy("blog:users:setup_email") form_list = ( - ('auth', AuthenticationForm), - ('token', AuthenticationTokenForm), + ("auth", AuthenticationForm), + ("token", AuthenticationTokenForm), ) def _add_user_does_not_exist_message(self): - """ Constructs a message for the UI """ + """Constructs a message for the UI""" html_msg = ( - "The username you've attempted to log in with does not exist.

" + - "Please re-check the username and try again." + "The username you've attempted to log in with does not exist.

" + + "Please re-check the username and try again." ) messages.info(self.request, mark_safe(html_msg)) def _add_incorrect_password_message(self): - """ Constructs a message for the UI """ + """Constructs a message for the UI""" html_msg = ( - "Please enter a correct username and password.

" + - "Note that both fields may be case-sensitive." + "Please enter a correct username and password.

" + + "Note that both fields may be case-sensitive." ) messages.error(self.request, mark_safe(html_msg)) def retrieve_token_from_db(self, user) -> EmailToken: - """ Retrieves the latest email token for the user from the DB """ - return EmailToken.objects.filter(user_id=user.id).latest('id') + """Retrieves the latest email token for the user from the DB""" + return EmailToken.objects.filter(user_id=user.id).latest("id") def build_html_content(self, user, token) -> str: - """" Specifies the email template and context variables """ + """ " Specifies the email template and context variables""" return render_to_string( - template_name='emails/token.html', + template_name="emails/token.html", context={ - 'user': user, - 'token': token, - } + "user": user, + "token": token, + }, ) def email_two_factor_token(self, user: get_user_model(), token): - """ Sends email containing current token """ + """Sends email containing current token""" subject = "Your One Time Token" msg = EmailMultiAlternatives( @@ -112,20 +120,20 @@ def email_two_factor_token(self, user: get_user_model(), token): from_email=settings.DEFAULT_FROM_EMAIL_SES, to=[user.email], ) - msg.content_subtype = 'html' - msg.mixed_subtype = 'related' + msg.content_subtype = "html" + msg.mixed_subtype = "related" msg.send() def _get_credentials(self, user) -> Dict[str, Any]: - """ Gets the credentials of the user being attempted """ + """Gets the credentials of the user being attempted""" return { - 'username': user.get_username(), - 'password': self.storage.request._post['auth-password'], + "username": user.get_username(), + "password": self.storage.request._post["auth-password"], } def is_password_correct(self, user, credentials) -> bool: - """ Checks the password entered by the user passes authentication check """ - return bool(check_password(credentials['password'], user.password)) + """Checks the password entered by the user passes authentication check""" + return bool(check_password(credentials["password"], user.password)) def authenticate_user(self, user): """ @@ -136,23 +144,23 @@ def authenticate_user(self, user): password_valid = self.is_password_correct(user, credentials) if not password_valid and self.request.user.is_anonymous: return self._add_incorrect_password_message() - username = credentials['username'] - password = credentials['password'] + username = credentials["username"] + password = credentials["password"] return auth.authenticate(request=self.request, username=username, password=password) def login_user(self, user): - """ Logs in the already authenticated user """ - backend = 'django.contrib.auth.backends.ModelBackend' + """Logs in the already authenticated user""" + backend = "django.contrib.auth.backends.ModelBackend" auth.login(self.request, user=user, backend=backend) def handle_email_auth_user(self, user): - """ Handles the actions for processing an email authenticated user """ + """Handles the actions for processing an email authenticated user""" user_passes_auth = self.authenticate_user(user=user) if user_passes_auth: self.login_user(user=user) token = self.retrieve_token_from_db(user) self.email_two_factor_token(user, token) - return redirect('blog:users:setup_email_token') + return redirect("blog:users:setup_email_token") return redirect(self.request.path_info) def post(self, *args, **kwargs): @@ -164,8 +172,8 @@ def post(self, *args, **kwargs): uses email, the project code handles it. """ - if self.steps.current == 'auth': - username = self.storage.request._post['auth-username'] + if self.steps.current == "auth": + username = self.storage.request._post["auth-username"] # Scenario 1: The user does not exist in the DB try: @@ -193,56 +201,54 @@ def post(self, *args, **kwargs): # If at the token step of the login wizard and the user uses the token method, # enable the Django Two-Factor Auth package to handle - elif self.steps.current == 'token': - user_pk = self.request.session['wizard_user_login_view']['user_pk'] + elif self.steps.current == "token": + user_pk = self.request.session["wizard_user_login_view"]["user_pk"] user = get_user_model().objects.get(pk=user_pk) if user.profile.is_two_factor_auth_by_token: return super().post(*args, **kwargs) class UserSetupQRView(SetupView): - template_name = 'two_factor/setup_by_qr.html' - success_url = reverse_lazy('blog:home') + template_name = "two_factor/setup_by_qr.html" + success_url = reverse_lazy("blog:home") - form_list = ( - ('generator', UserTOTPDeviceForm), - ) + form_list = (("generator", UserTOTPDeviceForm),) condition_dict = { - 'generator': lambda self: True, + "generator": lambda self: True, } def get_method(self): - return 'generator' + return "generator" class UserSetupEmailView(TemplateView): - template_name = 'two_factor/setup_by_email.html' - success_url = reverse_lazy('blog:users:setup_email_token') + template_name = "two_factor/setup_by_email.html" + success_url = reverse_lazy("blog:users:setup_email_token") def store_token_in_db(self, user: get_user_model(), token: str): - """ Creates an email token object in the DB """ + """Creates an email token object in the DB""" EmailToken.objects.create( challenge_email_address=user.email, challenge_token=token, challenge_generation_timestamp=timezone.now(), challenge_expiration_timestamp=get_challenge_expiration_timestamp(), challenge_completed=False, - user_id=user.id + user_id=user.id, ) def build_html_content(self, user: get_user_model(), token: str) -> str: - """" Specifies the email template and context variables """ + """ " Specifies the email template and context variables""" return render_to_string( - template_name='emails/token.html', + template_name="emails/token.html", context={ - 'user': user, - 'token': token, - 'setup': True, - } + "user": user, + "token": token, + "setup": True, + }, ) def email_two_factor_token(self, user: get_user_model(), token: str): - """ Sends email containing one-time token """ + """Sends email containing one-time token""" subject = "Your One Time Token" msg = EmailMultiAlternatives( @@ -251,13 +257,12 @@ def email_two_factor_token(self, user: get_user_model(), token: str): from_email=settings.DEFAULT_FROM_EMAIL_SES, to=[user.email], ) - msg.content_subtype = 'html' - msg.mixed_subtype = 'related' + msg.content_subtype = "html" + msg.mixed_subtype = "related" msg.send() - def post(self, request, *args, **kwargs): - """ Master func handling the user clicking the `Send Token by Email` button """ + """Master func handling the user clicking the `Send Token by Email` button""" token = generate_token() user = request.user self.store_token_in_db(user, token) @@ -266,7 +271,7 @@ def post(self, request, *args, **kwargs): class UserSetupEmailTokenView(FormView): - template_name = 'two_factor/setup_email_token.html' + template_name = "two_factor/setup_email_token.html" form_class = EmailTokenSubmissionForm success_url = reverse_lazy(settings.LOGIN_REDIRECT_URL) @@ -278,17 +283,17 @@ def get_context_data(self, **kwargs) -> Dict[str, Any]: """ context = super().get_context_data(user=self.request.user, **kwargs) email = self.request.user.email.strip() - domain = email.split('@')[-1] - context['user_first_name'] = self.request.user.get_short_name() - context['redacted_user_email'] = f"{email[0:2]}**********@{domain}" + domain = email.split("@")[-1] + context["user_first_name"] = self.request.user.get_short_name() + context["redacted_user_email"] = f"{email[0:2]}**********@{domain}" return context def get_email_token(self) -> EmailToken: - """ Retrieves the latest email token from the DB for the user """ - return EmailToken.objects.filter(user_id=self.request.user.id).latest('id') + """Retrieves the latest email token from the DB for the user""" + return EmailToken.objects.filter(user_id=self.request.user.id).latest("id") def does_challenge_pass(self, token_returned) -> bool: - """ Evaluates whether the token input passes the challenge """ + """Evaluates whether the token input passes the challenge""" token_to_match = self.get_email_token().challenge_token return token_returned == token_to_match @@ -301,18 +306,18 @@ def update_db(self, email_token): email_token.save() def build_html_content(self, user, token) -> str: - """" Specifies the email template and context variables """ + """ " Specifies the email template and context variables""" return render_to_string( - template_name='emails/success.html', + template_name="emails/success.html", context={ - 'user': user, - 'token': token, - 'setup': True, - } + "user": user, + "token": token, + "setup": True, + }, ) def email_two_factor_success(self, user: get_user_model(), token): - """ Sends email containing one-time token """ + """Sends email containing one-time token""" subject = "Two-Factor Authentication Successful" msg = EmailMultiAlternatives( @@ -321,8 +326,8 @@ def email_two_factor_success(self, user: get_user_model(), token): from_email=settings.DEFAULT_FROM_EMAIL_SES, to=[user.email], ) - msg.content_subtype = 'html' - msg.mixed_subtype = 'related' + msg.content_subtype = "html" + msg.mixed_subtype = "related" msg.send() def populate_message(self, challenge_passes, token_within_expiry): @@ -332,21 +337,21 @@ def populate_message(self, challenge_passes, token_within_expiry): """ if not challenge_passes: msg = ( - "The token you have entered is incorrect.

" + - "Please re-check the code and try again." + "The token you have entered is incorrect.

" + + "Please re-check the code and try again." ) messages.add_message(self.request, messages.INFO, mark_safe(msg)) elif not token_within_expiry: msg = ( - "The 5 minute expiration time has elapsed.

" + - "Use the 'still no code?' link below to generate a new token " + - "which you will receive by email. Then re-enter the new code above." + "The 5 minute expiration time has elapsed.

" + + "Use the 'still no code?' link below to generate a new token " + + "which you will receive by email. Then re-enter the new code above." ) messages.add_message(self.request, messages.INFO, mark_safe(msg)) def form_valid(self, form): super().form_valid(form) - token_returned = str(form.cleaned_data['token']).zfill(6) + token_returned = str(form.cleaned_data["token"]).zfill(6) challenge_passes = self.does_challenge_pass(token_returned) email_token = self.get_email_token() if challenge_passes and email_token.is_challenge_within_expiry: @@ -354,14 +359,14 @@ def form_valid(self, form): self.email_two_factor_success(self.request.user, email_token) return redirect(self.success_url) self.populate_message(challenge_passes, email_token.is_challenge_within_expiry) - return redirect('blog:users:setup_email_token') + return redirect("blog:users:setup_email_token") class ProfileView(TwoFactorAuthUserMixin, DetailView): - template_name = 'users/profile.html' + template_name = "users/profile.html" def get_object(self, queryset=None): - return get_object_or_404(get_user_model(), username=self.kwargs['username']) + return get_object_or_404(get_user_model(), username=self.kwargs["username"]) class ProfileUpdateView(TwoFactorAuthUserMixin, UserPassesTestMixin, MultiModelFormView): @@ -369,38 +374,39 @@ class ProfileUpdateView(TwoFactorAuthUserMixin, UserPassesTestMixin, MultiModelF Any user attempting to GET the profile update page of another user, whether present in the DB or not, will receive a 403 error. """ + form_classes = (UserUpdateForm, ProfileUpdateForm) - template_name = 'users/profile_update.html' + template_name = "users/profile_update.html" def test_func(self) -> bool: - return self.request.user.get_username() == self.kwargs['username'] + return self.request.user.get_username() == self.kwargs["username"] def get_instances(self) -> Dict[str, Any]: return { - 'userupdateform': self.request.user, - 'profileupdateform': self.request.user.profile + "userupdateform": self.request.user, + "profileupdateform": self.request.user.profile, } def get_success_url(self): username = self.request.user.get_username() - return reverse('blog:users:profile', kwargs={'username': username}) + return reverse("blog:users:profile", kwargs={"username": username}) class UserPasswordResetView(PasswordResetView): - template_name = 'users/password_reset_form.html' - email_template_name = 'users/password_reset_email.html' - subject_template_name = 'users/password_reset_subject.txt' - success_url = reverse_lazy('blog:users:password_reset_done') + template_name = "users/password_reset_form.html" + email_template_name = "users/password_reset_email.html" + subject_template_name = "users/password_reset_subject.txt" + success_url = reverse_lazy("blog:users:password_reset_done") class UserPasswordResetDoneView(PasswordResetDoneView): - template_name = 'users/password_reset_done.html' + template_name = "users/password_reset_done.html" class UserPasswordResetConfirmView(PasswordResetConfirmView): - template_name = 'users/password_reset_confirm.html' - success_url = reverse_lazy('blog:users:password_reset_complete') + template_name = "users/password_reset_confirm.html" + success_url = reverse_lazy("blog:users:password_reset_complete") class UserPasswordResetCompleteView(PasswordResetCompleteView): - template_name='users/password_reset_complete.html' + template_name = "users/password_reset_complete.html" diff --git a/conftest.py b/conftest.py index da9a8299..554a07e4 100644 --- a/conftest.py +++ b/conftest.py @@ -8,22 +8,20 @@ def pytest_addoption(parser): - """ Sets a command line flag `--runslow` for the CLI """ - parser.addoption( - '--runslow', action='store_true', default=False, help='run slow tests' - ) + """Sets a command line flag `--runslow` for the CLI""" + parser.addoption("--runslow", action="store_true", default=False, help="run slow tests") def pytest_configure(config): - """ Sets tests with `@pytest.mark.slow` decorator as ones to be skipped """ - config.addinivalue_line('markers', 'slow: mark test as slow to run') + """Sets tests with `@pytest.mark.slow` decorator as ones to be skipped""" + config.addinivalue_line("markers", "slow: mark test as slow to run") def pytest_collection_modifyitems(config, items): - """ Adds `slow` marker upon collection of tests to run """ - if config.getoption('--runslow'): + """Adds `slow` marker upon collection of tests to run""" + if config.getoption("--runslow"): return - skip_slow = pytest.mark.skip(reason='need --runslow option to run') + skip_slow = pytest.mark.skip(reason="need --runslow option to run") for item in items: - if 'slow' in item.keywords: + if "slow" in item.keywords: item.add_marker(skip_slow) diff --git a/docker/prod/gunicorn/conf.py b/docker/prod/gunicorn/conf.py index ad548a57..b36bf6e7 100644 --- a/docker/prod/gunicorn/conf.py +++ b/docker/prod/gunicorn/conf.py @@ -1,9 +1,9 @@ import multiprocessing -name = 'aa_project' -loglevel = 'debug' -errorlog = '-' +name = "aa_project" +loglevel = "debug" +errorlog = "-" accesslog = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' -bind = '0.0.0.0:8000' +bind = "0.0.0.0:8000" workers = multiprocessing.cpu_count() * 2 + 1 diff --git a/manage.py b/manage.py index c2a50225..597003df 100755 --- a/manage.py +++ b/manage.py @@ -3,8 +3,8 @@ import sys -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', os.environ['DJANGO_SETTINGS_MODULE']) +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.environ["DJANGO_SETTINGS_MODULE"]) try: from django.conf import settings @@ -12,12 +12,14 @@ if settings.DEBUG: from rich import pretty, traceback + pretty.install() traceback.install() - if os.environ.get('RUN_MAIN') or os.environ.get('WERKZEUG_RUN_MAIN'): + if os.environ.get("RUN_MAIN") or os.environ.get("WERKZEUG_RUN_MAIN"): import ptvsd - ptvsd.enable_attach(address=('0.0.0.0', 8890)) + + ptvsd.enable_attach(address=("0.0.0.0", 8890)) print("Attached remote debugger to Docker container") except ImportError as exc: