diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 607b3e844..80de8c83f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,12 +61,11 @@ jobs: - name: Install dependencies run: poetry install --no-interaction -# TODO: use ruff or upgrade pylint, pylint-django, and asteroid to support String based model references. -# - name: Lint -# run: poetry run pylint ./**/*.py + - name: Lint (Ruff) + run: poetry run ruff check . - - name: Code Formatting (Black) - run: poetry run black --check . + - name: Code Formatting (Ruff) + run: poetry run ruff format --check . # Configurations required for elasticsearch. - name: Configure sysctl limits diff --git a/README.md b/README.md index b0dee3178..39d25b768 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,13 @@ for running the app # Run Python test cases in a single file that match some function/class name docker-compose run --rm web pytest /path/to/test.py -k test_some_logic # Run Python linter - docker-compose run --rm web pylint + docker-compose run --rm web ruff check . ### PYTHON FORMATTING # Format all python files - docker-compose run --rm web black . + docker-compose run --rm web ruff format --check . # Format a specific file - docker-compose run --rm web black /path/to/file.py + docker-compose run --rm ruff format --check /path/to/file.py ### JS/CSS TESTS/LINTING # We also include a helper script to execute JS tests in most of our projects diff --git a/affiliate/admin.py b/affiliate/admin.py index ab34d427a..808676263 100644 --- a/affiliate/admin.py +++ b/affiliate/admin.py @@ -31,7 +31,7 @@ class AffiliateReferralActionAdmin(TimestampedModelAdmin): list_filter = ["affiliate__name"] ordering = ["-created_on"] - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002 """Overrides base method""" return self.model.objects.select_related("affiliate") diff --git a/affiliate/api.py b/affiliate/api.py index ee5d1de09..149aa7db9 100644 --- a/affiliate/api.py +++ b/affiliate/api.py @@ -14,10 +14,7 @@ def get_affiliate_code_from_qstring(request): Returns: Optional[str]: The affiliate code (or None) """ - if request.method != "GET": - return None - affiliate_code = request.GET.get(AFFILIATE_QS_PARAM) - return affiliate_code + return request.GET.get(AFFILIATE_QS_PARAM) if request.method == "GET" else None def get_affiliate_code_from_request(request): diff --git a/affiliate/api_test.py b/affiliate/api_test.py index 6478baa09..3fad2143f 100644 --- a/affiliate/api_test.py +++ b/affiliate/api_test.py @@ -36,7 +36,7 @@ def test_get_affiliate_code_from_request(): request = RequestFactory().get("/") code = get_affiliate_code_from_request(request) assert code is None - setattr(request, "affiliate_code", affiliate_code) + setattr(request, "affiliate_code", affiliate_code) # noqa: B010 code = get_affiliate_code_from_request(request) assert code == affiliate_code @@ -62,7 +62,7 @@ def test_get_affiliate_id_from_request(): """ affiliate_code = "abc" request = RequestFactory().get("/") - setattr(request, "affiliate_code", affiliate_code) + setattr(request, "affiliate_code", affiliate_code) # noqa: B010 affiliate_id = get_affiliate_id_from_request(request) assert affiliate_id is None affiliate = AffiliateFactory.create(code=affiliate_code) diff --git a/affiliate/middleware.py b/affiliate/middleware.py index 9c413a4d6..7668fd9cc 100644 --- a/affiliate/middleware.py +++ b/affiliate/middleware.py @@ -9,9 +9,9 @@ class AffiliateMiddleware: def __init__(self, get_response): self.get_response = get_response - def __call__(self, request): + def __call__(self, request): # noqa: D102 request.affiliate_code = None - session = getattr(request, "session") + session = getattr(request, "session") # noqa: B009 if session is None: return self.get_response(request) qs_affiliate_code = get_affiliate_code_from_qstring(request) diff --git a/affiliate/migrations/0001_affiliate_initial_models.py b/affiliate/migrations/0001_affiliate_initial_models.py index 1784a6c42..a60d76c90 100644 --- a/affiliate/migrations/0001_affiliate_initial_models.py +++ b/affiliate/migrations/0001_affiliate_initial_models.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.10 on 2020-10-22 16:42 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/affiliate/models.py b/affiliate/models.py index a2ca63903..2acc931c0 100644 --- a/affiliate/models.py +++ b/affiliate/models.py @@ -12,9 +12,7 @@ class Affiliate(TimestampedModel): name = models.CharField(max_length=50, unique=True) def __str__(self): - return "Affiliate: id={}, code={}, name={}".format( - self.id, self.code, self.name - ) + return f"Affiliate: id={self.id}, code={self.code}, name={self.name}" class AffiliateReferralAction(TimestampedModel): diff --git a/authentication/api.py b/authentication/api.py index 688de2b84..20127dcef 100644 --- a/authentication/api.py +++ b/authentication/api.py @@ -2,12 +2,11 @@ from importlib import import_module from django.conf import settings -from django.contrib.auth import SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY +from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY from django.db import IntegrityError -from users.utils import is_duplicate_username_error from users.api import find_available_username - +from users.utils import is_duplicate_username_error USERNAME_COLLISION_ATTEMPTS = 10 @@ -26,7 +25,7 @@ def create_user_session(user): session = SessionStore() - session[SESSION_KEY] = user._meta.pk.value_to_string(user) + session[SESSION_KEY] = user._meta.pk.value_to_string(user) # noqa: SLF001 session[BACKEND_SESSION_KEY] = "django.contrib.auth.backends.ModelBackend" session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() @@ -51,13 +50,13 @@ def create_user_with_generated_username(serializer, initial_username): username = initial_username attempts = 0 - if len(username) < 2: + if len(username) < 2: # noqa: PLR2004 username = username + "11" while created_user is None and attempts < USERNAME_COLLISION_ATTEMPTS: try: created_user = serializer.save(username=username) - except IntegrityError as exc: + except IntegrityError as exc: # noqa: PERF203 if not is_duplicate_username_error(exc): raise username = find_available_username(initial_username) diff --git a/authentication/api_test.py b/authentication/api_test.py index 737bb7196..e97c65f4c 100644 --- a/authentication/api_test.py +++ b/authentication/api_test.py @@ -110,6 +110,6 @@ def test_create_user_exception(mocker): patched_save = mocker.patch.object( UserSerializer, "save", side_effect=ValueError("idk") ) - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 api.create_user_with_generated_username(UserSerializer(data={}), "testuser") patched_save.assert_called_once() diff --git a/authentication/exceptions.py b/authentication/exceptions.py index 6db2932eb..e103fe9b1 100644 --- a/authentication/exceptions.py +++ b/authentication/exceptions.py @@ -92,5 +92,5 @@ class UserTryAgainLaterException(AuthException): """The user should try to register again later""" -class UserMissingSocialAuthException(Exception): +class UserMissingSocialAuthException(Exception): # noqa: N818 """Raised if the user doesn't have a social auth""" diff --git a/authentication/middleware.py b/authentication/middleware.py index 4821e945e..eaa545c50 100644 --- a/authentication/middleware.py +++ b/authentication/middleware.py @@ -1,7 +1,7 @@ """Authentication middleware""" -from django.shortcuts import redirect from urllib.parse import quote +from django.shortcuts import redirect from social_core.exceptions import SocialAuthBaseException from social_django.middleware import SocialAuthExceptionMiddleware @@ -19,17 +19,17 @@ def process_exception(self, request, exception): """ strategy = getattr(request, "social_strategy", None) if strategy is None or self.raise_exception(request, exception): - return + return # noqa: RET502 - if isinstance(exception, SocialAuthBaseException): + if isinstance(exception, SocialAuthBaseException): # noqa: RET503 backend = getattr(request, "backend", None) backend_name = getattr(backend, "name", "unknown-backend") message = self.get_message(request, exception) url = self.get_redirect_uri(request, exception) - if url: - url += ("?" in url and "&" or "?") + "message={0}&backend={1}".format( + if url: # noqa: RET503 + url += ("?" in url and "&" or "?") + "message={0}&backend={1}".format( # noqa: UP030 quote(message), backend_name ) return redirect(url) diff --git a/authentication/middleware_test.py b/authentication/middleware_test.py index 34b682872..6c44d5d90 100644 --- a/authentication/middleware_test.py +++ b/authentication/middleware_test.py @@ -1,7 +1,8 @@ """Tests for auth middleware""" +from urllib.parse import quote + from django.contrib.sessions.middleware import SessionMiddleware from django.shortcuts import reverse -from urllib.parse import quote from rest_framework import status from social_core.exceptions import AuthAlreadyAssociated from social_django.utils import load_backend, load_strategy diff --git a/authentication/pipeline/compliance.py b/authentication/pipeline/compliance.py index 179674bad..3e81f6d27 100644 --- a/authentication/pipeline/compliance.py +++ b/authentication/pipeline/compliance.py @@ -12,13 +12,15 @@ ) from compliance import api - log = logging.getLogger() def verify_exports_compliance( - strategy, backend, user=None, **kwargs -): # pylint: disable=unused-argument + strategy, # noqa: ARG001 + backend, + user=None, + **kwargs, # noqa: ARG001 +): """ Verify that the user is allowed by exports compliance @@ -37,7 +39,7 @@ def verify_exports_compliance( try: export_inquiry = api.verify_user_with_exports(user) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # hard failure to request the exports API, log an error but don't let the user proceed log.exception("Unable to verify exports compliance") raise UserTryAgainLaterException(backend) from exc @@ -78,12 +80,12 @@ def verify_exports_compliance( [settings.ADMIN_EMAIL], connection=connection, ) - except Exception: # pylint: disable=broad-except + except Exception: log.exception( "Exception sending email to support regarding export compliance check failure" ) raise UserExportBlockedException(backend, export_inquiry.reason_code) if export_inquiry.is_unknown: - raise AuthException("Unable to authenticate, please contact support") + raise AuthException("Unable to authenticate, please contact support") # noqa: EM101 return {} diff --git a/authentication/pipeline/compliance_test.py b/authentication/pipeline/compliance_test.py index 9292c2dd1..d2b6d0aee 100644 --- a/authentication/pipeline/compliance_test.py +++ b/authentication/pipeline/compliance_test.py @@ -9,7 +9,6 @@ from authentication.pipeline import compliance from compliance.factories import ExportsInquiryLogFactory - pytestmark = pytest.mark.django_db @@ -22,17 +21,17 @@ def test_verify_exports_compliance_disabled(mocker): @pytest.mark.parametrize( - "is_active, inquiry_exists, should_verify", + "is_active, inquiry_exists, should_verify", # noqa: PT006 [ - [True, True, False], - [True, False, True], - [False, True, True], - [False, False, True], + [True, True, False], # noqa: PT007 + [True, False, True], # noqa: PT007 + [False, True, True], # noqa: PT007 + [False, False, True], # noqa: PT007 ], ) -def test_verify_exports_compliance_user_active( +def test_verify_exports_compliance_user_active( # noqa: PLR0913 mailoutbox, mocker, user, is_active, inquiry_exists, should_verify -): # pylint: disable=too-many-arguments +): """Assert that the user is verified only if they already haven't been""" user.is_active = is_active if inquiry_exists: diff --git a/authentication/pipeline/user.py b/authentication/pipeline/user.py index f27a29d04..0d31273b0 100644 --- a/authentication/pipeline/user.py +++ b/authentication/pipeline/user.py @@ -24,12 +24,12 @@ ) from authentication.utils import SocialAuthState, is_user_email_blocked from compliance import api as compliance_api -from courseware import api as courseware_api, tasks as courseware_tasks +from courseware import api as courseware_api +from courseware import tasks as courseware_tasks from hubspot_xpro.task_helpers import sync_hubspot_user from users.serializers import ProfileSerializer, UserSerializer from users.utils import usernameify - log = logging.getLogger() User = get_user_model() @@ -37,12 +37,14 @@ CREATE_COURSEWARE_USER_RETRY_DELAY = 60 NAME_MIN_LENGTH = 2 -# pylint: disable=keyword-arg-before-vararg - def validate_email_auth_request( - strategy, backend, user=None, *args, **kwargs -): # pylint: disable=unused-argument + strategy, # noqa: ARG001 + backend, + user=None, + *args, # noqa: ARG001 + **kwargs, # noqa: ARG001 +): """ Validates an auth request for email @@ -62,8 +64,12 @@ def validate_email_auth_request( def get_username( - strategy, backend, user=None, *args, **kwargs -): # pylint: disable=unused-argument + strategy, + backend, # noqa: ARG001 + user=None, + *args, # noqa: ARG001 + **kwargs, # noqa: ARG001 +): """ Gets the username for a user @@ -77,8 +83,14 @@ def get_username( @partial def create_user_via_email( - strategy, backend, user=None, flow=None, current_partial=None, *args, **kwargs -): # pylint: disable=too-many-arguments,unused-argument + strategy, + backend, + user=None, + flow=None, + current_partial=None, + *args, # noqa: ARG001 + **kwargs, +): """ Creates a new user if needed and sets the password and name. Args: @@ -131,10 +143,10 @@ def create_user_via_email( try: created_user = create_user_with_generated_username(serializer, username) if created_user is None: - raise IntegrityError( - "Failed to create User with generated username ({})".format(username) + raise IntegrityError( # noqa: TRY301 + "Failed to create User with generated username ({})".format(username) # noqa: EM103, UP032 ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 raise UserCreationFailedException(backend, current_partial) from exc return {"is_new": True, "user": created_user, "username": created_user.username} @@ -142,8 +154,14 @@ def create_user_via_email( @partial def create_profile( - strategy, backend, user=None, flow=None, current_partial=None, *args, **kwargs -): # pylint: disable=too-many-arguments,unused-argument + strategy, + backend, + user=None, + flow=None, # noqa: ARG001 + current_partial=None, + *args, # noqa: ARG001 + **kwargs, # noqa: ARG001 +): """ Creates a new profile for the user Args: @@ -173,8 +191,14 @@ def create_profile( @partial def validate_email( - strategy, backend, user=None, flow=None, current_partial=None, *args, **kwargs -): # pylint: disable=unused-argument + strategy, + backend, + user=None, # noqa: ARG001 + flow=None, # noqa: ARG001 + current_partial=None, + *args, # noqa: ARG001 + **kwargs, # noqa: ARG001 +): """ Validates a user's email for register @@ -190,7 +214,7 @@ def validate_email( """ data = strategy.request_data() authentication_flow = data.get("flow") - if authentication_flow == SocialAuthState.FLOW_REGISTER and "email" in data: + if authentication_flow == SocialAuthState.FLOW_REGISTER and "email" in data: # noqa: SIM102 if is_user_email_blocked(data["email"]): raise EmailBlockedException(backend, current_partial) return {} @@ -198,8 +222,14 @@ def validate_email( @partial def validate_password( - strategy, backend, user=None, flow=None, current_partial=None, *args, **kwargs -): # pylint: disable=unused-argument + strategy, + backend, + user=None, + flow=None, + current_partial=None, + *args, # noqa: ARG001 + **kwargs, # noqa: ARG001 +): """ Validates a user's password for login @@ -231,7 +261,7 @@ def validate_password( return {} -def forbid_hijack(strategy, backend, **kwargs): # pylint: disable=unused-argument +def forbid_hijack(strategy, backend, **kwargs): # noqa: ARG001 """ Forbid an admin user from trying to login/register while hijacking another user @@ -241,13 +271,17 @@ def forbid_hijack(strategy, backend, **kwargs): # pylint: disable=unused-argume """ # As first step in pipeline, stop a hijacking admin from going any further if bool(strategy.session_get("hijack_history")): - raise AuthException("You are hijacking another user, don't try to login again") + raise AuthException("You are hijacking another user, don't try to login again") # noqa: EM101 return {} def activate_user( - strategy, backend, user=None, is_new=False, **kwargs -): # pylint: disable=unused-argument + strategy, # noqa: ARG001 + backend, # noqa: ARG001 + user=None, + is_new=False, # noqa: ARG001, FBT002 + **kwargs, # noqa: ARG001 +): """ Activate the user's account if they passed export controls @@ -272,8 +306,12 @@ def activate_user( def create_courseware_user( - strategy, backend, user=None, is_new=False, **kwargs -): # pylint: disable=unused-argument + strategy, # noqa: ARG001 + backend, # noqa: ARG001 + user=None, + is_new=False, # noqa: FBT002 + **kwargs, # noqa: ARG001 +): """ Create a user in the courseware, deferring a retry via celery if it fails @@ -286,7 +324,7 @@ def create_courseware_user( try: courseware_api.create_user(user) - except Exception: # pylint: disable=broad-except + except Exception: log.exception("Error creating courseware user records on User create") # try again later courseware_tasks.create_user_from_id.apply_async( @@ -319,14 +357,18 @@ def send_user_to_hubspot(request, **kwargs): url = f"https://forms.hubspot.com/uploads/form/v2/{portal_id}/{form_id}?&" - requests.post(url=url, data=data, headers=headers) + requests.post(url=url, data=data, headers=headers) # noqa: S113 return {} def sync_user_to_hubspot( - strategy, backend, user=None, is_new=False, **kwargs -): # pylint: disable=unused-argument + strategy, # noqa: ARG001 + backend, # noqa: ARG001 + user=None, + is_new=False, # noqa: ARG001, FBT002 + **kwargs, # noqa: ARG001 +): """ Sync the user's latest profile data with hubspot on login """ diff --git a/authentication/pipeline/user_test.py b/authentication/pipeline/user_test.py index 1da80d110..0771848af 100644 --- a/authentication/pipeline/user_test.py +++ b/authentication/pipeline/user_test.py @@ -1,5 +1,4 @@ """Tests of user pipeline actions""" -# pylint: disable=redefined-outer-name import pytest from django.contrib.sessions.middleware import SessionMiddleware @@ -36,7 +35,7 @@ def mock_email_backend(mocker, backend_settings): """Fixture that returns a fake EmailAuth backend object""" backend = mocker.Mock() backend.name = "email" - backend.setting.side_effect = lambda key, default, **kwargs: backend_settings.get( + backend.setting.side_effect = lambda key, default, **kwargs: backend_settings.get( # noqa: ARG005 key, default ) return backend @@ -85,7 +84,8 @@ def validate_email_auth_request_not_email_backend(mocker): @pytest.mark.parametrize( - "has_user,expected", [(True, {"flow": SocialAuthState.FLOW_LOGIN}), (False, {})] + "has_user,expected", # noqa: PT006 + [(True, {"flow": SocialAuthState.FLOW_LOGIN}), (False, {})], ) @pytest.mark.django_db def test_validate_email_auth_request(rf, has_user, expected, mocker): @@ -149,7 +149,7 @@ def test_user_password_not_email_backend(mocker): @pytest.mark.parametrize("user_password", ["abc123", "def456"]) def test_user_password_login(rf, user, user_password, mocker): """Tests that user_password works for login case""" - request_password = "abc123" + request_password = "abc123" # noqa: S105 user.set_password(user_password) user.save() request = rf.post( @@ -229,7 +229,7 @@ def test_user_password_not_exists(rf, mocker): @pytest.mark.parametrize( - "backend_name,flow", + "backend_name,flow", # noqa: PT006 [ ("notemail", None), ("notemail", SocialAuthState.FLOW_REGISTER), @@ -276,7 +276,7 @@ def test_create_user_via_email(mocker, mock_email_backend, mock_create_user_stra response = user_actions.create_user_via_email( mock_create_user_strategy, mock_email_backend, - details=dict(email=email), + details=dict(email=email), # noqa: C408 pipeline_index=0, flow=SocialAuthState.FLOW_REGISTER, ) @@ -333,7 +333,7 @@ def test_create_user_via_email_with_shorter_name(mocker, mock_email_backend): user_actions.create_user_via_email( mock_strategy, mock_email_backend, - details=dict(email="test@example.com"), + details=dict(email="test@example.com"), # noqa: C408 pipeline_index=0, flow=SocialAuthState.FLOW_REGISTER, ) @@ -372,14 +372,14 @@ def test_create_user_via_email_with_email_case_insensitive_existing_user( mock_email_backend, pipeline_index=0, flow=SocialAuthState.FLOW_REGISTER, - details=dict(email=email), + details=dict(email=email), # noqa: C408 ) @pytest.mark.django_db @pytest.mark.parametrize( - "create_user_return_val,create_user_exception", - [[None, None], [UserFactory.build(), ValueError("bad value")]], + "create_user_return_val,create_user_exception", # noqa: PT006 + [[None, None], [UserFactory.build(), ValueError("bad value")]], # noqa: PT007 ) def test_create_user_via_email_create_fail( mocker, @@ -398,7 +398,7 @@ def test_create_user_via_email_create_fail( user_actions.create_user_via_email( mock_create_user_strategy, mock_email_backend, - details=dict(email="someuser@example.com"), + details=dict(email="someuser@example.com"), # noqa: C408 pipeline_index=0, flow=SocialAuthState.FLOW_REGISTER, ) @@ -422,7 +422,7 @@ def test_create_user_via_email_affiliate( user_actions.create_user_via_email( mock_create_user_strategy, mock_email_backend, - details=dict(email="someuser@example.com"), + details=dict(email="someuser@example.com"), # noqa: C408 pipeline_index=0, flow=SocialAuthState.FLOW_REGISTER, ) @@ -438,7 +438,7 @@ def test_create_user_via_email_affiliate( @pytest.mark.parametrize("hubspot_key", [None, "fake-key"]) def test_create_profile( mock_email_backend, mock_create_profile_strategy, hubspot_key, settings, mocker -): # pylint:disable=too-many-arguments +): """ Tests that create_profile creates a profile """ @@ -495,7 +495,7 @@ def test_forbid_hijack(mocker, hijacked): kwargs = {"flow": SocialAuthState.FLOW_LOGIN} if hijacked: - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 user_actions.forbid_hijack(*args, **kwargs) else: assert user_actions.forbid_hijack(*args, **kwargs) == {} @@ -533,18 +533,33 @@ def test_send_user_to_hubspot(mocker, settings): @pytest.mark.parametrize("is_active", [True, False]) @pytest.mark.parametrize("is_new", [True, False]) @pytest.mark.parametrize( - "is_enabled, has_inquiry, computed_result, expected", + "is_enabled, has_inquiry, computed_result, expected", # noqa: PT006 [ - [True, True, RESULT_SUCCESS, True], # feature enabled, result is success - [True, True, RESULT_DENIED, False], # feature enabled, result is denied - [True, True, RESULT_UNKNOWN, False], # feature enabled, result is unknown - [False, False, None, True], # feature disabled - [True, False, None, False], # feature enabled, no result + [ # noqa: PT007 + True, + True, + RESULT_SUCCESS, + True, + ], # feature enabled, result is success + [ # noqa: PT007 + True, + True, + RESULT_DENIED, + False, + ], # feature enabled, result is denied + [ # noqa: PT007 + True, + True, + RESULT_UNKNOWN, + False, + ], # feature enabled, result is unknown + [False, False, None, True], # feature disabled # noqa: PT007 + [True, False, None, False], # feature enabled, no result # noqa: PT007 ], ) -def test_activate_user( +def test_activate_user( # noqa: PLR0913 mocker, user, is_active, is_new, is_enabled, has_inquiry, computed_result, expected -): # pylint: disable=too-many-arguments +): """Test that activate_user takes the correct action""" user.is_active = is_active if has_inquiry: @@ -564,17 +579,17 @@ def test_activate_user( @pytest.mark.parametrize("raises_error", [True, False]) @pytest.mark.parametrize( - "is_active, is_new, creates_records", + "is_active, is_new, creates_records", # noqa: PT006 [ - [True, True, True], - [True, False, False], - [False, True, False], - [False, False, False], + [True, True, True], # noqa: PT007 + [True, False, False], # noqa: PT007 + [False, True, False], # noqa: PT007 + [False, False, False], # noqa: PT007 ], ) -def test_create_courseware_user( +def test_create_courseware_user( # noqa: PLR0913 mocker, user, raises_error, is_active, is_new, creates_records -): # pylint: disable=too-many-arguments +): """Test that activate_user takes the correct action""" user.is_active = is_active @@ -606,10 +621,10 @@ def test_create_courseware_user( @pytest.mark.parametrize( - "backend_name,flow,data", + "backend_name,flow,data", # noqa: PT006 [ ("notemail", SocialAuthState.FLOW_REGISTER, {}), - ("notemail", SocialAuthState.FLOW_LOGIN, dict(email="test@example.com")), + ("notemail", SocialAuthState.FLOW_LOGIN, dict(email="test@example.com")), # noqa: C408 ], ) def test_validate_email_backend(mocker, backend_name, flow, data): diff --git a/authentication/serializers.py b/authentication/serializers.py index fdf1dfde8..a7cea5712 100644 --- a/authentication/serializers.py +++ b/authentication/serializers.py @@ -3,31 +3,31 @@ from django.contrib.auth import get_user_model from django.http import HttpResponseRedirect -from social_django.views import _do_login as login +from rest_framework import serializers from social_core.backends.email import EmailAuth -from social_core.exceptions import InvalidEmail, AuthException, AuthAlreadyAssociated +from social_core.exceptions import AuthAlreadyAssociated, AuthException, InvalidEmail from social_core.utils import ( - user_is_authenticated, - user_is_active, partial_pipeline_data, sanitize_redirect, + user_is_active, + user_is_authenticated, ) -from rest_framework import serializers +from social_django.views import _do_login as login from authentication.exceptions import ( + EmailBlockedException, InvalidPasswordException, - RequirePasswordException, RequirePasswordAndPersonalInfoException, + RequirePasswordException, + RequireProfileException, RequireProviderException, RequireRegistrationException, - RequireProfileException, UserExportBlockedException, UserTryAgainLaterException, - EmailBlockedException, ) from authentication.utils import SocialAuthState -PARTIAL_PIPELINE_TOKEN_KEY = "partial_pipeline_token" +PARTIAL_PIPELINE_TOKEN_KEY = "partial_pipeline_token" # noqa: S105 log = logging.getLogger() @@ -63,8 +63,8 @@ def _save_next(self, data): backend = self.context["backend"] # Check and sanitize a user-defined GET/POST next field value redirect_uri = data["next"] - if backend.setting("SANITIZE_REDIRECTS", True): - allowed_hosts = backend.setting("ALLOWED_REDIRECT_HOSTS", []) + [ + if backend.setting("SANITIZE_REDIRECTS", True): # noqa: FBT003 + allowed_hosts = backend.setting("ALLOWED_REDIRECT_HOSTS", []) + [ # noqa: RUF005 backend.strategy.request_host() ] redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri) @@ -72,8 +72,7 @@ def _save_next(self, data): "next", redirect_uri or backend.setting("LOGIN_REDIRECT_URL") ) - # pylint: disable=too-many-return-statements - def _authenticate(self, flow): + def _authenticate(self, flow): # noqa: PLR0911 """Authenticate the current request""" request = self.context["request"] strategy = self.context["strategy"] @@ -146,7 +145,7 @@ def _authenticate(self, flow): SocialAuthState.STATE_ERROR, errors=["Unexpected authentication result"] ) - def save(self, **kwargs): + def save(self, **kwargs): # noqa: C901 """'Save' the auth request""" try: result = super().save(**kwargs) @@ -247,7 +246,7 @@ class LoginPasswordSerializer(SocialAuthSerializer): password = serializers.CharField(min_length=8, write_only=True) - def create(self, validated_data): + def create(self, validated_data): # noqa: ARG002 """Try to 'save' the request""" try: result = super()._authenticate(SocialAuthState.FLOW_LOGIN) @@ -266,14 +265,14 @@ class RegisterEmailSerializer(SocialAuthSerializer): email = serializers.EmailField(write_only=True, required=False) next = serializers.CharField(write_only=True, required=False) - def validate(self, attrs): + def validate(self, attrs): # noqa: D102 token = (attrs.get("partial", {}) or {}).get("token", None) email = attrs.get("email", None) if not email and not token: - raise serializers.ValidationError("One of 'partial' or 'email' is required") + raise serializers.ValidationError("One of 'partial' or 'email' is required") # noqa: EM101 if email and token: - raise serializers.ValidationError("Pass only one of 'partial' or 'email'") + raise serializers.ValidationError("Pass only one of 'partial' or 'email'") # noqa: EM101 return attrs @@ -307,7 +306,7 @@ class RegisterConfirmSerializer(SocialAuthSerializer): partial_token = serializers.CharField(source="get_partial_token") verification_code = serializers.CharField(write_only=True) - def create(self, validated_data): + def create(self, validated_data): # noqa: ARG002 """Try to 'save' the request""" return super()._authenticate(SocialAuthState.FLOW_REGISTER) @@ -318,7 +317,7 @@ class RegisterDetailsSerializer(SocialAuthSerializer): password = serializers.CharField(min_length=8, write_only=True) name = serializers.CharField(write_only=True) - def create(self, validated_data): + def create(self, validated_data): # noqa: ARG002 """Try to 'save' the request""" return super()._authenticate(SocialAuthState.FLOW_REGISTER) @@ -347,6 +346,6 @@ class RegisterExtraDetailsSerializer(SocialAuthSerializer): write_only=True, allow_blank=True, required=False ) - def create(self, validated_data): + def create(self, validated_data): # noqa: ARG002 """Try to 'save' the request""" return super()._authenticate(SocialAuthState.FLOW_REGISTER) diff --git a/authentication/serializers_test.py b/authentication/serializers_test.py index ee3f8a67e..05ccabaad 100644 --- a/authentication/serializers_test.py +++ b/authentication/serializers_test.py @@ -2,7 +2,7 @@ import pytest from rest_framework.serializers import ValidationError from social_core.backends.email import EmailAuth -from social_core.exceptions import InvalidEmail, AuthException +from social_core.exceptions import AuthException, InvalidEmail from authentication.serializers import RegisterEmailSerializer from authentication.utils import SocialAuthState @@ -14,8 +14,8 @@ @pytest.mark.parametrize( - "side_effect,result", - ( + "side_effect,result", # noqa: PT006 + ( # noqa: PT007 ( AuthException(None, "message"), SocialAuthState(SocialAuthState.STATE_ERROR, errors=["message"]), @@ -41,16 +41,14 @@ def test_social_auth_serializer_error(mocker, side_effect, result): "request": mocker.Mock(), }, ) - assert serializer.is_valid() is True, "Received errors: {}".format( - serializer.errors - ) + assert serializer.is_valid() is True, f"Received errors: {serializer.errors}" assert isinstance(serializer.save(), SocialAuthState) assert serializer.data == RegisterEmailSerializer(result).data @pytest.mark.parametrize( - "data,raises,message", - ( + "data,raises,message", # noqa: PT006 + ( # noqa: PT007 ( {"email": None, "partial": None}, ValidationError, diff --git a/authentication/strategy.py b/authentication/strategy.py index 7936058f6..0968220f2 100644 --- a/authentication/strategy.py +++ b/authentication/strategy.py @@ -8,10 +8,10 @@ class DjangoRestFrameworkStrategy(DjangoStrategy): def __init__(self, storage, drf_request=None, tpl=None): self.drf_request = drf_request # pass the original django request to DjangoStrategy - request = drf_request._request # pylint: disable=protected-access + request = drf_request._request # noqa: SLF001 super().__init__(storage, request=request, tpl=tpl) - def request_data(self, merge=True): + def request_data(self, merge=True): # noqa: ARG002, FBT002 """Returns the request data""" if not self.drf_request: return {} diff --git a/authentication/strategy_test.py b/authentication/strategy_test.py index 36caae06e..2f76d0408 100644 --- a/authentication/strategy_test.py +++ b/authentication/strategy_test.py @@ -10,7 +10,7 @@ def test_strategy_init(mocker): drf_request = mocker.Mock() strategy = load_drf_strategy(request=drf_request) assert strategy.drf_request == drf_request - assert strategy.request == drf_request._request # pylint: disable=protected-access + assert strategy.request == drf_request._request # noqa: SLF001 def test_strategy_request_data(mocker): diff --git a/authentication/urls.py b/authentication/urls.py index 467d04907..35d267615 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -1,19 +1,19 @@ """URL configurations for authentication""" from django.urls import path from django.urls.conf import include + from authentication.views import ( + CustomLogoutView, LoginEmailView, LoginPasswordView, - RegisterEmailView, RegisterConfirmView, RegisterDetailsView, + RegisterEmailView, RegisterExtraDetailsView, get_social_auth_types, - CustomLogoutView, well_known_openid_configuration, ) - urlpatterns = [ path("api/login/email/", LoginEmailView.as_view(), name="psa-login-email"), path("api/login/password/", LoginPasswordView.as_view(), name="psa-login-password"), diff --git a/authentication/utils.py b/authentication/utils.py index 5cb7580f4..bb702bed0 100644 --- a/authentication/utils.py +++ b/authentication/utils.py @@ -1,11 +1,13 @@ """Authentication utils""" import hashlib + from social_core.utils import get_strategy from social_django.utils import STORAGE + from users.models import BlockList -class SocialAuthState: # pylint: disable=too-many-instance-attributes +class SocialAuthState: """Social auth state""" FLOW_REGISTER = "register" @@ -13,7 +15,7 @@ class SocialAuthState: # pylint: disable=too-many-instance-attributes # login states STATE_LOGIN_EMAIL = "login/email" - STATE_LOGIN_PASSWORD = "login/password" + STATE_LOGIN_PASSWORD = "login/password" # noqa: S105 STATE_LOGIN_PROVIDER = "login/provider" # registration states @@ -34,7 +36,7 @@ class SocialAuthState: # pylint: disable=too-many-instance-attributes STATE_INVALID_LINK = "invalid-link" STATE_EXISTING_ACCOUNT = "existing-account" - def __init__( + def __init__( # noqa: PLR0913 self, state, *, @@ -45,7 +47,7 @@ def __init__( field_errors=None, redirect_url=None, user=None, - ): # pylint: disable=too-many-arguments + ): self.state = state self.partial = partial self.flow = flow @@ -69,7 +71,7 @@ def load_drf_strategy(request=None): def get_md5_hash(value): """Returns the md5 hash object for the given value""" - return hashlib.md5(value.lower().encode("utf-8")) + return hashlib.md5(value.lower().encode("utf-8")) # noqa: S324 def is_user_email_blocked(email): @@ -87,11 +89,7 @@ def block_user_email(email): hashed_email=hash_object.hexdigest() ) if created: - msg = "Email {email} is added to the blocklist of MIT xPRO.".format( - email=email - ) + msg = f"Email {email} is added to the blocklist of MIT xPRO." else: - msg = "Email {email} is already marked blocked for MIT xPRO.".format( - email=email - ) + msg = f"Email {email} is already marked blocked for MIT xPRO." return msg diff --git a/authentication/views.py b/authentication/views.py index 0162de57d..0aef07d77 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,27 +1,27 @@ """Authentication views""" -from urllib.parse import quote, urlparse, urlencode, urljoin +from urllib.parse import quote, urlencode, urljoin, urlparse import requests from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.views import LogoutView from django.shortcuts import render, reverse -from social_core.backends.email import EmailAuth -from social_django.models import UserSocialAuth -from social_django.utils import load_backend from rest_framework import status -from rest_framework.views import APIView -from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes, renderer_classes from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework.views import APIView +from social_core.backends.email import EmailAuth +from social_django.models import UserSocialAuth +from social_django.utils import load_backend from authentication.serializers import ( LoginEmailSerializer, LoginPasswordSerializer, - RegisterEmailSerializer, RegisterConfirmSerializer, RegisterDetailsSerializer, + RegisterEmailSerializer, RegisterExtraDetailsSerializer, ) from authentication.utils import load_drf_strategy @@ -37,7 +37,7 @@ class SocialAuthAPIView(APIView): def get_serializer_cls(self): # pragma: no cover """Return the serializer cls""" - raise NotImplementedError("get_serializer_cls must be implemented") + raise NotImplementedError("get_serializer_cls must be implemented") # noqa: EM101 def post(self, request): """Processes a request""" @@ -86,7 +86,7 @@ def post(self, request): if bool(request.session.get("hijack_history")): return Response(status=status.HTTP_403_FORBIDDEN) if settings.RECAPTCHA_SITE_KEY: - r = requests.post( + r = requests.post( # noqa: S113 "https://www.google.com/recaptcha/api/siteverify?secret={key}&response={captcha}".format( key=quote(settings.RECAPTCHA_SECRET_KEY), captcha=quote(request.data["recaptcha"]), @@ -134,7 +134,7 @@ def get_social_auth_types(request): return Response(data=social_auths, status=status.HTTP_200_OK) -def confirmation_sent(request, **kwargs): # pylint: disable=unused-argument +def confirmation_sent(request, **kwargs): # noqa: ARG001 """The confirmation of an email being sent""" return render(request, "confirmation_sent.html") @@ -142,7 +142,7 @@ def confirmation_sent(request, **kwargs): # pylint: disable=unused-argument class CustomLogoutView(LogoutView): """Custom view to modify base functionality in django.contrib.auth.views.LogoutView""" - def get_next_page(self): + def get_next_page(self): # noqa: D102 next_page = super().get_next_page() if next_page in (self.next_page, self.request.path): @@ -156,7 +156,7 @@ def get_next_page(self): @api_view(["GET"]) @renderer_classes([JSONRenderer]) @permission_classes([]) -def well_known_openid_configuration(request): +def well_known_openid_configuration(request): # noqa: ARG001 """View for openid configuration""" # See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig # NOTE: this is intentionally incomplete because we don't fully support OpenID diff --git a/authentication/views_test.py b/authentication/views_test.py index 53a1970b2..d8d0595fb 100644 --- a/authentication/views_test.py +++ b/authentication/views_test.py @@ -1,28 +1,29 @@ """Tests for authentication views""" -# pylint: disable=redefined-outer-name -from contextlib import contextmanager, ExitStack +from contextlib import ExitStack, contextmanager from unittest.mock import patch +import factory +import pytest +import responses from django.conf import settings from django.contrib.auth import get_user, get_user_model from django.core import mail from django.db import transaction -from django.urls import reverse from django.test import Client, override_settings -import factory +from django.urls import reverse from faker import Faker -from hypothesis import settings as hypothesis_settings, strategies as st, Verbosity +from hypothesis import Verbosity +from hypothesis import settings as hypothesis_settings +from hypothesis import strategies as st +from hypothesis.extra.django import TestCase as HTestCase from hypothesis.stateful import ( + Bundle, + HealthCheck, + RuleBasedStateMachine, consumes, precondition, rule, - Bundle, - RuleBasedStateMachine, - HealthCheck, ) -from hypothesis.extra.django import TestCase as HTestCase -import pytest -import responses from rest_framework import status from social_core.backends.email import EmailAuth @@ -35,8 +36,8 @@ mock_cybersource_wsdl, mock_cybersource_wsdl_operation, ) +from mitxpro.test_utils import MockResponse, any_instance_of from users.factories import UserFactory, UserSocialAuthFactory -from mitxpro.test_utils import any_instance_of, MockResponse pytestmark = [pytest.mark.django_db] @@ -47,8 +48,6 @@ fake = Faker() -# pylint: disable=too-many-public-methods - @pytest.fixture def email_user(user): @@ -57,15 +56,14 @@ def email_user(user): return user -# pylint: disable=too-many-arguments -def assert_api_call( +def assert_api_call( # noqa: PLR0913 client, url, payload, expected, - expect_authenticated=False, + expect_authenticated=False, # noqa: FBT002 expect_status=status.HTTP_200_OK, - use_defaults=True, + use_defaults=True, # noqa: FBT002 ): """Run the API call and perform basic assertions""" assert bool(get_user(client).is_authenticated) is False @@ -92,10 +90,10 @@ def assert_api_call( return actual -@pytest.fixture() +@pytest.fixture def mock_email_send(mocker): """Mock the email send API""" - yield mocker.patch("mail.verification_api.send_verification_email") + return mocker.patch("mail.verification_api.send_verification_email") @contextmanager @@ -128,8 +126,6 @@ class AuthStateMachine(RuleBasedStateMachine): methods to define transitions into and (optionally) out of that state. """ - # pylint: disable=too-many-instance-attributes - ConfirmationSentAuthStates = Bundle("confirmation-sent") ConfirmationRedeemedAuthStates = Bundle("confirmation-redeemed") RegisterExtraDetailsAuthStates = Bundle("register-details-extra") @@ -167,7 +163,7 @@ def __init__(self): # shared data self.email = fake.email() self.user = None - self.password = "password123" + self.password = "password123" # noqa: S105 # track whether we've hit an action that starts a flow or not self.flow_started = False @@ -183,7 +179,7 @@ def teardown(self): self.courseware_tasks_patcher.stop() # end the transaction with a rollback to cleanup any state - transaction.set_rollback(True) + transaction.set_rollback(True) # noqa: FBT003, RUF100 self.atomic.__exit__(None, None, None) def create_existing_user(self): @@ -337,12 +333,14 @@ def login_email_exists(self): auth_state=consumes(RegisterExtraDetailsAuthStates), ) @precondition(lambda self: self.flow_started) - def login_email_abandoned(self, auth_state): # pylint: disable=unused-argument + def login_email_abandoned(self, auth_state): """Login with a user that abandoned the register flow""" # NOTE: This works by "consuming" an extra details auth state, # but discarding the state and starting a new login. # It then re-targets the new state into the extra details again. - auth_state = None # assign None to ensure no accidental usage here + auth_state = ( # noqa: F841 + None # assign None to ensure no accidental usage here + ) return assert_api_call( self.client, @@ -735,7 +733,7 @@ def test_new_register_no_session_partial(client): "state": SocialAuthState.STATE_REGISTER_CONFIRM_SENT, }, ) - assert PARTIAL_PIPELINE_TOKEN_KEY not in client.session.keys() + assert PARTIAL_PIPELINE_TOKEN_KEY not in client.session.keys() # noqa: SIM118 def test_login_email_error(client, mocker): diff --git a/b2b_ecommerce/admin_test.py b/b2b_ecommerce/admin_test.py index 5e48ecc91..fcf52a94c 100644 --- a/b2b_ecommerce/admin_test.py +++ b/b2b_ecommerce/admin_test.py @@ -5,7 +5,6 @@ from b2b_ecommerce.factories import B2BCouponFactory, B2BOrderFactory from b2b_ecommerce.models import B2BCouponAudit, B2BOrderAudit - pytestmark = pytest.mark.django_db diff --git a/b2b_ecommerce/api.py b/b2b_ecommerce/api.py index 4a0a83e57..0bc16306d 100644 --- a/b2b_ecommerce/api.py +++ b/b2b_ecommerce/api.py @@ -167,7 +167,7 @@ def determine_price_and_discount(*, product_version, discount_code, num_seats): coupon_code=discount_code, product_id=product_version.product.id ) except B2BCoupon.DoesNotExist as exc: - raise ValidationError("Invalid coupon code") from exc + raise ValidationError("Invalid coupon code") from exc # noqa: EM101 else: coupon = None diff --git a/b2b_ecommerce/api_test.py b/b2b_ecommerce/api_test.py index 0cddc3524..77819d310 100644 --- a/b2b_ecommerce/api_test.py +++ b/b2b_ecommerce/api_test.py @@ -18,7 +18,6 @@ from ecommerce.models import CouponPaymentVersion from mitxpro.utils import dict_without_keys, now_in_utc - FAKE = faker.Factory.create() @@ -31,7 +30,7 @@ @pytest.fixture(autouse=True) -def cybersource_settings(settings): +def cybersource_settings(settings): # noqa: PT004 """ Set cybersource settings """ @@ -88,7 +87,7 @@ def test_signed_payload(mocker, contract_number): "item_0_code": "enrollment_code", "item_0_name": f"Enrollment codes for {product_version.description}"[:254], "item_0_quantity": order.num_seats, - "item_0_sku": f"enrollment_code-{str(product.content_type)}-{product.content_object.id}", + "item_0_sku": f"enrollment_code-{str(product.content_type)}-{product.content_object.id}", # noqa: RUF010 "item_0_tax_amount": "0", "item_0_unit_price": str(total_price), "line_item_count": 1, @@ -108,7 +107,7 @@ def test_signed_payload(mocker, contract_number): @pytest.mark.parametrize( - "contract_number, b2b_coupon_code", + "contract_number, b2b_coupon_code", # noqa: PT006 [ ("contract_number", "code"), ("contract_number", None), @@ -158,7 +157,7 @@ def test_complete_b2b_order(mocker, contract_number, b2b_coupon_code): @pytest.mark.parametrize( - "order_status, decision", + "order_status, decision", # noqa: PT006 [ (B2BOrder.FAILED, "ERROR"), (B2BOrder.FULFILLED, "ERROR"), @@ -209,7 +208,7 @@ def test_ignore_duplicate_cancel(): assert B2BOrder.objects.get(id=order.id).status == B2BOrder.FAILED -def test_order_fulfilled(mocker): # pylint:disable=too-many-arguments +def test_order_fulfilled(mocker): """ Test the happy case """ diff --git a/b2b_ecommerce/apps.py b/b2b_ecommerce/apps.py index d5b2e5464..9bb3ae721 100644 --- a/b2b_ecommerce/apps.py +++ b/b2b_ecommerce/apps.py @@ -2,11 +2,11 @@ from django.apps import AppConfig -class B2B_EcommerceConfig(AppConfig): +class B2BEcommerceConfig(AppConfig): """AppConfig for B2B_Ecommerce""" name = "b2b_ecommerce" def ready(self): """Application is ready""" - import b2b_ecommerce.signals # pylint:disable=unused-import + import b2b_ecommerce.signals # noqa: F401 diff --git a/b2b_ecommerce/migrations/0001_initial.py b/b2b_ecommerce/migrations/0001_initial.py index 9e992e47a..494b4c430 100644 --- a/b2b_ecommerce/migrations/0001_initial.py +++ b/b2b_ecommerce/migrations/0001_initial.py @@ -1,13 +1,12 @@ # Generated by Django 2.2.3 on 2019-07-29 19:32 -from django.conf import settings import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/b2b_ecommerce/migrations/0002_order_coupon_payment.py b/b2b_ecommerce/migrations/0002_order_coupon_payment.py index 909d26165..0617f364c 100644 --- a/b2b_ecommerce/migrations/0002_order_coupon_payment.py +++ b/b2b_ecommerce/migrations/0002_order_coupon_payment.py @@ -1,12 +1,12 @@ # Generated by Django 2.2.3 on 2019-08-05 13:54 -from django.db import migrations, models -import django.db.models.deletion import uuid +import django.db.models.deletion +from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0016_payment_type_choices"), ("b2b_ecommerce", "0001_initial"), diff --git a/b2b_ecommerce/migrations/0003_coupons.py b/b2b_ecommerce/migrations/0003_coupons.py index 743cd8032..c6121bfd3 100644 --- a/b2b_ecommerce/migrations/0003_coupons.py +++ b/b2b_ecommerce/migrations/0003_coupons.py @@ -1,14 +1,13 @@ # Generated by Django 2.2.4 on 2019-09-04 20:11 -from django.conf import settings import django.contrib.postgres.fields.jsonb import django.core.validators -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0016_payment_type_choices"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), diff --git a/b2b_ecommerce/migrations/0004_coupon_company_blank.py b/b2b_ecommerce/migrations/0004_coupon_company_blank.py index edc4e69f9..172b9afe1 100644 --- a/b2b_ecommerce/migrations/0004_coupon_company_blank.py +++ b/b2b_ecommerce/migrations/0004_coupon_company_blank.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.4 on 2019-09-11 13:49 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("b2b_ecommerce", "0003_coupons")] operations = [ diff --git a/b2b_ecommerce/migrations/0005_b2border_contract_number.py b/b2b_ecommerce/migrations/0005_b2border_contract_number.py index e58fabf96..5228bad48 100644 --- a/b2b_ecommerce/migrations/0005_b2border_contract_number.py +++ b/b2b_ecommerce/migrations/0005_b2border_contract_number.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("b2b_ecommerce", "0004_coupon_company_blank")] operations = [ diff --git a/b2b_ecommerce/migrations/0006_b2border_admin_fix.py b/b2b_ecommerce/migrations/0006_b2border_admin_fix.py index 62746f1b5..eab24700f 100644 --- a/b2b_ecommerce/migrations/0006_b2border_admin_fix.py +++ b/b2b_ecommerce/migrations/0006_b2border_admin_fix.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.10 on 2020-04-06 22:19 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("b2b_ecommerce", "0005_b2border_contract_number")] operations = [ diff --git a/b2b_ecommerce/migrations/0007_b2bcoupon_reusable.py b/b2b_ecommerce/migrations/0007_b2bcoupon_reusable.py index 63620da3d..797308167 100644 --- a/b2b_ecommerce/migrations/0007_b2bcoupon_reusable.py +++ b/b2b_ecommerce/migrations/0007_b2bcoupon_reusable.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.10 on 2020-07-20 08:24 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("b2b_ecommerce", "0006_b2border_admin_fix")] operations = [ diff --git a/b2b_ecommerce/migrations/0008_b2b_order_program_run.py b/b2b_ecommerce/migrations/0008_b2b_order_program_run.py index a80fb4ba9..51ba00e2f 100644 --- a/b2b_ecommerce/migrations/0008_b2b_order_program_run.py +++ b/b2b_ecommerce/migrations/0008_b2b_order_program_run.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.10 on 2020-07-22 09:36 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0026_nullify_expiration_date"), ("b2b_ecommerce", "0007_b2bcoupon_reusable"), diff --git a/b2b_ecommerce/migrations/0009_jsonField_from_django_models.py b/b2b_ecommerce/migrations/0009_jsonField_from_django_models.py index 84c8f3a00..98c4df636 100644 --- a/b2b_ecommerce/migrations/0009_jsonField_from_django_models.py +++ b/b2b_ecommerce/migrations/0009_jsonField_from_django_models.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("b2b_ecommerce", "0008_b2b_order_program_run")] operations = [ diff --git a/b2b_ecommerce/migrations/0010_b2bline.py b/b2b_ecommerce/migrations/0010_b2bline.py index 613ae80de..7bc90fcf9 100644 --- a/b2b_ecommerce/migrations/0010_b2bline.py +++ b/b2b_ecommerce/migrations/0010_b2bline.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.15 on 2022-10-17 15:03 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models def backpopulate_b2b_lines(apps, schema_editor): @@ -14,7 +14,6 @@ def backpopulate_b2b_lines(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("b2b_ecommerce", "0009_jsonField_from_django_models"), ] diff --git a/b2b_ecommerce/migrations/0011_alter_b2bcoupon_coupon_code.py b/b2b_ecommerce/migrations/0011_alter_b2bcoupon_coupon_code.py index 119b13d59..151046057 100644 --- a/b2b_ecommerce/migrations/0011_alter_b2bcoupon_coupon_code.py +++ b/b2b_ecommerce/migrations/0011_alter_b2bcoupon_coupon_code.py @@ -1,11 +1,11 @@ # Generated by Django 3.2.23 on 2024-03-04 12:48 from django.db import migrations, models + import ecommerce.utils class Migration(migrations.Migration): - dependencies = [ ("b2b_ecommerce", "0010_b2bline"), ] diff --git a/b2b_ecommerce/models.py b/b2b_ecommerce/models.py index 0fdccdd2a..7c783c3f8 100644 --- a/b2b_ecommerce/models.py +++ b/b2b_ecommerce/models.py @@ -20,7 +20,6 @@ from mitxpro.models import AuditableModel, AuditModel, TimestampedModel from mitxpro.utils import serialize_model_object - B2B_INTEGRATION_PREFIX = "B2B-" @@ -106,7 +105,7 @@ class B2BCoupon(TimestampedModel, AuditableModel): objects = B2BCouponManager() @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 return B2BCouponAudit def to_dict(self): @@ -123,7 +122,7 @@ class B2BCouponAudit(AuditModel): coupon = models.ForeignKey(B2BCoupon, null=True, on_delete=models.PROTECT) @classmethod - def get_related_field_name(cls): + def get_related_field_name(cls): # noqa: D102 return "coupon" @@ -149,7 +148,7 @@ class B2BOrder(OrderAbstract, AuditableModel): discount = models.DecimalField( decimal_places=2, max_digits=20, null=True, blank=True ) - contract_number = models.CharField(max_length=50, null=True, blank=True) + contract_number = models.CharField(max_length=50, null=True, blank=True) # noqa: DJ001 program_run = models.ForeignKey( "courses.ProgramRun", blank=True, @@ -170,7 +169,7 @@ def __str__(self): return f"B2BOrder #{self.id}, status={self.status}" @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 return B2BOrderAudit def to_dict(self): @@ -225,7 +224,7 @@ class B2BOrderAudit(AuditModel): order = models.ForeignKey(B2BOrder, null=True, on_delete=models.PROTECT) @classmethod - def get_related_field_name(cls): + def get_related_field_name(cls): # noqa: D102 return "order" diff --git a/b2b_ecommerce/models_test.py b/b2b_ecommerce/models_test.py index 45f238a87..836dd1f8e 100644 --- a/b2b_ecommerce/models_test.py +++ b/b2b_ecommerce/models_test.py @@ -9,7 +9,6 @@ from b2b_ecommerce.models import B2BCoupon, B2BOrder, B2BOrderAudit from mitxpro.utils import serialize_model_object - pytestmark = pytest.mark.django_db @@ -57,12 +56,12 @@ def test_reference_number(settings): @pytest.mark.parametrize( - "activation_date, expiration_date", + "activation_date, expiration_date", # noqa: PT006 [ - [None, None], - [timezone.now() - timedelta(days=1), timezone.now() + timedelta(days=1)], - [None, timezone.now() + timedelta(days=1)], - [timezone.now() - timedelta(days=1), None], + [None, None], # noqa: PT007 + [timezone.now() - timedelta(days=1), timezone.now() + timedelta(days=1)], # noqa: PT007 + [None, timezone.now() + timedelta(days=1)], # noqa: PT007 + [timezone.now() - timedelta(days=1), None], # noqa: PT007 ], ) def test_get_unexpired_coupon(order_with_coupon, activation_date, expiration_date): @@ -80,11 +79,11 @@ def test_get_unexpired_coupon(order_with_coupon, activation_date, expiration_dat @pytest.mark.parametrize( - "attr_name, attr_value", + "attr_name, attr_value", # noqa: PT006 [ - ["enabled", False], - ["activation_date", timezone.now() + timedelta(days=1)], - ["expiration_date", timezone.now() - timedelta(days=1)], + ["enabled", False], # noqa: PT007 + ["activation_date", timezone.now() + timedelta(days=1)], # noqa: PT007 + ["expiration_date", timezone.now() - timedelta(days=1)], # noqa: PT007 ], ) def test_get_unexpired_coupon_not_found(order_with_coupon, attr_name, attr_value): diff --git a/b2b_ecommerce/signals.py b/b2b_ecommerce/signals.py index be6f875bc..6c68c17c5 100644 --- a/b2b_ecommerce/signals.py +++ b/b2b_ecommerce/signals.py @@ -7,8 +7,11 @@ @receiver(post_save, sender=B2BOrder, dispatch_uid="b2b_order_post_save") def create_b2b_line( - sender, instance, created, **kwargs -): # pylint:disable=unused-argument + sender, # noqa: ARG001 + instance, + created, + **kwargs, # noqa: ARG001 +): """ Create a B2BLine object for each B2BOrder """ diff --git a/b2b_ecommerce/urls.py b/b2b_ecommerce/urls.py index 9871178f7..f25a7dabb 100644 --- a/b2b_ecommerce/urls.py +++ b/b2b_ecommerce/urls.py @@ -9,7 +9,6 @@ ) from mitxpro.views import index - urlpatterns = [ path("api/b2b/checkout/", B2BCheckoutView.as_view(), name="b2b-checkout"), path( diff --git a/b2b_ecommerce/views.py b/b2b_ecommerce/views.py index f19baa5aa..239afbf35 100644 --- a/b2b_ecommerce/views.py +++ b/b2b_ecommerce/views.py @@ -30,7 +30,6 @@ from mitxpro.utils import make_csv_http_response from users.models import User - log = logging.getLogger(__name__) @@ -44,8 +43,11 @@ class B2BCheckoutView(APIView): permission_classes = () def post( - self, request, *args, **kwargs - ): # pylint: disable=too-many-locals,unused-argument + self, + request, + *args, # noqa: ARG002 + **kwargs, # noqa: ARG002 + ): """ Create a new unfulfilled Order from the user's basket and return information used to submit to CyberSource. @@ -58,17 +60,17 @@ def post( contract_number = request.data.get("contract_number") run_id = request.data.get("run_id") except KeyError as ex: - raise ValidationError(f"Missing parameter {ex.args[0]}") + raise ValidationError(f"Missing parameter {ex.args[0]}") # noqa: B904, EM102, TRY200 try: validate_email(email) except DjangoValidationError: - raise ValidationError({"email": "Invalid email"}) + raise ValidationError({"email": "Invalid email"}) # noqa: B904, TRY200 try: num_seats = int(num_seats) except ValueError: - raise ValidationError({"num_seats": "num_seats must be a number"}) + raise ValidationError({"num_seats": "num_seats must be a number"}) # noqa: B904, TRY200 if ( contract_number @@ -144,7 +146,7 @@ class B2BOrderStatusView(APIView): authentication_classes = () permission_classes = () - def get(self, request, *args, **kwargs): # pylint: disable=unused-argument + def get(self, request, *args, **kwargs): # noqa: ARG002 """Return B2B order status and other information about the order needed to display the receipt""" order_hash = kwargs["hash"] order = get_object_or_404(B2BOrder, unique_id=order_hash) @@ -196,7 +198,7 @@ class B2BEnrollmentCodesView(APIView): authentication_classes = () permission_classes = () - def get(self, request, *args, **kwargs): # pylint: disable=unused-argument + def get(self, request, *args, **kwargs): # noqa: ARG002 """Create a CSV with enrollment codes""" order_hash = kwargs["hash"] order = get_object_or_404( @@ -236,14 +238,14 @@ class B2BCouponView(APIView): authentication_classes = () permission_classes = () - def get(self, request, *args, **kwargs): # pylint: disable=unused-argument + def get(self, request, *args, **kwargs): # noqa: ARG002 """Get information about a coupon""" product = None try: coupon_code = request.GET["code"] product_id = request.GET["product_id"] except KeyError as ex: - raise ValidationError(f"Missing parameter {ex.args[0]}") + raise ValidationError(f"Missing parameter {ex.args[0]}") # noqa: B904, EM102, TRY200 try: # product_id can be an integer e.g. 1234 or @@ -259,7 +261,7 @@ def get(self, request, *args, **kwargs): # pylint: disable=unused-argument coupon_code=coupon_code, product_id=product_id ) except B2BCoupon.DoesNotExist: - raise Http404 + raise Http404 # noqa: B904 return Response( data={ diff --git a/b2b_ecommerce/views_test.py b/b2b_ecommerce/views_test.py index 1cdc373a1..9d38478b4 100644 --- a/b2b_ecommerce/views_test.py +++ b/b2b_ecommerce/views_test.py @@ -21,7 +21,6 @@ from mitxpro.utils import dict_without_keys from users.factories import UserFactory - CYBERSOURCE_SECURE_ACCEPTANCE_URL = "http://fake" CYBERSOURCE_ACCESS_KEY = "access" CYBERSOURCE_PROFILE_ID = "profile" @@ -30,11 +29,10 @@ pytestmark = pytest.mark.django_db -# pylint: disable=redefined-outer-name,unused-argument,too-many-lines @pytest.fixture(autouse=True) -def ecommerce_settings(settings): +def ecommerce_settings(settings): # noqa: PT004 """ Set cybersource settings """ @@ -85,7 +83,7 @@ def test_create_order(client, mocker): assert order.num_seats == num_seats assert order.b2breceipt_set.count() == 0 base_url = "http://testserver/" - receipt_url = f'{urljoin(base_url, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' + receipt_url = f'{urljoin(base_url, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' # noqa: RUF010 assert generate_mock.call_count == 1 assert generate_mock.call_args[0] == () assert generate_mock.call_args[1] == { @@ -138,7 +136,7 @@ def test_create_order_with_coupon(client, mocker): assert order.num_seats == num_seats assert order.b2breceipt_set.count() == 0 base_url = "http://testserver/" - receipt_url = f'{urljoin(base_url, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' + receipt_url = f'{urljoin(base_url, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' # noqa: RUF010 assert generate_payload_mock.call_count == 1 assert generate_payload_mock.call_args[0] == () assert generate_payload_mock.call_args[1] == { @@ -260,7 +258,7 @@ def test_create_order_product_version(client): assert resp.status_code == status.HTTP_404_NOT_FOUND -def test_zero_price_checkout(client, mocker): # pylint:disable=too-many-arguments +def test_zero_price_checkout(client, mocker): """ If the order total is $0, we should just fulfill the order and direct the user to our order receipt page """ @@ -279,7 +277,7 @@ def test_zero_price_checkout(client, mocker): # pylint:disable=too-many-argumen assert B2BOrder.objects.count() == 1 order = B2BOrder.objects.first() base_url = "http://testserver" - receipt_url = f'{urljoin(base_url, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' + receipt_url = f'{urljoin(base_url, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' # noqa: RUF010 assert resp.status_code == status.HTTP_200_OK assert resp.json() == {"payload": {}, "url": receipt_url, "method": "GET"} diff --git a/blog/api.py b/blog/api.py index ac7e6c956..f17249c7c 100644 --- a/blog/api.py +++ b/blog/api.py @@ -7,7 +7,6 @@ from django.utils.dateformat import DateFormat from django.utils.dateparse import parse_datetime - log = logging.getLogger() RSS_FEED_URL = "https://curve.mit.edu/rss.xml" diff --git a/blog/api_test.py b/blog/api_test.py index 7f0661184..850cce099 100644 --- a/blog/api_test.py +++ b/blog/api_test.py @@ -16,8 +16,8 @@ def valid_blog_post(): 'src="https://curve.mit.edu/hubfs/Screenshot%202023-10-05%20at%203.55.25%20PM.png" alt="Ask ' 'an MIT Professor: The Science Behind Oppenheimer" class="hs-featured-image" ' 'style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"> ' - "\n \n

It’s not every day you see a topic like quantum physics represented in a hit " - 'summer movie. Yet Christopher Nolan’s Oppenheimer ' + "\n \n

It’s not every day you see a topic like quantum physics represented in a hit " # noqa: RUF001 + 'summer movie. Yet Christopher Nolan’s Oppenheimer ' # noqa: RUF001 "has dazzled audiences everywhere and is on track to earn nearly $1 billion at the global box ' @@ -28,8 +28,8 @@ def valid_blog_post(): 'src="https://curve.mit.edu/hubfs/Screenshot%202023-10-05%20at%203.55.25%20PM.png" alt="Ask ' 'an MIT Professor: The Science Behind Oppenheimer" class="hs-featured-image" ' 'style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"> ' - "\n \n

It’s not every day you see a topic like quantum physics represented in a hit " - 'summer movie. Yet Christopher Nolan’s Oppenheimer ' + "\n \n

It’s not every day you see a topic like quantum physics represented in a hit " # noqa: RUF001 + 'summer movie. Yet Christopher Nolan’s Oppenheimer ' # noqa: RUF001 "has dazzled audiences everywhere and is on track to earn nearly $1 billion at the global box ' @@ -43,18 +43,16 @@ def valid_blog_post(): @pytest.mark.parametrize( - "category, expected_category", + "category, expected_category", # noqa: PT006 [ - ["Quantum Computing", ["Quantum Computing"]], - [ + ["Quantum Computing", ["Quantum Computing"]], # noqa: PT007 + [ # noqa: PT007 ["Quantum Computing", "Online Education"], ["Quantum Computing", "Online Education"], ], ], ) -def test_parse_blog( - category, expected_category, valid_blog_post -): # pylint: disable=redefined-outer-name +def test_parse_blog(category, expected_category, valid_blog_post): """ Tests that `parse_blog` parses a blog post as required. """ @@ -93,8 +91,8 @@ def test_parse_blog( ) assert ( valid_blog_post["description"] - == "It’s not every day you see a topic like quantum physics represented in a hit " - "summer movie. Yet Christopher Nolan’s Oppenheimer has dazzled audiences everywhere" + == "It’s not every day you see a topic like quantum physics represented in a hit " # noqa: RUF001 + "summer movie. Yet Christopher Nolan’s Oppenheimer has dazzled audiences everywhere" # noqa: RUF001 " and is on track to earn nearly $1 billion at the global box office." ) assert valid_blog_post["categories"] == expected_category @@ -124,9 +122,7 @@ def test_fetch_blog(): ) -def test_parse_blog_invalid_type_and_data( - mocker, valid_blog_post -): # pylint: disable=redefined-outer-name +def test_parse_blog_invalid_type_and_data(mocker, valid_blog_post): """ Test that `parse_blog` logs error when post item type or data is not valid. """ diff --git a/cms/api.py b/cms/api.py index f745a7b98..7f160b0ab 100644 --- a/cms/api.py +++ b/cms/api.py @@ -3,16 +3,15 @@ import logging from datetime import MAXYEAR, datetime, timezone - from django.contrib.contenttypes.models import ContentType from wagtail.models import Page, Site + from cms import models as cms_models from cms.constants import CERTIFICATE_INDEX_SLUG, ENTERPRISE_PAGE_SLUG - log = logging.getLogger(__name__) -DEFAULT_HOMEPAGE_PROPS = dict(title="Home Page", subhead="This is the home page") -DEFAULT_SITE_PROPS = dict(hostname="localhost", port=80) +DEFAULT_HOMEPAGE_PROPS = dict(title="Home Page", subhead="This is the home page") # noqa: C408 +DEFAULT_SITE_PROPS = dict(hostname="localhost", port=80) # noqa: C408 def filter_and_sort_catalog_pages( @@ -136,7 +135,7 @@ def ensure_catalog_page(): catalog_page.refresh_from_db() -def ensure_index_pages(): # pylint: disable=too-many-branches +def ensure_index_pages(): # noqa: C901 """ Ensures that the proper index pages exist as children of the home page, and that any pages that should belong to those index pages are set as children. @@ -203,7 +202,7 @@ def ensure_index_pages(): # pylint: disable=too-many-branches def ensure_enterprise_page(): """ - Ensures that an enterprise page with the correct slug exists. + Ensure that an enterprise page with the correct slug exists. """ enterprise_page = cms_models.EnterprisePage.objects.first() @@ -213,7 +212,7 @@ def ensure_enterprise_page(): enterprise_page_data = { "title": "Enterprise Page", "slug": ENTERPRISE_PAGE_SLUG, - "description": "Deepen your team’s career knowledge and expand their abilities with MIT xPRO’s online " + "description": "Deepen your team's career knowledge and expand their abilities with MIT xPRO's online " "courses for professionals.", "action_title": "Find out what MIT xPRO can do for your team.", "headings": [ diff --git a/cms/api_test.py b/cms/api_test.py index 5fae1cf98..a960cefa7 100644 --- a/cms/api_test.py +++ b/cms/api_test.py @@ -2,16 +2,17 @@ from datetime import timedelta import pytest + +from cms.api import filter_and_sort_catalog_pages from cms.factories import ExternalCoursePageFactory, ExternalProgramPageFactory from cms.models import ExternalCoursePage -from cms.api import filter_and_sort_catalog_pages from courses.factories import CourseRunFactory, ProgramRunFactory from mitxpro.utils import now_in_utc pytestmark = pytest.mark.django_db -def test_filter_and_sort_catalog_pages(): # pylint:disable=too-many-locals +def test_filter_and_sort_catalog_pages(): """ Test that filter_and_sort_catalog_pages removes program/course/external course pages that do not have a future start date or enrollment end date, and returns appropriately sorted lists of pages diff --git a/cms/blocks.py b/cms/blocks.py index bd26451f1..cf498d635 100644 --- a/cms/blocks.py +++ b/cms/blocks.py @@ -164,7 +164,7 @@ def validate_unique_readable_ids(value): unique readable IDs """ # We want to validate the overall stream not underlying blocks individually - if len(value) < 2: + if len(value) < 2: # noqa: PLR2004 return items = [ stream_block.value.get("readable_id") diff --git a/cms/embeds.py b/cms/embeds.py index 771f5034f..10354f08f 100644 --- a/cms/embeds.py +++ b/cms/embeds.py @@ -4,9 +4,8 @@ from urllib.parse import parse_qs, urlencode, urlparse, urlunparse -from django.core.exceptions import ImproperlyConfigured - from bs4 import BeautifulSoup +from django.core.exceptions import ImproperlyConfigured from wagtail.embeds.finders.oembed import OEmbedFinder from wagtail.embeds.oembed_providers import youtube @@ -25,12 +24,12 @@ def __init__(self, providers=None, options=None): if providers != [youtube]: raise ImproperlyConfigured( - "The YouTubeEmbedFinder only operates on the youtube provider" + "The YouTubeEmbedFinder only operates on the youtube provider" # noqa: EM101 ) super().__init__(providers=providers, options=options) - def find_embed(self, url, max_width=None): # pylint: disable=arguments-differ + def find_embed(self, url, max_width=None): # noqa: D102 embed = super().find_embed(url, max_width) embed_tag = BeautifulSoup(embed["html"], "html.parser") player_iframe = embed_tag.find("iframe") diff --git a/cms/factories.py b/cms/factories.py index 83298ea60..dd309b94e 100644 --- a/cms/factories.py +++ b/cms/factories.py @@ -53,7 +53,6 @@ ) from courses.factories import CourseFactory, ProgramFactory - factory.Faker.add_provider(internet) FAKE = faker.Factory.create() @@ -84,7 +83,7 @@ class Meta: model = ProgramPage @factory.post_generation - def post_gen(obj, create, extracted, **kwargs): # pylint:disable=unused-argument + def post_gen(obj, create, extracted, **kwargs): # noqa: ARG002, N805 """Post-generation hook""" if create: # Move the created page to be a child of the program index page @@ -114,7 +113,7 @@ class Meta: model = CoursePage @factory.post_generation - def post_gen(obj, create, extracted, **kwargs): # pylint:disable=unused-argument + def post_gen(obj, create, extracted, **kwargs): # noqa: ARG002, N805 """Post-generation hook""" if create: # Move the created page to be a child of the course index page @@ -143,7 +142,7 @@ class Meta: model = ExternalCoursePage @factory.post_generation - def post_gen(obj, create, extracted, **kwargs): # pylint:disable=unused-argument + def post_gen(obj, create, extracted, **kwargs): # noqa: ARG002, N805 """Post-generation hook""" if create: # Move the created page to be a child of the course index page @@ -172,7 +171,7 @@ class Meta: model = ExternalProgramPage @factory.post_generation - def post_gen(obj, create, extracted, **kwargs): # pylint:disable=unused-argument + def post_gen(obj, create, extracted, **kwargs): # noqa: ARG002, N805 """Post-generation hook""" if create: # Move the created page to be a child of the program index page @@ -375,9 +374,7 @@ class FacultyBlockFactory(wagtail_factories.StructBlockFactory): name = factory.Faker("name") image = factory.SubFactory(wagtail_factories.ImageChooserBlockFactory) - description = factory.LazyFunction( - lambda: RichText("

{}

".format(FAKE.paragraph())) - ) + description = factory.LazyFunction(lambda: RichText(f"

{FAKE.paragraph()}

")) class Meta: model = FacultyBlock @@ -458,7 +455,7 @@ class Meta: model = SignatoryPage -class SignatoryChooserBlockFactory(wagtail_factories.PageChooserBlockFactory): +class SignatoryChooserBlockFactory(wagtail_factories.PageChooserBlockFactory): # noqa: D101 class Meta: model = SignatoryPage diff --git a/cms/forms.py b/cms/forms.py index 28277d27e..5e7af2b9f 100644 --- a/cms/forms.py +++ b/cms/forms.py @@ -14,7 +14,6 @@ class CertificatePageForm(WagtailAdminPageForm): Custom form for CertificatePage in order to filter course run IDs """ - # pylint: disable=keyword-arg-before-vararg def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs): super().__init__(data, files, parent_page, *args, **kwargs) if parent_page.specific.is_course_page: @@ -53,7 +52,7 @@ def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs): elif instance.is_internal_or_external_program_page: self.fields["price"].initial = instance.program.current_price - def save(self, commit=True): + def save(self, commit=True): # noqa: FBT002 """ Handles pricing update and creates product(if required) and product version for a course run. """ diff --git a/cms/management/commands/configure_wagtail.py b/cms/management/commands/configure_wagtail.py index 02112b749..8066164cb 100644 --- a/cms/management/commands/configure_wagtail.py +++ b/cms/management/commands/configure_wagtail.py @@ -9,5 +9,5 @@ class Command(BaseCommand): help = __doc__ - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002, D102 configure_wagtail() diff --git a/cms/management/commands/setup_index_pages.py b/cms/management/commands/setup_index_pages.py index d164e1f1a..147f43923 100644 --- a/cms/management/commands/setup_index_pages.py +++ b/cms/management/commands/setup_index_pages.py @@ -9,5 +9,5 @@ class Command(BaseCommand): help = __doc__ - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002, D102 ensure_index_pages() diff --git a/cms/migrations/0001_initial.py b/cms/migrations/0001_initial.py index a61c8aebf..de58c79d9 100644 --- a/cms/migrations/0001_initial.py +++ b/cms/migrations/0001_initial.py @@ -1,14 +1,13 @@ # Generated by Django 2.1.7 on 2019-03-12 17:59 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/cms/migrations/0002_add_duration.py b/cms/migrations/0002_add_duration.py index 24a52d35f..d589f6c7e 100644 --- a/cms/migrations/0002_add_duration.py +++ b/cms/migrations/0002_add_duration.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0001_initial")] operations = [ diff --git a/cms/migrations/0003_add_video_url_bg_image.py b/cms/migrations/0003_add_video_url_bg_image.py index 3321901fd..5a09b02a1 100644 --- a/cms/migrations/0003_add_video_url_bg_image.py +++ b/cms/migrations/0003_add_video_url_bg_image.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-18 10:16 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0001_squashed_0021"), ("cms", "0002_add_duration"), diff --git a/cms/migrations/0004_add_video_title.py b/cms/migrations/0004_add_video_title.py index 443b30a2f..129cf3087 100644 --- a/cms/migrations/0004_add_video_title.py +++ b/cms/migrations/0004_add_video_title.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-19 05:44 -from django.db import migrations import wagtail.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [("cms", "0003_add_video_url_bg_image")] operations = [ diff --git a/cms/migrations/0005_move_fields_to_product.py b/cms/migrations/0005_move_fields_to_product.py index bd3dc9bb6..c5c30dfe0 100644 --- a/cms/migrations/0005_move_fields_to_product.py +++ b/cms/migrations/0005_move_fields_to_product.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-04-22 08:57 -from django.db import migrations, models import django.db.models.deletion import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0001_squashed_0021"), ("cms", "0004_add_video_title"), diff --git a/cms/migrations/0006_make_subhead_required.py b/cms/migrations/0006_make_subhead_required.py index a276ea661..bcec683db 100644 --- a/cms/migrations/0006_make_subhead_required.py +++ b/cms/migrations/0006_make_subhead_required.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0005_move_fields_to_product")] operations = [ diff --git a/cms/migrations/0007_add_time_commitment.py b/cms/migrations/0007_add_time_commitment.py index de6d1b648..837853fc7 100644 --- a/cms/migrations/0007_add_time_commitment.py +++ b/cms/migrations/0007_add_time_commitment.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0006_make_subhead_required")] operations = [ diff --git a/cms/migrations/0008_learningoutcomespage.py b/cms/migrations/0008_learningoutcomespage.py index eb871d6e6..00b1733ec 100644 --- a/cms/migrations/0008_learningoutcomespage.py +++ b/cms/migrations/0008_learningoutcomespage.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-04-30 09:36 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0007_add_time_commitment"), diff --git a/cms/migrations/0009_learningtechniquespage.py b/cms/migrations/0009_learningtechniquespage.py index c3aa24cb1..0b398a3e4 100644 --- a/cms/migrations/0009_learningtechniquespage.py +++ b/cms/migrations/0009_learningtechniquespage.py @@ -1,14 +1,13 @@ # Generated by Django 2.1.7 on 2019-05-03 12:41 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0008_learningoutcomespage"), diff --git a/cms/migrations/0010_add_faqspage.py b/cms/migrations/0010_add_faqspage.py index 5fdfc6b40..500b3e969 100644 --- a/cms/migrations/0010_add_faqspage.py +++ b/cms/migrations/0010_add_faqspage.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-05-03 06:27 -from django.db import migrations, models import django.db.models.deletion import modelcluster.fields import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0009_learningtechniquespage"), diff --git a/cms/migrations/0011_for_teams_subpage.py b/cms/migrations/0011_for_teams_subpage.py index 2dcf7dd16..af2e1127e 100644 --- a/cms/migrations/0011_for_teams_subpage.py +++ b/cms/migrations/0011_for_teams_subpage.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-02 14:01 -from django.db import migrations, models import django.db.models.deletion import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("wagtailimages", "0001_squashed_0021"), diff --git a/cms/migrations/0012_who_should_enroll_subpage.py b/cms/migrations/0012_who_should_enroll_subpage.py index 3961d39e6..20f066121 100644 --- a/cms/migrations/0012_who_should_enroll_subpage.py +++ b/cms/migrations/0012_who_should_enroll_subpage.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-05-06 20:40 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("wagtailimages", "0001_squashed_0021"), diff --git a/cms/migrations/0013_courses_in_program_subpage.py b/cms/migrations/0013_courses_in_program_subpage.py index a515c37c9..caf173d3c 100644 --- a/cms/migrations/0013_courses_in_program_subpage.py +++ b/cms/migrations/0013_courses_in_program_subpage.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-07 12:13 -from django.db import migrations, models import django.db.models.deletion import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0012_who_should_enroll_subpage"), diff --git a/cms/migrations/0014_resourcepage.py b/cms/migrations/0014_resourcepage.py index 5ad4053bb..d948a2dd0 100644 --- a/cms/migrations/0014_resourcepage.py +++ b/cms/migrations/0014_resourcepage.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-05-09 11:46 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0013_courses_in_program_subpage"), diff --git a/cms/migrations/0015_user_testimonials_subpage.py b/cms/migrations/0015_user_testimonials_subpage.py index 3cd2af00d..f485abda2 100644 --- a/cms/migrations/0015_user_testimonials_subpage.py +++ b/cms/migrations/0015_user_testimonials_subpage.py @@ -1,14 +1,13 @@ # Generated by Django 2.1.7 on 2019-05-08 20:08 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0014_resourcepage"), diff --git a/cms/migrations/0016_faculty_members_subpage.py b/cms/migrations/0016_faculty_members_subpage.py index 848ec770b..7fb100029 100644 --- a/cms/migrations/0016_faculty_members_subpage.py +++ b/cms/migrations/0016_faculty_members_subpage.py @@ -1,14 +1,13 @@ # Generated by Django 2.1.7 on 2019-05-06 15:25 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0015_user_testimonials_subpage"), diff --git a/cms/migrations/0017_sections_generalize.py b/cms/migrations/0017_sections_generalize.py index c31bb6ebc..99d8cac63 100644 --- a/cms/migrations/0017_sections_generalize.py +++ b/cms/migrations/0017_sections_generalize.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0016_faculty_members_subpage")] operations = [ diff --git a/cms/migrations/0018_sitenotification.py b/cms/migrations/0018_sitenotification.py index 3c58b3057..156a0fed3 100644 --- a/cms/migrations/0018_sitenotification.py +++ b/cms/migrations/0018_sitenotification.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-22 06:15 -from django.db import migrations, models import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("cms", "0017_sections_generalize")] operations = [ diff --git a/cms/migrations/0019_home_page.py b/cms/migrations/0019_home_page.py index a05a68ce7..86e4a857d 100644 --- a/cms/migrations/0019_home_page.py +++ b/cms/migrations/0019_home_page.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-21 08:46 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0018_sitenotification"), diff --git a/cms/migrations/0020_section_fixes.py b/cms/migrations/0020_section_fixes.py index 5d82774ef..8820e9540 100644 --- a/cms/migrations/0020_section_fixes.py +++ b/cms/migrations/0020_section_fixes.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-21 09:41 -from django.db import migrations import wagtail.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [("cms", "0019_home_page")] operations = [ diff --git a/cms/migrations/0021_text_image_dark_theme.py b/cms/migrations/0021_text_image_dark_theme.py index 6715b878b..152a948f8 100644 --- a/cms/migrations/0021_text_image_dark_theme.py +++ b/cms/migrations/0021_text_image_dark_theme.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0020_section_fixes")] operations = [ diff --git a/cms/migrations/0022_header_background_hls_video.py b/cms/migrations/0022_header_background_hls_video.py index 63b2ee119..933ba4dd4 100644 --- a/cms/migrations/0022_header_background_hls_video.py +++ b/cms/migrations/0022_header_background_hls_video.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-24 11:29 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0001_squashed_0021"), ("cms", "0021_text_image_dark_theme"), diff --git a/cms/migrations/0023_text_image_section_action_url.py b/cms/migrations/0023_text_image_section_action_url.py index fa60f38a2..53b620501 100644 --- a/cms/migrations/0023_text_image_section_action_url.py +++ b/cms/migrations/0023_text_image_section_action_url.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0022_header_background_hls_video")] operations = [ diff --git a/cms/migrations/0024_courseware_carousel_manual_contents.py b/cms/migrations/0024_courseware_carousel_manual_contents.py index 9711e3f60..c19499218 100644 --- a/cms/migrations/0024_courseware_carousel_manual_contents.py +++ b/cms/migrations/0024_courseware_carousel_manual_contents.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-23 08:02 -from django.db import migrations, models import wagtail.blocks import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("cms", "0023_text_image_section_action_url")] operations = [ diff --git a/cms/migrations/0025_add_wagtailmetadata.py b/cms/migrations/0025_add_wagtailmetadata.py index 54b9e2dcb..83021c645 100644 --- a/cms/migrations/0025_add_wagtailmetadata.py +++ b/cms/migrations/0025_add_wagtailmetadata.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-27 09:18 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0001_squashed_0021"), ("cms", "0024_courseware_carousel_manual_contents"), diff --git a/cms/migrations/0026_text_video_section.py b/cms/migrations/0026_text_video_section.py index 3be939f44..8fb47f9ff 100644 --- a/cms/migrations/0026_text_video_section.py +++ b/cms/migrations/0026_text_video_section.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-27 09:52 -from django.db import migrations, models import django.db.models.deletion import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0025_add_wagtailmetadata"), diff --git a/cms/migrations/0027_imagecarouselpage.py b/cms/migrations/0027_imagecarouselpage.py index 00ee43bba..cae881c6a 100644 --- a/cms/migrations/0027_imagecarouselpage.py +++ b/cms/migrations/0027_imagecarouselpage.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-05-28 15:43 -from django.db import migrations, models import django.db.models.deletion import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0026_text_video_section"), diff --git a/cms/migrations/0028_course_program_index_pages.py b/cms/migrations/0028_course_program_index_pages.py index f67196f49..d8891c230 100644 --- a/cms/migrations/0028_course_program_index_pages.py +++ b/cms/migrations/0028_course_program_index_pages.py @@ -1,12 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-28 12:40 -import cms.models -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0027_imagecarouselpage"), diff --git a/cms/migrations/0029_setup_course_program_index_pages.py b/cms/migrations/0029_setup_course_program_index_pages.py index 9c4a828d1..3cd687740 100644 --- a/cms/migrations/0029_setup_course_program_index_pages.py +++ b/cms/migrations/0029_setup_course_program_index_pages.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0028_course_program_index_pages")] operations = [ diff --git a/cms/migrations/0030_catalog_page_model.py b/cms/migrations/0030_catalog_page_model.py index 6518f38d6..fe353d6e0 100644 --- a/cms/migrations/0030_catalog_page_model.py +++ b/cms/migrations/0030_catalog_page_model.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-30 09:52 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0029_setup_course_program_index_pages"), diff --git a/cms/migrations/0031_setup_catalog_page.py b/cms/migrations/0031_setup_catalog_page.py index c04c7af11..62df27767 100644 --- a/cms/migrations/0031_setup_catalog_page.py +++ b/cms/migrations/0031_setup_catalog_page.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0030_catalog_page_model")] operations = [ diff --git a/cms/migrations/0032_product_video_hls.py b/cms/migrations/0032_product_video_hls.py index df9314392..2a60a01b3 100644 --- a/cms/migrations/0032_product_video_hls.py +++ b/cms/migrations/0032_product_video_hls.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0031_setup_catalog_page")] operations = [ diff --git a/cms/migrations/0033_text_section_page.py b/cms/migrations/0033_text_section_page.py index 61094cab9..633b202e5 100644 --- a/cms/migrations/0033_text_section_page.py +++ b/cms/migrations/0033_text_section_page.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-06-10 11:25 -from django.db import migrations, models import django.db.models.deletion import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0032_product_video_hls"), diff --git a/cms/migrations/0034_alter_helpttext_thumbnail.py b/cms/migrations/0034_alter_helpttext_thumbnail.py index 231712aa7..2a236f33c 100644 --- a/cms/migrations/0034_alter_helpttext_thumbnail.py +++ b/cms/migrations/0034_alter_helpttext_thumbnail.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-06-17 09:26 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("cms", "0033_text_section_page")] operations = [ diff --git a/cms/migrations/0035_alter_helptext_video_url.py b/cms/migrations/0035_alter_helptext_video_url.py index f1fe56c91..1cab1745d 100644 --- a/cms/migrations/0035_alter_helptext_video_url.py +++ b/cms/migrations/0035_alter_helptext_video_url.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0034_alter_helpttext_thumbnail")] operations = [ diff --git a/cms/migrations/0036_make_subhead_optional.py b/cms/migrations/0036_make_subhead_optional.py index efdb6ab12..41de99ef8 100644 --- a/cms/migrations/0036_make_subhead_optional.py +++ b/cms/migrations/0036_make_subhead_optional.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0035_alter_helptext_video_url")] operations = [ diff --git a/cms/migrations/0037_featured_product.py b/cms/migrations/0037_featured_product.py index e21156252..69736e2bb 100644 --- a/cms/migrations/0037_featured_product.py +++ b/cms/migrations/0037_featured_product.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0036_make_subhead_optional")] operations = [ diff --git a/cms/migrations/0038_learning_outcomes_subhead_richtext.py b/cms/migrations/0038_learning_outcomes_subhead_richtext.py index 9c3a88fea..c72f0dc57 100644 --- a/cms/migrations/0038_learning_outcomes_subhead_richtext.py +++ b/cms/migrations/0038_learning_outcomes_subhead_richtext.py @@ -23,7 +23,7 @@ def convert_to_plaintext(apps, schema_editor): Because going back from RichText to CharField we don't want any HTML to be embedded within the field content so the content will be extracted and any html stripped away. """ - if not schema_editor.connection.alias == "default": + if schema_editor.connection.alias != "default": return LearningOutcomesPage = apps.get_model("cms", "LearningOutcomesPage") for page in LearningOutcomesPage.objects.all(): @@ -32,7 +32,6 @@ def convert_to_plaintext(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("cms", "0037_featured_product")] operations = [ diff --git a/cms/migrations/0039_product_page_catalog_details.py b/cms/migrations/0039_product_page_catalog_details.py index b87763091..eb342945f 100644 --- a/cms/migrations/0039_product_page_catalog_details.py +++ b/cms/migrations/0039_product_page_catalog_details.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.9 on 2019-07-03 11:43 -from django.db import migrations, models import wagtail.fields +from django.db import migrations, models def copy_description_to_catalog_details(apps, schema_editor): @@ -21,11 +21,10 @@ def dummy_reverse(apps, schema_editor): Dummy reverse placeholder for Django to be able to reverse this migration. """ - pass + pass # noqa: PIE790 class Migration(migrations.Migration): - dependencies = [("cms", "0038_learning_outcomes_subhead_richtext")] operations = [ diff --git a/cms/migrations/0040_whoshouldenrollpage_heading.py b/cms/migrations/0040_whoshouldenrollpage_heading.py index f88e9c24f..6ba0944aa 100644 --- a/cms/migrations/0040_whoshouldenrollpage_heading.py +++ b/cms/migrations/0040_whoshouldenrollpage_heading.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0039_product_page_catalog_details")] operations = [ diff --git a/cms/migrations/0041_certificatepage_signatoryindexpage_signatorypage.py b/cms/migrations/0041_certificatepage_signatoryindexpage_signatorypage.py index 0fd779271..60dc4179d 100644 --- a/cms/migrations/0041_certificatepage_signatoryindexpage_signatorypage.py +++ b/cms/migrations/0041_certificatepage_signatoryindexpage_signatorypage.py @@ -1,13 +1,12 @@ # Generated by Django 2.2.3 on 2019-08-07 13:29 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0001_squashed_0021"), ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), diff --git a/cms/migrations/0042_certificate_index_page.py b/cms/migrations/0042_certificate_index_page.py index b8d9db99f..45ff7e133 100644 --- a/cms/migrations/0042_certificate_index_page.py +++ b/cms/migrations/0042_certificate_index_page.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.4 on 2019-09-18 07:20 -from django.db import migrations, models import django.db.models.deletion import wagtail.contrib.routable_page.models +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), ("cms", "0041_certificatepage_signatoryindexpage_signatorypage"), diff --git a/cms/migrations/0043_setup_certificate_index_page.py b/cms/migrations/0043_setup_certificate_index_page.py index e387dede7..7c5fafe21 100644 --- a/cms/migrations/0043_setup_certificate_index_page.py +++ b/cms/migrations/0043_setup_certificate_index_page.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [("cms", "0042_certificate_index_page")] operations = [ diff --git a/cms/migrations/0044_external_course_cms_page.py b/cms/migrations/0044_external_course_cms_page.py index fbad8ecbb..7869d64ca 100644 --- a/cms/migrations/0044_external_course_cms_page.py +++ b/cms/migrations/0044_external_course_cms_page.py @@ -1,15 +1,14 @@ # Generated by Django 2.2.4 on 2019-10-21 14:11 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks import wagtailmetadata.models +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0001_squashed_0021"), ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), diff --git a/cms/migrations/0045_certificate_page_courserun_overrides.py b/cms/migrations/0045_certificate_page_courserun_overrides.py index 0e883cac7..ed2bb1d75 100644 --- a/cms/migrations/0045_certificate_page_courserun_overrides.py +++ b/cms/migrations/0045_certificate_page_courserun_overrides.py @@ -1,13 +1,13 @@ # Generated by Django 2.2.8 on 2020-01-31 14:31 -import cms.blocks -from django.db import migrations import wagtail.blocks import wagtail.fields +from django.db import migrations +import cms.blocks -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [("cms", "0044_external_course_cms_page")] operations = [ diff --git a/cms/migrations/0047_externalprogrampage.py b/cms/migrations/0047_externalprogrampage.py index d45b8b90c..bf533b366 100644 --- a/cms/migrations/0047_externalprogrampage.py +++ b/cms/migrations/0047_externalprogrampage.py @@ -1,15 +1,14 @@ # Generated by Django 2.2.13 on 2021-01-06 09:51 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks import wagtailmetadata.models +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0022_uploadedimage"), ("wagtailcore", "0045_assign_unlock_grouppagepermission"), diff --git a/cms/migrations/0048_external_course_selection_carousel.py b/cms/migrations/0048_external_course_selection_carousel.py index fabf0bda8..29d58dd82 100644 --- a/cms/migrations/0048_external_course_selection_carousel.py +++ b/cms/migrations/0048_external_course_selection_carousel.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.13 on 2021-01-11 12:37 -from django.db import migrations import wagtail.blocks import wagtail.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [("cms", "0047_externalprogrampage")] operations = [ diff --git a/cms/migrations/0049_positive_fields_constraints.py b/cms/migrations/0049_positive_fields_constraints.py index 6bfaf0fd1..ed1f087f4 100644 --- a/cms/migrations/0049_positive_fields_constraints.py +++ b/cms/migrations/0049_positive_fields_constraints.py @@ -1,12 +1,12 @@ # Generated by Django 2.2.13 on 2021-02-01 12:40 from decimal import Decimal + import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("cms", "0048_external_course_selection_carousel")] operations = [ diff --git a/cms/migrations/0050_news_and_events_page.py b/cms/migrations/0050_news_and_events_page.py index 9e287078d..9e57b754e 100644 --- a/cms/migrations/0050_news_and_events_page.py +++ b/cms/migrations/0050_news_and_events_page.py @@ -1,14 +1,13 @@ # Generated by Django 2.2.13 on 2021-02-09 12:51 -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0045_assign_unlock_grouppagepermission"), ("cms", "0049_positive_fields_constraints"), diff --git a/cms/migrations/0053_certificatepage_partner_logo.py b/cms/migrations/0053_certificatepage_partner_logo.py index 5195ff9ea..3549ced50 100644 --- a/cms/migrations/0053_certificatepage_partner_logo.py +++ b/cms/migrations/0053_certificatepage_partner_logo.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.14 on 2022-08-23 10:48 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0023_add_choose_permissions"), ("cms", "0052_new_page_data_migrations"), diff --git a/cms/migrations/0054_create_external_courseware_asociations.py b/cms/migrations/0054_create_external_courseware_asociations.py index 32ce76675..c66edbce5 100644 --- a/cms/migrations/0054_create_external_courseware_asociations.py +++ b/cms/migrations/0054_create_external_courseware_asociations.py @@ -2,8 +2,9 @@ from datetime import datetime, timezone -from django.db import migrations, models + import django.db.models.deletion +from django.db import migrations, models # Importing here because we need to use methods from this model and replicating the functionality # would make the migrations complex since it would include replication of some of the Wagtail's Page model. @@ -163,7 +164,6 @@ def migrate_external_courseware(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("courses", "0030_add_courseware_external_fields"), ("cms", "0053_certificatepage_partner_logo"), @@ -239,5 +239,5 @@ class Migration(migrations.Migration): ), # Commenting this, The fields in this data migration are being removed now as cleanup. # So the build fails on the fresh instance since it runs this migration - # migrations.RunPython(migrate_external_courseware, migrations.RunPython.noop), + # migrations.RunPython(migrate_external_courseware, migrations.RunPython.noop), # noqa: ERA001 ] diff --git a/cms/migrations/0055_associate_courseware_page_with_topics.py b/cms/migrations/0055_associate_courseware_page_with_topics.py index 6d9f29e27..6e10fe114 100644 --- a/cms/migrations/0055_associate_courseware_page_with_topics.py +++ b/cms/migrations/0055_associate_courseware_page_with_topics.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.18 on 2023-03-24 11:36 -from django.db import migrations import modelcluster.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("courses", "0031_create_topics_sublevel"), ("cms", "0054_create_external_courseware_asociations"), diff --git a/cms/migrations/0057_mark_topics_not_required.py b/cms/migrations/0057_mark_topics_not_required.py index d3bd941ae..b946fcaf7 100644 --- a/cms/migrations/0057_mark_topics_not_required.py +++ b/cms/migrations/0057_mark_topics_not_required.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.18 on 2023-04-17 11:22 -from django.db import migrations import modelcluster.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("courses", "0031_create_topics_sublevel"), ("cms", "0056_prepopulate_coursepage_topics"), diff --git a/cms/migrations/0058_add_external_marketing_url_product.py b/cms/migrations/0058_add_external_marketing_url_product.py index bfc4cb25f..bfc33c87d 100644 --- a/cms/migrations/0058_add_external_marketing_url_product.py +++ b/cms/migrations/0058_add_external_marketing_url_product.py @@ -77,5 +77,5 @@ class Migration(migrations.Migration): ), # Commenting this because we won't need to run data migration after the data has been migrated # The data migration was done in https://github.com/mitodl/mitxpro/pull/2628/ - # migrations.RunPython(migrate_external_marketing_url, migrations.RunPython.noop), + # migrations.RunPython(migrate_external_marketing_url, migrations.RunPython.noop), # noqa: ERA001 ] diff --git a/cms/migrations/0059_remove_unused_external_courseware_fields.py b/cms/migrations/0059_remove_unused_external_courseware_fields.py index 55f0d664b..7e662a818 100644 --- a/cms/migrations/0059_remove_unused_external_courseware_fields.py +++ b/cms/migrations/0059_remove_unused_external_courseware_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cms", "0058_add_external_marketing_url_product"), ] diff --git a/cms/migrations/0060_webinarindexpage_webinarpage.py b/cms/migrations/0060_webinarindexpage_webinarpage.py index 6a4259f90..af65d8996 100644 --- a/cms/migrations/0060_webinarindexpage_webinarpage.py +++ b/cms/migrations/0060_webinarindexpage_webinarpage.py @@ -8,7 +8,6 @@ class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0023_add_choose_permissions"), ("wagtailcore", "0062_comment_models_and_pagesubscription"), diff --git a/cms/migrations/0061_added_new_webinar_fields.py b/cms/migrations/0061_added_new_webinar_fields.py index 4747cd358..27d3e13b9 100644 --- a/cms/migrations/0061_added_new_webinar_fields.py +++ b/cms/migrations/0061_added_new_webinar_fields.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.18 on 2023-08-04 15:47 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0033_remove_course_coursetopic_association"), ("cms", "0060_webinarindexpage_webinarpage"), diff --git a/cms/migrations/0062_webinarpage_body_text.py b/cms/migrations/0062_webinarpage_body_text.py index 6b292bdc6..dc08c9767 100644 --- a/cms/migrations/0062_webinarpage_body_text.py +++ b/cms/migrations/0062_webinarpage_body_text.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cms", "0061_added_new_webinar_fields"), ] diff --git a/cms/migrations/0063_webinarindexpage_banner_image.py b/cms/migrations/0063_webinarindexpage_banner_image.py index 032f06599..32a048a6a 100644 --- a/cms/migrations/0063_webinarindexpage_banner_image.py +++ b/cms/migrations/0063_webinarindexpage_banner_image.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0023_add_choose_permissions"), ("cms", "0062_webinarpage_body_text"), diff --git a/cms/migrations/0064_productpage_format_field.py b/cms/migrations/0064_productpage_format_field.py index a3b8d491a..2b7ae0df5 100644 --- a/cms/migrations/0064_productpage_format_field.py +++ b/cms/migrations/0064_productpage_format_field.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cms", "0063_webinarindexpage_banner_image"), ] diff --git a/cms/migrations/0065_blogindexpage.py b/cms/migrations/0065_blogindexpage.py index 4e4d8f7a6..3d72e49d5 100644 --- a/cms/migrations/0065_blogindexpage.py +++ b/cms/migrations/0065_blogindexpage.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("wagtailimages", "0023_add_choose_permissions"), ("wagtailcore", "0062_comment_models_and_pagesubscription"), diff --git a/cms/migrations/0067_wagtail_5_upgrade.py b/cms/migrations/0067_wagtail_5_upgrade.py index 29f220af5..629186289 100644 --- a/cms/migrations/0067_wagtail_5_upgrade.py +++ b/cms/migrations/0067_wagtail_5_upgrade.py @@ -1,14 +1,14 @@ # Generated by Django 3.2.23 on 2023-11-22 07:34 -import cms.blocks -from django.db import migrations import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations +import cms.blocks -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ ("cms", "0066_fix_revision_content"), ] diff --git a/cms/migrations/0068_enterprisepage.py b/cms/migrations/0068_enterprisepage.py index 08c3e4cde..230b9cad5 100644 --- a/cms/migrations/0068_enterprisepage.py +++ b/cms/migrations/0068_enterprisepage.py @@ -1,15 +1,15 @@ # Generated by Django 3.2.23 on 2024-01-11 10:03 -import cms.models -from django.db import migrations, models import django.db.models.deletion import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations, models +import cms.models -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ ("wagtailcore", "0089_log_entry_data_json_null_to_object"), ("wagtailimages", "0025_alter_image_file_alter_rendition_file"), diff --git a/cms/migrations/0069_signup_for_more_information.py b/cms/migrations/0069_signup_for_more_information.py index 72d5a2a90..76b1e8d93 100644 --- a/cms/migrations/0069_signup_for_more_information.py +++ b/cms/migrations/0069_signup_for_more_information.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cms", "0068_enterprisepage"), ] diff --git a/cms/models.py b/cms/models.py index f49e22f32..54cdda606 100644 --- a/cms/models.py +++ b/cms/models.py @@ -1,7 +1,6 @@ """ Page models for the CMS """ -# pylint: disable=too-many-lines, too-many-public-methods import re from collections import defaultdict from datetime import datetime, timedelta @@ -93,7 +92,7 @@ class DisableSitemapURLMixin: """Mixin to Disable sitemap URLs""" - def get_sitemap_urls(self, request): + def get_sitemap_urls(self, request): # noqa: ARG002 """Disable sitemap urls for the page.""" return [] @@ -131,7 +130,7 @@ def get_child_by_readable_id(self, readable_id): """Fetch a child page by a Program/Course readable_id value""" raise NotImplementedError - def route(self, request, path_components): + def route(self, request, path_components): # noqa: D102 if path_components: # request is for a child of this page child_readable_id = path_components[0] @@ -142,12 +141,12 @@ def route(self, request, path_components): # instead of the page slug (as Wagtail does by default) subpage = self.get_child_by_readable_id(child_readable_id) except Page.DoesNotExist: - raise Http404 + raise Http404 # noqa: B904 return subpage.specific.route(request, remaining_components) return super().route(request, path_components) - def serve(self, request, *args, **kwargs): + def serve(self, request, *args, **kwargs): # noqa: ARG002 """ For index pages we raise a 404 because these pages do not have a template of their own and we do not expect a page to available at their slug. @@ -168,7 +167,7 @@ class Meta: parent_page_types = ["HomePage"] subpage_types = ["SignatoryPage"] - def serve(self, request, *args, **kwargs): + def serve(self, request, *args, **kwargs): # noqa: ARG002 """ For index pages we raise a 404 because these pages do not have a template of their own and we do not expect a page to available at their slug. @@ -213,7 +212,7 @@ def get_context(self, request, *args, **kwargs): .exclude(Q(category=UPCOMING_WEBINAR) & Q(date__lt=now_in_utc().date())) .order_by("-category", "date") ) - webinars_dict = defaultdict(lambda: []) + webinars_dict = defaultdict(lambda: []) # noqa: PIE807 for webinar in webinars: webinar.detail_page_url = webinar.detail_page_url(request) webinars_dict[webinar.category].append(webinar) @@ -248,21 +247,21 @@ class BlogIndexPage(Page): related_name="+", help_text="Banner image for the Blog page.", ) - sub_heading = models.CharField( + sub_heading = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, help_text="Sub heading of the blog page.", default="Online learning stories for professionals, from MIT", ) - recent_posts_heading = models.CharField( + recent_posts_heading = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, help_text="Heading of the recent posts section.", default="Top Most Recent Posts", ) - more_posts_heading = models.CharField( + more_posts_heading = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, @@ -324,23 +323,23 @@ class WebinarPage(MetadataPageMixin, Page): date = models.DateField( null=True, blank=True, help_text="The start date of the webinar." ) - time = models.TextField( + time = models.TextField( # noqa: DJ001 null=True, blank=True, help_text="The timings of the webinar e.g (11 AM - 12 PM ET).", ) - description = models.TextField( + description = models.TextField( # noqa: DJ001 null=True, blank=True, help_text="Description of the webinar." ) body_text = RichTextField( null=True, blank=True, help_text="Longer description text of the webinar." ) - action_url = models.URLField( + action_url = models.URLField( # noqa: DJ001 help_text="Specify the webinar action-url here (like a link to an external webinar page).", null=True, blank=True, ) - sub_heading = models.CharField( + sub_heading = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, @@ -386,7 +385,7 @@ def clean(self): if errors: raise ValidationError(errors) - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: ARG002, D102 course = CoursePage.objects.filter(course=self.course).first() program = ProgramPage.objects.filter(program=self.program).first() courseware = program or course @@ -402,11 +401,11 @@ def get_context(self, request, *args, **kwargs): @property def is_upcoming_webinar(self): - """returns a boolean that indicates whether a webinar is upcoming or not""" + """Returns a boolean that indicates whether a webinar is upcoming or not""" return self.category == UPCOMING_WEBINAR def detail_page_url(self, request): - """returns the detail page url for the webinar""" + """Returns the detail page url for the webinar""" if self.is_upcoming_webinar: return self.action_url if self.action_url else "" @@ -414,7 +413,7 @@ def detail_page_url(self, request): @property def detail_page_button_title(self): - """returns the title of the webinar detail page button""" + """Returns the title of the webinar detail page button""" return ( UPCOMING_WEBINAR_BUTTON_TITLE if self.is_upcoming_webinar @@ -435,7 +434,7 @@ def get_child_by_readable_id(self, readable_id): # Try to find internal course page otherwise return external course page try: return self.get_children().get(coursepage__course__readable_id=readable_id) - except Exception: # pylint: disable=broad-except + except Exception: # noqa: BLE001 return self.get_children().get( externalcoursepage__course__readable_id=readable_id ) @@ -468,7 +467,7 @@ def get_child_by_readable_id(self, readable_id): return self.get_children().get( programpage__program__readable_id=readable_id ) - except Exception: # pylint: disable=broad-except + except Exception: # noqa: BLE001 return self.get_children().get( externalprogrampage__program__readable_id=readable_id ) @@ -505,7 +504,7 @@ def can_create_at(cls, parent): slug = "catalog" - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: ARG002 """ Populate the context with live programs, courses and programs + courses """ @@ -641,8 +640,12 @@ def can_create_at(cls, parent): @route(r"^program/([A-Fa-f0-9-]+)/?$") def program_certificate( - self, request, uuid, *args, **kwargs - ): # pylint: disable=unused-argument + self, + request, + uuid, + *args, # noqa: ARG002 + **kwargs, # noqa: ARG002 + ): """ Serve a program certificate by uuid """ @@ -650,7 +653,7 @@ def program_certificate( try: certificate = ProgramCertificate.objects.get(uuid=uuid) except ProgramCertificate.DoesNotExist: - raise Http404() + raise Http404 # noqa: B904 # Get a CertificatePage to serve this request certificate_page = ( @@ -663,7 +666,7 @@ def program_certificate( ) ) if not certificate_page: - raise Http404() + raise Http404 if not certificate.certificate_page_revision: # It'll save the certificate page revision @@ -675,8 +678,12 @@ def program_certificate( @route(r"^([A-Fa-f0-9-]+)/?$") def course_certificate( - self, request, uuid, *args, **kwargs - ): # pylint: disable=unused-argument + self, + request, + uuid, + *args, # noqa: ARG002 + **kwargs, # noqa: ARG002 + ): """ Serve a course certificate by uuid """ @@ -684,7 +691,7 @@ def course_certificate( try: certificate = CourseRunCertificate.objects.get(uuid=uuid) except CourseRunCertificate.DoesNotExist: - raise Http404() + raise Http404 # noqa: B904 # Get a CertificatePage to serve this request certificate_page = ( @@ -697,7 +704,7 @@ def course_certificate( ) ) if not certificate_page: - raise Http404() + raise Http404 if not certificate.certificate_page_revision: certificate.save() @@ -706,11 +713,11 @@ def course_certificate( return certificate_page.serve(request) @route(r"^$") - def index_route(self, request, *args, **kwargs): + def index_route(self, request, *args, **kwargs): # noqa: ARG002 """ The index page is not meant to be served/viewed directly """ - raise Http404() + raise Http404 class WagtailCachedPageMixin: @@ -754,13 +761,13 @@ class HomePage(RoutablePageMixin, MetadataPageMixin, WagtailCachedPageMixin, Pag related_name="+", help_text="Background image size must be at least 1900x650 pixels.", ) - background_video_url = models.URLField( + background_video_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="Background video that should play over the hero section. Must be an HLS video URL. Will cover background image if selected.", ) - content_panels = Page.content_panels + [ + content_panels = Page.content_panels + [ # noqa: RUF005 FieldPanel("subhead"), FieldPanel("background_image"), FieldPanel("background_video_url"), @@ -840,7 +847,7 @@ def image_carousel_section(self): """ return self._get_child_page_of_type(ImageCarouselPage) - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: ARG002, D102 return { **super().get_context(request), **get_base_context(request), @@ -870,7 +877,7 @@ class Meta: description = RichTextField( blank=True, help_text="The description shown on the product page" ) - external_marketing_url = models.URLField( + external_marketing_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="The URL of the external course web page." ) marketing_hubspot_form_id = models.CharField( @@ -889,12 +896,12 @@ class Meta: video_title = RichTextField( blank=True, help_text="The title to be displayed for the program/course video" ) - video_url = models.URLField( + video_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="URL to the video to be displayed for this program/course. It can be an HLS or Youtube video URL.", ) - duration = models.CharField( + duration = models.CharField( # noqa: DJ001 max_length=50, null=True, blank=True, @@ -918,12 +925,12 @@ class Meta: related_name="+", help_text="Background image size must be at least 1900x650 pixels.", ) - background_video_url = models.URLField( + background_video_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="Background video that should play over the hero section. Must be an HLS video URL. Will cover background image if selected.", ) - time_commitment = models.CharField( + time_commitment = models.CharField( # noqa: DJ001 max_length=100, null=True, blank=True, @@ -953,7 +960,7 @@ class Meta: help_text="The content of this tab on the program page", use_json_field=True, ) - content_panels = Page.content_panels + [ + content_panels = Page.content_panels + [ # noqa: RUF005 FieldPanel("external_marketing_url"), FieldPanel("marketing_hubspot_form_id"), FieldPanel("subhead"), @@ -987,7 +994,7 @@ class Meta: # Matches the standard page path that Wagtail returns for this page type. slugged_page_path_pattern = re.compile(r"(^.*/)([^/]+)(/?$)") - def get_url_parts(self, request=None): + def get_url_parts(self, request=None): # noqa: D102 url_parts = super().get_url_parts(request=request) if not url_parts: return None @@ -1000,12 +1007,12 @@ def get_url_parts(self, request=None): # of the Course/Program instead (e.g.: "/courses/course-v1:edX+DemoX+Demo_Course") re.sub( self.slugged_page_path_pattern, - r"\1{}\3".format(self.product.readable_id), + r"\1{}\3".format(self.product.readable_id), # noqa: UP032 url_parts[2], ), ) - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: D102 return { **super().get_context(request, *args, **kwargs), **get_base_context(request), @@ -1025,7 +1032,7 @@ def get_context(self, request, *args, **kwargs): "news_and_events": self.news_and_events, } - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: FBT002 """If featured is True then set False in any existing product page(s).""" if self.featured: courseware_subclasses = ( @@ -1177,7 +1184,7 @@ class Meta: parent_page_types = ["ProgramIndexPage"] content_panels = ( - [FieldPanel("program")] + ProductPage.content_panels + [FieldPanel("price")] + [FieldPanel("program")] + ProductPage.content_panels + [FieldPanel("price")] # noqa: RUF005 ) base_form_class = CoursewareForm @@ -1224,7 +1231,7 @@ def program_page(self): """ return self - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: ARG002, D102 # Hits a circular import at the top of the module from courses.models import ProgramEnrollment @@ -1250,7 +1257,7 @@ def get_context(self, request, *args, **kwargs): ), ) product = ( - list(program.products.all())[0] + list(program.products.all())[0] # noqa: RUF015 if program and program.products.all() else None ) @@ -1312,7 +1319,7 @@ class Meta: parent_page_types = ["CourseIndexPage"] content_panels = ( - [ + [ # noqa: RUF005 FieldPanel("course"), FieldPanel("topics"), ] @@ -1404,12 +1411,12 @@ class CoursePage(CourseProductPage): template = "product_page.html" - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: ARG002, D102 # Hits a circular import at the top of the module from courses.models import CourseRunEnrollment run = self.course_with_related_objects.first_unexpired_run - product = list(run.products.all())[0] if run and run.products.all() else None + product = list(run.products.all())[0] if run and run.products.all() else None # noqa: RUF015 is_anonymous = request.user.is_anonymous enrolled = ( CourseRunEnrollment.objects.filter(user=request.user, run=run).exists() @@ -1472,18 +1479,18 @@ class Meta: promote_panels = [] @classmethod - def can_create_at(cls, parent): + def can_create_at(cls, parent): # noqa: D102 # You can only create one of these page under course / program. return ( - super(CourseProgramChildPage, cls).can_create_at(parent) + super(CourseProgramChildPage, cls).can_create_at(parent) # noqa: UP008 and parent.get_children().type(cls).count() == 0 ) - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: D102, FBT002 # autogenerate a unique slug so we don't hit a ValidationError if not self.title: - self.title = self.__class__._meta.verbose_name.title() - self.slug = slugify("{}-{}".format(self.get_parent().id, self.title)) + self.title = self.__class__._meta.verbose_name.title() # noqa: SLF001 + self.slug = slugify(f"{self.get_parent().id}-{self.title}") super().save(clean=clean, user=user, log_action=log_action, **kwargs) def get_url_parts(self, request=None): @@ -1503,12 +1510,12 @@ def get_url_parts(self, request=None): # Depending on whether we have trailing slashes or not, build the correct path if WAGTAIL_APPEND_SLASH: - page_path = "{}{}/".format(parent_path, self.slug) + page_path = f"{parent_path}{self.slug}/" else: - page_path = "{}/{}".format(parent_path, self.slug) + page_path = f"{parent_path}/{self.slug}" return (site_id, site_root, page_path) - def serve(self, request, *args, **kwargs): + def serve(self, request, *args, **kwargs): # noqa: ARG002 """ As the name suggests these pages are going to be children of some other page. They are not designed to be viewed on their own so we raise a 404 if someone tries to access their slug. @@ -1525,7 +1532,7 @@ class UserTestimonialsPage(CourseProgramChildPage): heading = models.CharField( max_length=255, help_text="The heading to display on this section." ) - subhead = models.CharField( + subhead = models.CharField( # noqa: DJ001 null=True, blank=True, max_length=255, @@ -1578,15 +1585,15 @@ class NewsAndEventsPage(DisableSitemapURLMixin, Page): class Meta: verbose_name = "News and Events" - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: D102, FBT002 # auto generate a unique slug so we don't hit a ValidationError if not self.title: - self.title = self.__class__._meta.verbose_name.title() + self.title = self.__class__._meta.verbose_name.title() # noqa: SLF001 - self.slug = slugify("{}-{}".format(self.title, self.id)) + self.slug = slugify(f"{self.title}-{self.id}") super().save(clean=clean, user=user, log_action=log_action, **kwargs) - def serve(self, request, *args, **kwargs): + def serve(self, request, *args, **kwargs): # noqa: ARG002 """ As the name suggests these pages are going to be children of some other page. They are not designed to be viewed on their own so we raise a 404 if someone tries to access their slug. @@ -1662,7 +1669,7 @@ class ForTeamsPage(CourseProgramChildPage): action_title = models.CharField( max_length=255, help_text="The text to show on the call to action button" ) - action_url = models.URLField( + action_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="The URL to go to when the action button is clicked.", @@ -1706,13 +1713,13 @@ class TextSection(CourseProgramChildPage): """ content = RichTextField(help_text="The content shown in the section") - action_title = models.CharField( + action_title = models.CharField( # noqa: DJ001 null=True, blank=True, max_length=255, help_text="The text to show on the call to action button. Note: action button is visible only when both url and title are configured.", ) - action_url = models.URLField( + action_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="The URL to go to when the action button is clicked. Note: action button is visible only when both url and title are configured.", @@ -1738,13 +1745,13 @@ class TextVideoSection(CourseProgramChildPage): """ content = RichTextField(help_text="The content shown in the section") - action_title = models.CharField( + action_title = models.CharField( # noqa: DJ001 null=True, blank=True, max_length=255, help_text="The text to show on the call to action button", ) - action_url = models.URLField( + action_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="The URL to go to when the action button is clicked.", @@ -1759,7 +1766,7 @@ class TextVideoSection(CourseProgramChildPage): default=False, help_text="When checked, switches the position of the content and video, i.e. video on left and content on right.", ) - video_url = models.URLField( + video_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="The URL of the video to display. It can be an HLS or Youtube video URL.", @@ -1868,11 +1875,7 @@ def content_pages(self): """ Extracts all the pages out of the `contents` stream into a list """ - pages = [] - for block in self.contents: # pylint: disable=not-an-iterable - if block.value: - pages.append(block.value.specific) - return pages + return [block.value.specific for block in self.contents if block.value] class Meta: verbose_name = "Courseware Carousel" @@ -1894,7 +1897,7 @@ class FacultyMembersPage(CourseProgramChildPage): max_length=255, help_text="The heading to display for this section on the product page.", ) - subhead = models.CharField( + subhead = models.CharField( # noqa: DJ001 null=True, blank=True, max_length=255, @@ -1946,10 +1949,10 @@ class FrequentlyAskedQuestionPage(CourseProgramChildPage): content_panels = [InlinePanel("faqs", label="Frequently Asked Questions")] - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: D102, FBT002 # autogenerate a unique slug so we don't hit a ValidationError self.title = "Frequently Asked Questions" - self.slug = slugify("{}-{}".format(self.get_parent().id, self.title)) + self.slug = slugify(f"{self.get_parent().id}-{self.title}") super().save(clean=clean, user=user, log_action=log_action, **kwargs) @@ -1970,7 +1973,7 @@ class ResourcePage(Page): template = "../../mitxpro/templates/resource_template.html" - sub_heading = models.CharField( + sub_heading = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, @@ -1984,12 +1987,12 @@ class ResourcePage(Page): use_json_field=True, ) - content_panels = Page.content_panels + [ + content_panels = Page.content_panels + [ # noqa: RUF005 FieldPanel("sub_heading"), FieldPanel("content"), ] - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: ARG002, D102 context = super().get_context(request) context.update(**get_base_context(request)) @@ -2006,19 +2009,19 @@ class SignatoryPage(DisableSitemapURLMixin, Page): name = models.CharField( max_length=250, null=False, blank=False, help_text="Name of the signatory." ) - title_1 = models.CharField( + title_1 = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, help_text="Specify signatory first title in organization.", ) - title_2 = models.CharField( + title_2 = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, help_text="Specify signatory second title in organization.", ) - organization = models.CharField( + organization = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, @@ -2045,15 +2048,15 @@ class Meta: FieldPanel("signature_image"), ] - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: D102, FBT002 # auto generate a unique slug so we don't hit a ValidationError if not self.title: - self.title = self.__class__._meta.verbose_name.title() + "-" + self.name + self.title = self.__class__._meta.verbose_name.title() + "-" + self.name # noqa: SLF001 - self.slug = slugify("{}-{}".format(self.title, self.id)) + self.slug = slugify(f"{self.title}-{self.id}") super().save(clean=clean, user=user, log_action=log_action, **kwargs) - def serve(self, request, *args, **kwargs): + def serve(self, request, *args, **kwargs): # noqa: ARG002 """ As the name suggests these pages are going to be children of some other page. They are not designed to be viewed on their own so we raise a 404 if someone tries to access their slug. @@ -2091,11 +2094,11 @@ class PartnerLogoPlacement(models.IntegerChoices): help_text="Specify the course/program name.", ) - institute_text = models.CharField( + institute_text = models.CharField( # noqa: DJ001 max_length=255, null=True, blank=True, help_text="Specify the institute text" ) - CEUs = models.CharField( + CEUs = models.CharField( # noqa: DJ001 max_length=250, null=True, blank=True, @@ -2161,15 +2164,15 @@ def __init__(self, *args, **kwargs): self.certificate = None super().__init__(*args, **kwargs) - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: D102, FBT002 # auto generate a unique slug so we don't hit a ValidationError self.title = ( - self.__class__._meta.verbose_name.title() + self.__class__._meta.verbose_name.title() # noqa: SLF001 + " For " + self.get_parent().title ) - self.slug = slugify("certificate-{}".format(self.get_parent().id)) + self.slug = slugify(f"certificate-{self.get_parent().id}") Page.save(self, clean=clean, user=user, log_action=log_action, **kwargs) def serve(self, request, *args, **kwargs): @@ -2183,11 +2186,7 @@ def signatory_pages(self): """ Extracts all the pages out of the `signatories` stream into a list """ - pages = [] - for block in self.signatories: # pylint: disable=not-an-iterable - if block.value: - pages.append(block.value.specific) - return pages + return [block.value.specific for block in self.signatories if block.value] @property def parent(self): @@ -2196,7 +2195,7 @@ def parent(self): """ return self.get_parent().specific - def get_context(self, request, *args, **kwargs): + def get_context(self, request, *args, **kwargs): # noqa: D102 preview_context = {} context = {} @@ -2205,20 +2204,20 @@ def get_context(self, request, *args, **kwargs): "learner_name": "Anthony M. Stark", "start_date": self.parent.product.first_unexpired_run.start_date if self.parent.product.first_unexpired_run - else datetime.now(), + else datetime.now(), # noqa: DTZ005 "end_date": self.parent.product.first_unexpired_run.end_date if self.parent.product.first_unexpired_run - else datetime.now() + timedelta(days=45), + else datetime.now() + timedelta(days=45), # noqa: DTZ005 "CEUs": self.CEUs, } elif self.certificate: # Verify that the certificate in fact is for this same course if self.parent.product.id != self.certificate.get_courseware_object_id(): - raise Http404() + raise Http404 start_date, end_date = self.certificate.start_end_dates CEUs = self.CEUs - for override in self.overrides: # pylint: disable=not-an-iterable + for override in self.overrides: if ( override.value.get("readable_id") == self.certificate.get_courseware_object_readable_id() @@ -2240,7 +2239,7 @@ def get_context(self, request, *args, **kwargs): "is_program_certificate": is_program_certificate, } else: - raise Http404() + raise Http404 # The share image url needs to be absolute return { @@ -2293,7 +2292,7 @@ class Meta: @classmethod def can_create_at(cls, parent): """ - Ensures that only one instance of this page type can be created + Ensure that only one instance of this page type can be created under each parent. """ return ( @@ -2301,21 +2300,21 @@ def can_create_at(cls, parent): and not parent.get_children().type(cls).exists() ) - def save(self, clean=True, user=None, log_action=False, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: FBT002 """ Auto-generates a slug for this page if it doesn't already have one. The slug is generated from the page title and its ID to ensure uniqueness. """ if not self.title: - self.title = self.__class__._meta.verbose_name.title() + self.title = self.__class__._meta.verbose_name.title() # noqa: SLF001 if not self.slug: self.slug = slugify(f"{self.title}-{self.id}") super().save(clean=clean, user=user, log_action=log_action, **kwargs) - def serve(self, request, *args, **kwargs): + def serve(self, request, *args, **kwargs): # noqa: ARG002 """ Prevents direct access to this page type by raising a 404 error. @@ -2374,7 +2373,7 @@ class LearningJourneySection(EnterpriseChildPage): default="View Full Diagram", help_text="Text for the call-to-action button.", ) - action_url = models.URLField( + action_url = models.URLField( # noqa: DJ001 null=True, blank=True, help_text="URL for the call-to-action button, used if no PDF is linked.", @@ -2414,11 +2413,11 @@ def button_url(self): return self.pdf_file.url if self.pdf_file else self.action_url def clean(self): - """Validates that either action_url or pdf_file must be added.""" + """Validate that either action_url or pdf_file must be added.""" super().clean() if not self.action_url and not self.pdf_file: raise ValidationError( - "Please enter an Action URL or select a PDF document." + "Please enter an Action URL or select a PDF document." # noqa: EM101 ) class Meta: @@ -2528,7 +2527,7 @@ class EnterprisePage(WagtailCachedPageMixin, Page): help_text="The text to show on the call to action button", ) - content_panels = Page.content_panels + [ + content_panels = Page.content_panels + [ # noqa: RUF005 FieldPanel("headings"), FieldPanel("background_image"), FieldPanel("overlay_image"), @@ -2541,7 +2540,7 @@ class Meta: def serve(self, request, *args, **kwargs): """ - Serves the enterprise page. + Serve the enterprise page. This method is overridden to handle specific rendering needs for the enterprise template, especially during previews. @@ -2578,7 +2577,7 @@ def learning_strategy_form(self): def get_context(self, request, *args, **kwargs): """ - Builds the context for rendering the enterprise page. + Build the context for rendering the enterprise page. """ return { **super().get_context(request, *args, **kwargs), diff --git a/cms/models_test.py b/cms/models_test.py index 35305a3c4..be8f2beb7 100644 --- a/cms/models_test.py +++ b/cms/models_test.py @@ -1,5 +1,4 @@ -""" Tests for cms pages. """ -# pylint: disable=too-many-lines +"""Tests for cms pages.""" import json from datetime import date, datetime, timedelta @@ -40,7 +39,6 @@ LearningTechniquesPageFactory, NewsAndEventsPageFactory, ProgramFactory, - ProgramIndexPageFactory, ProgramPageFactory, ResourcePageFactory, SignatoryPageFactory, @@ -67,7 +65,6 @@ ) from courses.factories import CourseFactory, CourseRunFactory - pytestmark = [pytest.mark.django_db] @@ -94,7 +91,7 @@ def test_resource_page(): assert page.title == "title of the page" assert page.sub_heading == "sub heading of the page" - for block in page.content: # pylint: disable=not-an-iterable + for block in page.content: assert block.block_type == "content" assert block.value["heading"] == "Introduction" assert block.value["detail"].source == "details of introduction" @@ -153,10 +150,10 @@ def test_webinar_context(staff_user): @pytest.mark.parametrize( - "time, webinar_date,", - ( - ["11 am", datetime.today() + timedelta(days=1)], - [None, None], + "time, webinar_date,", # noqa: PT006 + ( # noqa: PT007 + ["11 am", datetime.today() + timedelta(days=1)], # noqa: DTZ002, PT007 + [None, None], # noqa: PT007 ), ) def test_upcoming_webinar_date_time(time, webinar_date): @@ -272,25 +269,19 @@ def test_custom_detail_page_urls(): [external_readable_id, "non-matching-external-id"] ), ) - assert program_pages[0].get_url() == "/programs/{}/".format(readable_id) - assert external_program_pages[0].get_url() == "/programs/{}/".format( - external_readable_id - ) - assert course_pages[0].get_url() == "/courses/{}/".format(readable_id) - assert external_course_pages[0].get_url() == "/courses/{}/".format( - external_readable_id - ) + assert program_pages[0].get_url() == f"/programs/{readable_id}/" + assert external_program_pages[0].get_url() == f"/programs/{external_readable_id}/" + assert course_pages[0].get_url() == f"/courses/{readable_id}/" + assert external_course_pages[0].get_url() == f"/courses/{external_readable_id}/" def test_custom_detail_page_urls_handled(): """Verify that custom URL paths for our course/program are served by the standard Wagtail view""" readable_id = "some:readable-id" CoursePageFactory.create(course__readable_id=readable_id) - resolver_match = resolve("/courses/{}/".format(readable_id)) - assert ( - resolver_match.func.__module__ == "wagtail.views" - ) # pylint: disable=protected-access - assert resolver_match.func.__name__ == "serve" # pylint: disable=protected-access + resolver_match = resolve(f"/courses/{readable_id}/") + assert resolver_match.func.__module__ == "wagtail.views" + assert resolver_match.func.__name__ == "serve" def test_home_page(): @@ -327,7 +318,7 @@ def test_home_page_testimonials(): assert home_page.testimonials == testimonials_page assert testimonials_page.heading == "heading" assert testimonials_page.subhead == "subhead" - for testimonial in testimonials_page.items: # pylint: disable=not-an-iterable + for testimonial in testimonials_page.items: assert testimonial.value.get("name") == "name" assert testimonial.value.get("title") == "title" assert testimonial.value.get("image").title == "image" @@ -448,7 +439,7 @@ def test_image_carousel_section(): assert home_page.image_carousel_section == image_carousel_page assert image_carousel_page.title == "title" for index, image in enumerate(image_carousel_page.images): - assert image.value.title == "image-title-{}".format(index) + assert image.value.title == f"image-title-{index}" def test_program_page_faculty_subpage(): @@ -547,7 +538,7 @@ def test_course_page_testimonials(): assert course_page.testimonials == testimonials_page assert testimonials_page.heading == "heading" assert testimonials_page.subhead == "subhead" - for testimonial in testimonials_page.items: # pylint: disable=not-an-iterable + for testimonial in testimonials_page.items: assert testimonial.value.get("name") == "name" assert testimonial.value.get("title") == "title" assert testimonial.value.get("image").title == "image" @@ -572,7 +563,7 @@ def test_external_course_page_testimonials(): assert external_course_page.testimonials == testimonials_page assert testimonials_page.heading == "heading" assert testimonials_page.subhead == "subhead" - for testimonial in testimonials_page.items: # pylint: disable=not-an-iterable + for testimonial in testimonials_page.items: assert testimonial.value.get("name") == "name" assert testimonial.value.get("title") == "title" assert testimonial.value.get("image").title == "image" @@ -597,7 +588,7 @@ def test_program_page_testimonials(): assert program_page.testimonials == testimonials_page assert testimonials_page.heading == "heading" assert testimonials_page.subhead == "subhead" - for testimonial in testimonials_page.items: # pylint: disable=not-an-iterable + for testimonial in testimonials_page.items: assert testimonial.value.get("name") == "name" assert testimonial.value.get("title") == "title" assert testimonial.value.get("image").title == "image" @@ -622,7 +613,7 @@ def test_external_program_page_testimonials(): assert external_program_page.testimonials == testimonials_page assert testimonials_page.heading == "heading" assert testimonials_page.subhead == "subhead" - for testimonial in testimonials_page.items: # pylint: disable=not-an-iterable + for testimonial in testimonials_page.items: assert testimonial.value.get("name") == "name" assert testimonial.value.get("title") == "title" assert testimonial.value.get("image").title == "image" @@ -650,9 +641,9 @@ def test_program_page_child_page_url(): child_page_url = child_page.get_full_url() if WAGTAIL_APPEND_SLASH: - assert child_page_url == "{}{}/".format(program_page_url, child_page.slug) + assert child_page_url == f"{program_page_url}{child_page.slug}/" else: - assert child_page_url == "{}/{}".format(program_page_url, child_page.slug) + assert child_page_url == f"{program_page_url}/{child_page.slug}" def test_course_page_child_page_url(): @@ -667,9 +658,9 @@ def test_course_page_child_page_url(): child_page_url = child_page.get_full_url() if WAGTAIL_APPEND_SLASH: - assert child_page_url == "{}{}/".format(course_page_url, child_page.slug) + assert child_page_url == f"{course_page_url}{child_page.slug}/" else: - assert child_page_url == "{}/{}".format(course_page_url, child_page.slug) + assert child_page_url == f"{course_page_url}/{child_page.slug}" def test_course_page_for_teams(): @@ -997,9 +988,7 @@ def test_course_page_learning_outcomes(): assert learning_outcomes_page.get_parent() == course_page assert learning_outcomes_page.heading == "heading" assert learning_outcomes_page.sub_heading == "

subheading

" - for ( - block - ) in learning_outcomes_page.outcome_items: # pylint: disable=not-an-iterable + for block in learning_outcomes_page.outcome_items: assert block.block_type == "outcome" assert block.value == "benefit" @@ -1028,9 +1017,7 @@ def test_external_course_page_learning_outcomes(): assert learning_outcomes_page.get_parent() == external_course_page assert learning_outcomes_page.heading == "heading" assert learning_outcomes_page.sub_heading == "

subheading

" - for ( - block - ) in learning_outcomes_page.outcome_items: # pylint: disable=not-an-iterable + for block in learning_outcomes_page.outcome_items: assert block.block_type == "outcome" assert block.value == "benefit" @@ -1057,9 +1044,7 @@ def test_program_learning_outcomes(): ) assert learning_outcomes_page.get_parent() == program_page assert learning_outcomes_page.heading == "heading" - for ( - block - ) in learning_outcomes_page.outcome_items: # pylint: disable=not-an-iterable + for block in learning_outcomes_page.outcome_items: assert block.block_type == "outcome" assert block.value == "benefit" assert program_page.outcomes == learning_outcomes_page @@ -1082,9 +1067,7 @@ def test_external_program_learning_outcomes(): ) assert learning_outcomes_page.get_parent() == external_program_page assert learning_outcomes_page.heading == "heading" - for ( - block - ) in learning_outcomes_page.outcome_items: # pylint: disable=not-an-iterable + for block in learning_outcomes_page.outcome_items: assert block.block_type == "outcome" assert block.value == "benefit" assert external_program_page.outcomes == learning_outcomes_page @@ -1106,9 +1089,7 @@ def test_course_page_learning_techniques(): technique_items__0__techniques__image__image__title="image-title", ) assert learning_techniques_page.get_parent() == course_page - for ( - technique - ) in learning_techniques_page.technique_items: # pylint: disable=not-an-iterable + for technique in learning_techniques_page.technique_items: assert technique.value.get("heading") == "heading" assert technique.value.get("sub_heading") == "sub_heading" assert technique.value.get("image").title == "image-title" @@ -1129,9 +1110,7 @@ def test_external_course_page_learning_techniques(): technique_items__0__techniques__image__image__title="image-title", ) assert learning_techniques_page.get_parent() == external_course_page - for ( - technique - ) in learning_techniques_page.technique_items: # pylint: disable=not-an-iterable + for technique in learning_techniques_page.technique_items: assert technique.value.get("heading") == "heading" assert technique.value.get("sub_heading") == "sub_heading" assert technique.value.get("image").title == "image-title" @@ -1154,9 +1133,7 @@ def test_program_page_learning_techniques(): technique_items__0__techniques__image__image__title="image-title", ) assert learning_techniques_page.get_parent() == program_page - for ( - technique - ) in learning_techniques_page.technique_items: # pylint: disable=not-an-iterable + for technique in learning_techniques_page.technique_items: assert technique.value.get("heading") == "heading" assert technique.value.get("sub_heading") == "sub_heading" assert technique.value.get("image").title == "image-title" @@ -1179,9 +1156,7 @@ def test_external_program_page_learning_techniques(): technique_items__0__techniques__image__image__title="image-title", ) assert learning_techniques_page.get_parent() == external_program_page - for ( - technique - ) in learning_techniques_page.technique_items: # pylint: disable=not-an-iterable + for technique in learning_techniques_page.technique_items: assert technique.value.get("heading") == "heading" assert technique.value.get("sub_heading") == "sub_heading" assert technique.value.get("image").title == "image-title" @@ -1205,7 +1180,7 @@ def test_program_page_who_should_enroll(): ) assert who_should_enroll_page.get_parent() == program_page assert len(who_should_enroll_page.content) == 2 - for block in who_should_enroll_page.content: # pylint: disable=not-an-iterable + for block in who_should_enroll_page.content: assert block.block_type == "item" assert block.value.source == "

item

" assert program_page.who_should_enroll == who_should_enroll_page @@ -1240,7 +1215,7 @@ def test_external_program_page_who_should_enroll(): ) assert who_should_enroll_page.get_parent() == external_program_page assert len(who_should_enroll_page.content) == 2 - for block in who_should_enroll_page.content: # pylint: disable=not-an-iterable + for block in who_should_enroll_page.content: assert block.block_type == "item" assert block.value.source == "

item

" assert external_program_page.who_should_enroll == who_should_enroll_page @@ -1410,7 +1385,7 @@ def test_certificate_for_course_page(): assert certificate_page.CEUs == "1.8" assert certificate_page.product_name == "product_name" assert certificate_page.partner_logo.title == "Partner Logo" - for signatory in certificate_page.signatories: # pylint: disable=not-an-iterable + for signatory in certificate_page.signatories: assert signatory.value.name == "Name" assert signatory.value.title_1 == "Title_1" assert signatory.value.title_2 == "Title_2" @@ -1446,7 +1421,7 @@ def test_certificate_for_program_page(): assert certificate_page.CEUs == "2.8" assert certificate_page.product_name == "product_name" assert certificate_page.partner_logo.title == "Partner Logo" - for signatory in certificate_page.signatories: # pylint: disable=not-an-iterable + for signatory in certificate_page.signatories: assert signatory.value.name == "Name" assert signatory.value.title_1 == "Title_1" assert signatory.value.title_2 == "Title_2" @@ -1623,7 +1598,7 @@ def test_enterprise_page_companies_logo_carousel(): assert companies_logo_carousel.heading == "heading" for index, image in enumerate(companies_logo_carousel.images): - assert image.value.title == "image-title-{}".format(index) + assert image.value.title == f"image-title-{index}" def test_enterprise_page_learning_journey(): @@ -1650,7 +1625,7 @@ def test_enterprise_page_learning_journey(): assert learning_journey.heading == "heading" assert learning_journey.description == "description" - for block in learning_journey.journey_items: # pylint: disable=not-an-iterable + for block in learning_journey.journey_items: assert block.block_type == "journey" assert block.value == "value" diff --git a/cms/templatetags/image_version_url.py b/cms/templatetags/image_version_url.py index 87ed915e4..97a70564a 100644 --- a/cms/templatetags/image_version_url.py +++ b/cms/templatetags/image_version_url.py @@ -1,6 +1,7 @@ """CMS templatetags""" from urllib.parse import quote_plus, urljoin + from django import template from django.conf import settings from wagtail.images.templatetags.wagtailimages_tags import image_url @@ -10,7 +11,10 @@ @register.simple_tag() def image_version_url( - image, filter_spec, full_url=False, viewname="wagtailimages_serve" + image, + filter_spec, + full_url=False, # noqa: FBT002 + viewname="wagtailimages_serve", ): """ Generates an image URL using Wagtail's library and appends a version to the path to enable effective caching diff --git a/cms/urls.py b/cms/urls.py index c23e37d70..061566335 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -6,19 +6,17 @@ edX course ids, and those edX course ids contain characters that do not match Wagtail's expected URL pattern (https://github.com/wagtail/wagtail/blob/a657a75/wagtail/core/urls.py) -Example: "course-v1:edX+DemoX+Demo_Course" – Wagtail's pattern does not match the ":" or +Example: "course-v1:edX+DemoX+Demo_Course" - Wagtail's pattern does not match the ":" or the "+" characters. The pattern(s) defined here serve the same Wagtail view that the library-defined pattern serves. """ from django.urls import re_path - from wagtail import views from wagtail.coreutils import WAGTAIL_APPEND_SLASH from cms.constants import COURSE_INDEX_SLUG, PROGRAM_INDEX_SLUG, WEBINAR_INDEX_SLUG - index_page_pattern = r"(?:{}|{}|{})".format( COURSE_INDEX_SLUG, PROGRAM_INDEX_SLUG, WEBINAR_INDEX_SLUG ) @@ -26,13 +24,13 @@ if WAGTAIL_APPEND_SLASH: custom_serve_pattern = ( - r"^({index_page_pattern}/(?:[{resource_pattern}]+/)*)$".format( + r"^({index_page_pattern}/(?:[{resource_pattern}]+/)*)$".format( # noqa: UP032 index_page_pattern=index_page_pattern, resource_pattern=detail_path_char_pattern, ) ) else: - custom_serve_pattern = r"^({index_page_pattern}/[{resource_pattern}/]*)$".format( + custom_serve_pattern = r"^({index_page_pattern}/[{resource_pattern}/]*)$".format( # noqa: UP032 index_page_pattern=index_page_pattern, resource_pattern=detail_path_char_pattern ) diff --git a/cms/urls_test.py b/cms/urls_test.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/cms/views_test.py b/cms/views_test.py index 22afeb63b..5740283b9 100644 --- a/cms/views_test.py +++ b/cms/views_test.py @@ -18,11 +18,11 @@ WEBINAR_DEFAULT_IMAGES, ) from cms.factories import ( - EnterprisePageFactory, BlogIndexPageFactory, CatalogPageFactory, CourseIndexPageFactory, CoursePageFactory, + EnterprisePageFactory, HomePageFactory, ProgramIndexPageFactory, ProgramPageFactory, @@ -44,12 +44,10 @@ from ecommerce.factories import ProductVersionFactory from mitxpro.utils import now_in_utc - pytestmark = pytest.mark.django_db -# pylint: disable=redefined-outer-name,unused-argument -@pytest.fixture() +@pytest.fixture def wagtail_basics(): """Fixture for Wagtail objects that we expect to always exist""" site = Site.objects.get(is_default_site=True) @@ -84,9 +82,7 @@ def test_wagtail_items_ordering(client, admin_user): ) catalog_page = CatalogPageFactory.create(parent=home_page, title="Catalog") - resp = client.get( - "/cms/api/main/pages/?child_of={}&for_explorer=1".format(home_page.id) - ) + resp = client.get(f"/cms/api/main/pages/?child_of={home_page.id}&for_explorer=1") assert resp.status_code == status.HTTP_200_OK items = list(resp.data.items())[1][1] # Pages in response response_page_titles = [item["title"] for item in items] @@ -364,14 +360,14 @@ def test_catalog_page_product(client, wagtail_basics): @pytest.mark.parametrize( - "topic_filter, expected_courses_count, expected_program_count, expected_selected_topic", + "topic_filter, expected_courses_count, expected_program_count, expected_selected_topic", # noqa: PT006 [ - [None, 2, 2, ALL_TOPICS], - ["Engineering", 1, 1, "Engineering"], - ["RandomTopic", 0, 0, "RandomTopic"], + [None, 2, 2, ALL_TOPICS], # noqa: PT007 + ["Engineering", 1, 1, "Engineering"], # noqa: PT007 + ["RandomTopic", 0, 0, "RandomTopic"], # noqa: PT007 ], ) -def test_catalog_page_topics( # pylint: disable=too-many-arguments +def test_catalog_page_topics( # noqa: PLR0913 client, wagtail_basics, topic_filter, @@ -382,7 +378,6 @@ def test_catalog_page_topics( # pylint: disable=too-many-arguments """ Test that topic filters are working fine. """ - # pylint:disable=too-many-locals homepage = wagtail_basics.root catalog_page = CatalogPageFactory.create(parent=homepage) catalog_page.save_revision().publish() @@ -507,10 +502,10 @@ def test_program_page_for_program_run(client): ) page_base_url = program_page.get_url().rstrip("/") - good_url = "{}+{}/".format(page_base_url, program_run.run_tag) + good_url = f"{page_base_url}+{program_run.run_tag}/" resp = client.get(good_url) assert resp.status_code == 200 - bad_url = "{}+R2/".format(page_base_url) + bad_url = f"{page_base_url}+R2/" resp = client.get(bad_url) assert resp.status_code == 404 @@ -552,7 +547,7 @@ def test_webinar_formatted_date(wagtail_basics): webinar_index_page = WebinarIndexPageFactory.create(parent=homepage) webinar_index_page.save_revision().publish() - start_date = datetime.strptime("Tuesday, May 2, 2023", "%A, %B %d, %Y") + start_date = datetime.strptime("Tuesday, May 2, 2023", "%A, %B %d, %Y") # noqa: DTZ007 webinar = WebinarPageFactory.create(parent=webinar_index_page, date=start_date) assert webinar.formatted_date == "Tuesday, May 2, 2023" diff --git a/cms/wagtail_hooks.py b/cms/wagtail_hooks.py index 781d55b52..faa6b77e0 100644 --- a/cms/wagtail_hooks.py +++ b/cms/wagtail_hooks.py @@ -1,12 +1,14 @@ """Custom hooks to configure wagtail behavior""" -from wagtail.admin.api.views import PagesAdminAPIViewSet from wagtail import hooks +from wagtail.admin.api.views import PagesAdminAPIViewSet @hooks.register("construct_explorer_page_queryset") def sort_pages_alphabetically( - parent_page, pages, request -): # pylint: disable=unused-argument + parent_page, # noqa: ARG001 + pages, + request, # noqa: ARG001 +): """Sort all pages by title alphabetically""" return pages.order_by("title") diff --git a/compliance/admin.py b/compliance/admin.py index 699c83057..6a41ff809 100644 --- a/compliance/admin.py +++ b/compliance/admin.py @@ -36,7 +36,7 @@ def country(self, instance): return country.name if country else "N/A" @admin.action(description="Manually approve selected records") - def manually_approve_inquiry(self, request, queryset): + def manually_approve_inquiry(self, request, queryset): # noqa: ARG002 """Admin action to manually approve export compliance inquiry records""" eligible_objects = queryset.exclude( computed_result__in=[RESULT_MANUALLY_APPROVED, RESULT_SUCCESS] @@ -45,10 +45,10 @@ def manually_approve_inquiry(self, request, queryset): ensure_active_user(obj.user) eligible_objects.update(computed_result=RESULT_MANUALLY_APPROVED) - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 # We want to allow this while debugging return settings.DEBUG - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 # We want to allow this while debugging return settings.DEBUG diff --git a/compliance/api.py b/compliance/api.py index cdf0791db..f27c8bd15 100644 --- a/compliance/api.py +++ b/compliance/api.py @@ -1,6 +1,6 @@ """Compliance API""" -from collections import namedtuple import logging +from collections import namedtuple from django.conf import settings from lxml import etree @@ -11,19 +11,18 @@ from zeep.wsse.username import UsernameToken from compliance.constants import ( - REASON_CODE_SUCCESS, EXPORTS_BLOCKED_REASON_CODES, - TEMPORARY_FAILURE_REASON_CODES, + REASON_CODE_SUCCESS, RESULT_DENIED, RESULT_SUCCESS, RESULT_UNKNOWN, + TEMPORARY_FAILURE_REASON_CODES, ) from compliance.models import ExportsInquiryLog - log = logging.getLogger() -DecryptedLog = namedtuple("DecryptedLog", ["request", "response"]) +DecryptedLog = namedtuple("DecryptedLog", ["request", "response"]) # noqa: PYI024 EXPORTS_REQUIRED_KEYS = [ diff --git a/compliance/api_test.py b/compliance/api_test.py index 4806b8ff0..a1c51b22c 100644 --- a/compliance/api_test.py +++ b/compliance/api_test.py @@ -1,6 +1,4 @@ """Tests for compliance api""" - -# pylint: disable=redefined-outer-name import time import pytest @@ -10,8 +8,8 @@ from compliance import api from compliance.constants import ( - RESULT_SUCCESS, RESULT_DENIED, + RESULT_SUCCESS, RESULT_UNKNOWN, TEMPORARY_FAILURE_REASON_CODES, ) @@ -71,18 +69,18 @@ def test_log_exports_inquiry(mocker, cybersource_private_key, user): @pytest.mark.parametrize( - "cybersource_mock_client_responses, expected_result", + "cybersource_mock_client_responses, expected_result", # noqa: PT006 [ - ["700_reject", RESULT_DENIED], - ["100_success_match", RESULT_DENIED], - ["100_success", RESULT_SUCCESS], - ["978_unknown", RESULT_UNKNOWN], + ["700_reject", RESULT_DENIED], # noqa: PT007 + ["100_success_match", RESULT_DENIED], # noqa: PT007 + ["100_success", RESULT_SUCCESS], # noqa: PT007 + ["978_unknown", RESULT_UNKNOWN], # noqa: PT007 ], indirect=["cybersource_mock_client_responses"], ) def test_verify_user_with_exports( user, cybersource_mock_client_responses, expected_result -): # pylint: disable=unused-argument +): """Test that verify_user_with_exports handles""" result = api.verify_user_with_exports(user) @@ -117,7 +115,8 @@ def test_verify_user_with_exports_temporary_errors(mocker, user, reason_code): @pytest.mark.parametrize( - "sanctions_lists, expect_passed", [[None, False], ["", False], ["OFAC", True]] + "sanctions_lists, expect_passed", # noqa: PT006 + [[None, False], ["", False], ["OFAC", True]], # noqa: PT007 ) def test_verify_user_with_exports_sanctions_lists( mocker, user, cybersource_settings, sanctions_lists, expect_passed diff --git a/compliance/factories.py b/compliance/factories.py index b3b34cc7c..65de7686f 100644 --- a/compliance/factories.py +++ b/compliance/factories.py @@ -3,7 +3,7 @@ from factory.django import DjangoModelFactory from factory.fuzzy import FuzzyChoice -from compliance.constants import RESULT_DENIED, RESULT_SUCCESS, RESULT_CHOICES +from compliance.constants import RESULT_CHOICES, RESULT_DENIED, RESULT_SUCCESS from compliance.models import ExportsInquiryLog diff --git a/compliance/management/commands/decrypt_exports_inquiry.py b/compliance/management/commands/decrypt_exports_inquiry.py index afe5e715b..5a03c1d86 100644 --- a/compliance/management/commands/decrypt_exports_inquiry.py +++ b/compliance/management/commands/decrypt_exports_inquiry.py @@ -3,8 +3,8 @@ """ import sys -from django.core.management import BaseCommand from django.contrib.auth import get_user_model +from django.core.management import BaseCommand from nacl.encoding import Base64Encoder from nacl.public import PrivateKey @@ -29,7 +29,7 @@ def add_arguments(self, parser): group.add_argument("--email", help="the email of the user") group.add_argument("--username", help="the username of the user") - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Run the command""" if options["user_id"]: diff --git a/compliance/migrations/0001_add_export_inquiry_log.py b/compliance/migrations/0001_add_export_inquiry_log.py index ff649de58..36eaaaba4 100644 --- a/compliance/migrations/0001_add_export_inquiry_log.py +++ b/compliance/migrations/0001_add_export_inquiry_log.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-10 13:10 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] diff --git a/compliance/models.py b/compliance/models.py index 3fda83447..78e126b0c 100644 --- a/compliance/models.py +++ b/compliance/models.py @@ -4,10 +4,10 @@ from compliance.constants import ( RESULT_CHOICES, - RESULT_SUCCESS, RESULT_DENIED, - RESULT_UNKNOWN, RESULT_MANUALLY_APPROVED, + RESULT_SUCCESS, + RESULT_UNKNOWN, ) from mitxpro.models import TimestampedModel @@ -28,7 +28,7 @@ class ExportsInquiryLog(TimestampedModel): ) reason_code = models.IntegerField() - info_code = models.CharField(max_length=255, null=True) + info_code = models.CharField(max_length=255, null=True) # noqa: DJ001 encrypted_request = models.TextField() encrypted_response = models.TextField() diff --git a/compliance/test_utils.py b/compliance/test_utils.py index dcb85fa91..e683ee61a 100644 --- a/compliance/test_utils.py +++ b/compliance/test_utils.py @@ -1,6 +1,6 @@ """Testing utils around CyberSource""" -from nacl.public import PrivateKey from nacl.encoding import Base64Encoder +from nacl.public import PrivateKey from rest_framework import status SERVICE_VERSION = "1.154" @@ -32,14 +32,14 @@ def mock_cybersource_wsdl(mocked_responses, settings, service_version=SERVICE_VE Mocks the responses to achieve a functional WSDL """ # in order for zeep to load the wsdl, it will load the wsdl and the accompanying xsd definitions - with open(f"{DATA_DIR}/CyberSourceTransaction_{service_version}.wsdl", "r") as wsdl: + with open(f"{DATA_DIR}/CyberSourceTransaction_{service_version}.wsdl", "r") as wsdl: # noqa: PTH123, UP015 mocked_responses.add( mocked_responses.GET, settings.CYBERSOURCE_WSDL_URL, body=wsdl.read(), status=status.HTTP_200_OK, ) - with open(f"{DATA_DIR}/CyberSourceTransaction_{SERVICE_VERSION}.xsd", "r") as xsd: + with open(f"{DATA_DIR}/CyberSourceTransaction_{SERVICE_VERSION}.xsd", "r") as xsd: # noqa: PTH123, UP015 mocked_responses.add( mocked_responses.GET, f"http://localhost/service/CyberSourceTransaction_{service_version}.xsd", @@ -52,7 +52,7 @@ def mock_cybersource_wsdl_operation(mocked_responses, response_name): """ Mock the response for an operation """ - with open(f"{DATA_DIR}/{response_name}.xml") as operation_response: + with open(f"{DATA_DIR}/{response_name}.xml") as operation_response: # noqa: PTH123 mocked_responses.add( mocked_responses.POST, "http://localhost/service", diff --git a/conftest.py b/conftest.py index 0ed1030dd..d14f5e9f0 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,4 @@ """Project conftest""" -# pylint: disable=wildcard-import,unused-wildcard-import import os import shutil @@ -23,10 +22,9 @@ SignatoryIndexPage, WebinarIndexPage, ) -from fixtures.autouse import * -from fixtures.common import * -from fixtures.cybersource import * - +from fixtures.autouse import * # noqa: F403 +from fixtures.common import * # noqa: F403 +from fixtures.cybersource import * # noqa: F403 TEST_MEDIA_SUBDIR = "test_media_root" @@ -42,7 +40,7 @@ def pytest_addoption(parser): def pytest_cmdline_main(config): """Pytest hook that runs after command line options are parsed""" - if getattr(config.option, "simple") is True: + if getattr(config.option, "simple") is True: # noqa: B009 config.option.pylint = False config.option.no_pylint = True @@ -50,9 +48,9 @@ def pytest_cmdline_main(config): def pytest_configure(config): """Pytest hook to perform some initial configuration""" if not settings.MEDIA_ROOT.endswith(TEST_MEDIA_SUBDIR): - settings.MEDIA_ROOT = os.path.join(settings.MEDIA_ROOT, TEST_MEDIA_SUBDIR) + settings.MEDIA_ROOT = os.path.join(settings.MEDIA_ROOT, TEST_MEDIA_SUBDIR) # noqa: PTH118 - if getattr(config.option, "simple") is True: + if getattr(config.option, "simple") is True: # noqa: B009 # NOTE: These plugins are already configured by the time the pytest_cmdline_main hook is run, so we can't # simply add/alter the command line options in that hook. This hook is being used to # reconfigure/unregister plugins that we can't change via the pytest_cmdline_main hook. @@ -66,18 +64,18 @@ def pytest_configure(config): @pytest.fixture(scope="session", autouse=True) -def clean_up_files(): +def clean_up_files(): # noqa: PT004 """ Fixture that removes the media root folder after the suite has finished running, effectively deleting any files that were created by factories over the course of the test suite. """ yield - if os.path.exists(settings.MEDIA_ROOT): + if os.path.exists(settings.MEDIA_ROOT): # noqa: PTH110 shutil.rmtree(settings.MEDIA_ROOT) @pytest.fixture(scope="session") -def django_db_setup(django_db_setup, django_db_blocker): +def django_db_setup(django_db_setup, django_db_blocker): # noqa: ARG001, PT004 """ Creates all the index pages during the tests setup as index pages are required by the factories. """ diff --git a/courses/__init__.py b/courses/__init__.py index d835a5f92..e69de29bb 100644 --- a/courses/__init__.py +++ b/courses/__init__.py @@ -1 +0,0 @@ -# pylint: disable=missing-docstring,invalid-name diff --git a/courses/admin.py b/courses/admin.py index 228501d20..4b0dc3548 100644 --- a/courses/admin.py +++ b/courses/admin.py @@ -91,7 +91,7 @@ class CourseRunAdmin(TimestampedModelAdmin): def get_changeform_initial_data(self, request): """ - Returns initial data for the change form. + Return initial data for the change form. Sets the initial values for start_date and end_date fields to the current date with a time of 23:59:00 for start_date, @@ -180,10 +180,10 @@ def get_user(self, obj): """Returns the related User's email""" return obj.enrollment.user.email - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @@ -257,10 +257,10 @@ def get_user(self, obj): """Returns the related User's email""" return obj.enrollment.user.email - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @@ -274,7 +274,7 @@ class CourseRunGradeAdmin(admin.ModelAdmin): raw_id_fields = ("user",) search_fields = ["user__email", "user__username"] - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002, D102 return self.model.objects.get_queryset().select_related("user", "course_run") @admin.display( @@ -324,10 +324,10 @@ def get_run_courseware_id(self, obj): """Returns the related CourseRun courseware_id""" return obj.course_run_grade.course_run.courseware_id - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @@ -357,10 +357,10 @@ class CourseRunCertificateAdmin(TimestampedModelAdmin): boolean=True, ) def get_revoked_state(self, obj): - """return the revoked state""" + """Return the revoked state""" return obj.is_revoked is not True - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002, D102 return self.model.all_objects.get_queryset().select_related( "user", "course_run" ) @@ -392,10 +392,10 @@ class ProgramCertificateAdmin(TimestampedModelAdmin): boolean=True, ) def get_revoked_state(self, obj): - """return the revoked state""" + """Return the revoked state""" return obj.is_revoked is not True - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002, D102 return self.model.all_objects.get_queryset().select_related("user", "program") diff --git a/courses/api.py b/courses/api.py index 05439280f..543dd312f 100644 --- a/courses/api.py +++ b/courses/api.py @@ -6,7 +6,8 @@ from traceback import format_exc from django.core.exceptions import ValidationError -from requests.exceptions import ConnectionError as RequestsConnectionError, HTTPError +from requests.exceptions import ConnectionError as RequestsConnectionError +from requests.exceptions import HTTPError from courses.constants import ENROLL_CHANGE_STATUS_DEFERRED from courses.models import CourseRun, CourseRunEnrollment, ProgramEnrollment @@ -21,9 +22,8 @@ from ecommerce import mail_api from mitxpro.utils import first_or_none, partition - log = logging.getLogger(__name__) -UserEnrollments = namedtuple( +UserEnrollments = namedtuple( # noqa: PYI024 "UserEnrollments", [ "programs", @@ -57,7 +57,7 @@ def get_user_enrollments(user): for program_enrollment in program_enrollments ) ) - program_course_ids = set(course.id for course in program_courses) + program_course_ids = set(course.id for course in program_courses) # noqa: C401 course_run_enrollments = ( CourseRunEnrollment.objects.select_related("run__course__coursepage", "company") .filter(user=user) @@ -90,10 +90,10 @@ def get_user_enrollments(user): def create_run_enrollments( user, runs, - keep_failed_enrollments=False, + keep_failed_enrollments=False, # noqa: FBT002 order=None, company=None, -): # pylint: disable=too-many-arguments +): """ Creates local records of a user's enrollment in course runs, and attempts to enroll them in edX via API @@ -133,7 +133,7 @@ def create_run_enrollments( edx_request_success = False if not keep_failed_enrollments: - raise EdxEnrollmentCreateError(str(error_message)) + raise EdxEnrollmentCreateError(str(error_message)) # noqa: B904, TRY200 log.exception(str(error_message)) else: edx_request_success = True @@ -144,12 +144,12 @@ def create_run_enrollments( user=user, run=run, order=order, - defaults=dict(company=company, edx_enrolled=edx_request_success), + defaults=dict(company=company, edx_enrolled=edx_request_success), # noqa: C408 ) if not created and not enrollment.active: enrollment.edx_enrolled = edx_request_success enrollment.reactivate_and_save() - except: # pylint: disable=bare-except + except: # noqa: E722, PERF203 mail_api.send_enrollment_failure_message(order, run, details=format_exc()) log.exception( "Failed to create/update enrollment record (user: %s, run: %s, order: %s)", @@ -182,11 +182,14 @@ def create_program_enrollments(user, programs, order=None, company=None): for program in programs: try: enrollment, created = ProgramEnrollment.all_objects.get_or_create( - user=user, program=program, order=order, defaults=dict(company=company) + user=user, + program=program, + order=order, + defaults=dict(company=company), # noqa: C408 ) if not created and not enrollment.active: enrollment.reactivate_and_save() - except: # pylint: disable=bare-except + except: # noqa: E722, PERF203 mail_api.send_enrollment_failure_message( order, program, details=format_exc() ) @@ -202,7 +205,9 @@ def create_program_enrollments(user, programs, order=None, company=None): def deactivate_run_enrollment( - run_enrollment, change_status, keep_failed_enrollments=False + run_enrollment, + change_status, + keep_failed_enrollments=False, # noqa: FBT002 ): """ Helper method to deactivate a CourseRunEnrollment @@ -218,7 +223,7 @@ def deactivate_run_enrollment( """ try: unenroll_edx_course_run(run_enrollment) - except Exception: # pylint: disable=broad-except + except Exception: log.exception( "Failed to unenroll course run '%s' for user '%s' in edX", run_enrollment.run.courseware_id, @@ -239,8 +244,8 @@ def deactivate_run_enrollment( def deactivate_program_enrollment( program_enrollment, change_status, - keep_failed_enrollments=False, - limit_to_order=True, + keep_failed_enrollments=False, # noqa: FBT002 + limit_to_order=True, # noqa: FBT002 ): """ Helper method to deactivate a ProgramEnrollment @@ -257,7 +262,7 @@ def deactivate_program_enrollment( tuple of ProgramEnrollment, list(CourseRunEnrollment): The deactivated enrollments """ run_enrollment_params = ( - dict(order_id=program_enrollment.order_id) + dict(order_id=program_enrollment.order_id) # noqa: C408 if limit_to_order and program_enrollment.order_id else {} ) @@ -272,7 +277,7 @@ def deactivate_program_enrollment( change_status=change_status, keep_failed_enrollments=keep_failed_enrollments, ): - deactivated_course_runs.append(run_enrollment) + deactivated_course_runs.append(run_enrollment) # noqa: PERF401 if deactivated_course_runs: program_enrollment.deactivate_and_save(change_status, no_user=True) @@ -286,8 +291,8 @@ def defer_enrollment( user, from_courseware_id, to_courseware_id, - keep_failed_enrollments=False, - force=False, + keep_failed_enrollments=False, # noqa: FBT002 + force=False, # noqa: FBT002 ): """ Deactivates a user's existing enrollment in one course run and enrolls the user in another. @@ -310,7 +315,7 @@ def defer_enrollment( ) if not force and not from_enrollment.active: raise ValidationError( - "Cannot defer from inactive enrollment (id: {}, run: {}, user: {}). " + "Cannot defer from inactive enrollment (id: {}, run: {}, user: {}). " # noqa: EM103 "Set force=True to defer anyway.".format( from_enrollment.id, from_enrollment.run.courseware_id, user.email ) @@ -318,17 +323,17 @@ def defer_enrollment( to_run = CourseRun.objects.get(courseware_id=to_courseware_id) if from_enrollment.run == to_run: raise ValidationError( - "Cannot defer to the same course run (run: {})".format(to_run.courseware_id) + "Cannot defer to the same course run (run: {})".format(to_run.courseware_id) # noqa: EM103, UP032 ) if not force and not to_run.is_not_beyond_enrollment: raise ValidationError( - "Cannot defer to a course run that is outside of its enrollment period (run: {}).".format( + "Cannot defer to a course run that is outside of its enrollment period (run: {}).".format( # noqa: EM103, UP032 to_run.courseware_id ) ) if not force and from_enrollment.run.course != to_run.course: raise ValidationError( - "Cannot defer to a course run of a different course ('{}' -> '{}'). " + "Cannot defer to a course run of a different course ('{}' -> '{}'). " # noqa: EM103 "Set force=True to defer anyway.".format( from_enrollment.run.course.title, to_run.course.title ) @@ -347,6 +352,6 @@ def defer_enrollment( ENROLL_CHANGE_STATUS_DEFERRED, keep_failed_enrollments=keep_failed_enrollments, ) - except EdxEnrollmentCreateError: # pylint: disable=try-except-raise + except EdxEnrollmentCreateError: # noqa: TRY302 raise return from_enrollment, first_or_none(to_enrollments) diff --git a/courses/api_test.py b/courses/api_test.py index 5ee886805..8e89d80bb 100644 --- a/courses/api_test.py +++ b/courses/api_test.py @@ -6,7 +6,8 @@ import factory import pytest from django.core.exceptions import ValidationError -from requests import ConnectionError as RequestsConnectionError, HTTPError +from requests import ConnectionError as RequestsConnectionError +from requests import HTTPError from courses.api import ( create_program_enrollments, @@ -27,8 +28,6 @@ ProgramEnrollmentFactory, ProgramFactory, ) - -# pylint: disable=redefined-outer-name from courses.models import CourseRunEnrollment, ProgramEnrollment from courseware.exceptions import ( EdxApiEnrollErrorException, @@ -149,7 +148,7 @@ def test_create_run_enrollments(mocker, user): assert edx_request_success is True assert len(successful_enrollments) == num_runs enrollments = CourseRunEnrollment.objects.order_by("run__id").all() - for (run, enrollment) in zip(runs, enrollments): + for run, enrollment in zip(runs, enrollments): assert enrollment.change_status is None assert enrollment.active is True assert enrollment.edx_enrolled is True @@ -192,10 +191,10 @@ def test_create_run_enrollments_api_fail(mocker, user, exception_cls): @pytest.mark.django_db @pytest.mark.parametrize("keep_failed_enrollments", [True, False]) @pytest.mark.parametrize( - "exception_cls,inner_exception", + "exception_cls,inner_exception", # noqa: PT006 [ - [EdxApiEnrollErrorException, MockHttpError()], - [UnknownEdxApiEnrollException, Exception()], + [EdxApiEnrollErrorException, MockHttpError()], # noqa: PT007 + [UnknownEdxApiEnrollException, Exception()], # noqa: PT007 ], ) def test_create_run_enrollments_enroll_api_fail( @@ -204,7 +203,7 @@ def test_create_run_enrollments_enroll_api_fail( keep_failed_enrollments, exception_cls, inner_exception, -): # pylint: disable=too-many-arguments +): """ create_run_enrollments should log a message and still create local enrollment records when an enrollment exception is raised if a flag is set to true @@ -224,7 +223,7 @@ def test_create_run_enrollments_enroll_api_fail( with pytest.raises( EdxEnrollmentCreateError - ) if not keep_failed_enrollments else contextlib.suppress(): + ) if not keep_failed_enrollments else contextlib.suppress(): # noqa: B022 successful_enrollments, edx_request_success = create_run_enrollments( user, runs, @@ -294,7 +293,7 @@ def test_create_program_enrollments(user): assert len(successful_enrollments) == num_programs enrollments = ProgramEnrollment.objects.order_by("program__id").all() assert len(enrollments) == len(programs) - for (program, enrollment) in zip(programs, enrollments): + for program, enrollment in zip(programs, enrollments): assert enrollment.change_status is None assert enrollment.active is True assert enrollment.program == program @@ -326,8 +325,8 @@ def test_create_program_enrollments_creation_fail(mocker, user): class TestDeactivateEnrollments: """Test cases for functions that deactivate enrollments""" - @pytest.fixture() - def patches(self, mocker): # pylint: disable=missing-docstring + @pytest.fixture + def patches(self, mocker): # noqa: D102 edx_unenroll = mocker.patch("courses.api.unenroll_edx_course_run") send_unenrollment_email = mocker.patch( "courses.api.mail_api.send_course_run_unenrollment_email" diff --git a/courses/apps.py b/courses/apps.py index 366372fc4..0d2b0040d 100644 --- a/courses/apps.py +++ b/courses/apps.py @@ -13,4 +13,4 @@ def ready(self): """ Ready handler. Import signals. """ - import courses.signals # pylint: disable=unused-import + import courses.signals # noqa: F401 diff --git a/courses/constants.py b/courses/constants.py index 2fcd194c3..6cb312627 100644 --- a/courses/constants.py +++ b/courses/constants.py @@ -8,11 +8,11 @@ PROGRAM_TEXT_ID_PREFIX = "program-" ENROLLABLE_ITEM_ID_SEPARATOR = "+" -TEXT_ID_RUN_TAG_PATTERN = r"\{separator}(?PR\d+)$".format( +TEXT_ID_RUN_TAG_PATTERN = r"\{separator}(?PR\d+)$".format( # noqa: UP032 separator=ENROLLABLE_ITEM_ID_SEPARATOR ) PROGRAM_RUN_ID_PATTERN = ( - r"^(?P{program_prefix}.*){run_tag_pattern}".format( + r"^(?P{program_prefix}.*){run_tag_pattern}".format( # noqa: UP032 program_prefix=PROGRAM_TEXT_ID_PREFIX, run_tag_pattern=TEXT_ID_RUN_TAG_PATTERN ) ) diff --git a/courses/constants_test.py b/courses/constants_test.py index c693636e0..4f0f6bb58 100644 --- a/courses/constants_test.py +++ b/courses/constants_test.py @@ -1,24 +1,23 @@ """Tests for courses constants""" import pytest - from django.contrib.contenttypes.models import ContentType -from courses.models import Program, Course, CourseRun from courses.constants import ( - CONTENT_TYPE_MODEL_PROGRAM, CONTENT_TYPE_MODEL_COURSE, CONTENT_TYPE_MODEL_COURSERUN, + CONTENT_TYPE_MODEL_PROGRAM, ) +from courses.models import Course, CourseRun, Program @pytest.mark.django_db def test_content_type_names(): """Ensure that content type constants have the correct values relative to the actual ContentTypes""" assert ( - CONTENT_TYPE_MODEL_PROGRAM == ContentType.objects.get_for_model(Program).model + CONTENT_TYPE_MODEL_PROGRAM == ContentType.objects.get_for_model(Program).model # noqa: SIM300 ) - assert CONTENT_TYPE_MODEL_COURSE == ContentType.objects.get_for_model(Course).model + assert CONTENT_TYPE_MODEL_COURSE == ContentType.objects.get_for_model(Course).model # noqa: SIM300 assert ( - CONTENT_TYPE_MODEL_COURSERUN + CONTENT_TYPE_MODEL_COURSERUN # noqa: SIM300 == ContentType.objects.get_for_model(CourseRun).model ) diff --git a/courses/credentials.py b/courses/credentials.py index 6e51fcd3a..fbe950113 100644 --- a/courses/credentials.py +++ b/courses/credentials.py @@ -1,6 +1,6 @@ """Digital courseware credentials""" import logging -from typing import Dict, Union +from typing import Union from urllib.parse import urljoin from django.conf import settings @@ -9,25 +9,24 @@ from courses.models import CourseRunCertificate, ProgramCertificate - log = logging.getLogger(__name__) -def build_program_credential(certificate: ProgramCertificate) -> Dict: +def build_program_credential(certificate: ProgramCertificate) -> dict: """Build a credential object for a ProgramCertificate""" start_date, end_date = certificate.start_end_dates if not start_date or not end_date: - raise Exception("Program has no start or end date") + raise Exception("Program has no start or end date") # noqa: EM101, TRY002 if not certificate.program.page: - raise Exception("Program has no CMS program page") + raise Exception("Program has no CMS program page") # noqa: EM101, TRY002 if not certificate.program.page.certificate_page: - raise Exception("Program has no CMS program certificate page") + raise Exception("Program has no CMS program certificate page") # noqa: EM101, TRY002 if not certificate.program.page.certificate_page.CEUs: - raise Exception("Program has no CEUs defined") + raise Exception("Program has no CEUs defined") # noqa: EM101, TRY002 return { "type": ["EducationalOccupationalCredential", "ProgramCompletionCredential"], @@ -47,22 +46,22 @@ def build_program_credential(certificate: ProgramCertificate) -> Dict: } -def build_course_run_credential(certificate: CourseRunCertificate) -> Dict: +def build_course_run_credential(certificate: CourseRunCertificate) -> dict: """Build a credential object for a CourseRunCertificate""" course = certificate.course_run.course start_date, end_date = certificate.start_end_dates if not start_date or not end_date: - raise Exception("CourseRun has no start or end date") + raise Exception("CourseRun has no start or end date") # noqa: EM101, TRY002 if not course.page: - raise Exception("Course has no CMS course page") + raise Exception("Course has no CMS course page") # noqa: EM101, TRY002 if not course.page.certificate_page: - raise Exception("Course has no CMS course certificate page") + raise Exception("Course has no CMS course certificate page") # noqa: EM101, TRY002 if not course.page.certificate_page.CEUs: - raise Exception("Course has no CEUs defined") + raise Exception("Course has no CEUs defined") # noqa: EM101, TRY002 return { "type": ["EducationalOccupationalCredential", "CourseCompletionCredential"], @@ -81,17 +80,17 @@ def build_course_run_credential(certificate: CourseRunCertificate) -> Dict: def build_digital_credential( - certificate: Union[ProgramCertificate, CourseRunCertificate], + certificate: Union[ProgramCertificate, CourseRunCertificate], # noqa: FA100 learner_did: LearnerDID, -) -> Dict: +) -> dict: """Function for building certificate digital credentials""" if isinstance(certificate, ProgramCertificate): has_credential = build_program_credential(certificate) elif isinstance(certificate, CourseRunCertificate): has_credential = build_course_run_credential(certificate) else: - raise Exception( - f"Unexpected courseware object type for digital credentials: {type(certificate)}" + raise Exception( # noqa: TRY002, TRY004 + f"Unexpected courseware object type for digital credentials: {type(certificate)}" # noqa: EM102 ) return { diff --git a/courses/credentials_test.py b/courses/credentials_test.py index 3789bc456..93a05af02 100644 --- a/courses/credentials_test.py +++ b/courses/credentials_test.py @@ -18,7 +18,6 @@ ProgramFactory, ) - pytestmark = pytest.mark.django_db @@ -47,7 +46,7 @@ def test_build_program_credential(user): @pytest.mark.parametrize( - "kwargs, error_message", + "kwargs, error_message", # noqa: PT006 [ ({"page": None}, "Program has no CMS program page"), ( @@ -62,7 +61,7 @@ def test_build_program_credential_error(user, kwargs, error_message): program = ProgramFactory.create(**kwargs) certificate = ProgramCertificateFactory.create(user=user, program=program) CourseRunCertificateFactory.create(user=user, course_run__course__program=program) - with pytest.raises(Exception) as exc_info: + with pytest.raises(Exception) as exc_info: # noqa: PT011 build_program_credential(certificate) assert exc_info.value.args[0] == error_message @@ -71,7 +70,7 @@ def test_build_program_credential_error(user, kwargs, error_message): def test_build_program_credential_no_start_end_dates_error(): """Verify build_program_credential errors with no start or end dates""" certificate = ProgramCertificateFactory.create() - with pytest.raises(Exception) as exc_info: + with pytest.raises(Exception) as exc_info: # noqa: PT011 build_program_credential(certificate) assert exc_info.value.args[0] == "Program has no start or end date" @@ -100,7 +99,7 @@ def test_build_course_run_credential(): @pytest.mark.parametrize( - "kwargs, error_message", + "kwargs, error_message", # noqa: PT006 [ ({"course__page": None}, "Course has no CMS course page"), ( @@ -117,14 +116,14 @@ def test_build_course_run_credential_error(kwargs, error_message): course_run = CourseRunFactory.create(**kwargs) certificate = CourseRunCertificateFactory.create(course_run=course_run) - with pytest.raises(Exception) as exc_info: + with pytest.raises(Exception) as exc_info: # noqa: PT011 build_course_run_credential(certificate) assert exc_info.value.args[0] == error_message def test_build_digital_credential_course_run(settings, mocker): - "Verify build_digital_credential works correctly for a course run" + """Verify build_digital_credential works correctly for a course run""" mock_build_course_run_credential = mocker.patch( "courses.credentials.build_course_run_credential", autospec=True ) @@ -166,7 +165,7 @@ def test_build_digital_credential_course_run(settings, mocker): def test_build_digital_credential_program_run(settings, mocker): - "Verify build_digital_credential works correctly for a program run" + """Verify build_digital_credential works correctly for a program run""" mock_build_program_credential = mocker.patch( "courses.credentials.build_program_credential", autospec=True ) @@ -209,5 +208,5 @@ def test_build_digital_credential_program_run(settings, mocker): def test_test_build_digital_credential_invalid_certified_object(mocker): """Verify an exception is raised for an invalid courseware object""" invalid_courseware = CourseFactory.create() - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017, PT011 build_digital_credential(invalid_courseware, mocker.Mock()) diff --git a/courses/factories.py b/courses/factories.py index 586f48706..4d0875e63 100644 --- a/courses/factories.py +++ b/courses/factories.py @@ -1,5 +1,6 @@ """Factories for creating course data in tests""" from datetime import timezone + import factory import faker from factory import SubFactory, Trait, fuzzy @@ -24,7 +25,6 @@ ProgramRun, ) - FAKE = faker.Factory.create() @@ -50,9 +50,7 @@ class ProgramFactory(DjangoModelFactory): """Factory for Programs""" title = fuzzy.FuzzyText(prefix="Program ") - readable_id = factory.Sequence( - lambda number: "{}{}".format(PROGRAM_TEXT_ID_PREFIX, number) - ) + readable_id = factory.Sequence(lambda number: f"{PROGRAM_TEXT_ID_PREFIX}{number}") platform = factory.SubFactory(PlatformFactory) live = True @@ -95,10 +93,8 @@ class CourseRunFactory(DjangoModelFactory): """Factory for CourseRuns""" course = factory.SubFactory(CourseFactory) - title = factory.LazyAttribute(lambda x: "CourseRun " + FAKE.sentence()) - courseware_id = factory.Sequence( - lambda number: "course:/v{}/{}".format(number, FAKE.slug()) - ) + title = factory.LazyAttribute(lambda x: "CourseRun " + FAKE.sentence()) # noqa: ARG005 + courseware_id = factory.Sequence(lambda number: f"course:/v{number}/{FAKE.slug()}") run_tag = factory.Sequence("R{0}".format) courseware_url_path = factory.Faker("uri") start_date = factory.Faker( diff --git a/courses/management/commands/backfill_program_runs.py b/courses/management/commands/backfill_program_runs.py index 6346e8179..a79918104 100644 --- a/courses/management/commands/backfill_program_runs.py +++ b/courses/management/commands/backfill_program_runs.py @@ -6,7 +6,7 @@ from django.core.management import BaseCommand from courses.constants import TEXT_ID_RUN_TAG_PATTERN -from courses.models import Program, ProgramRun, CourseRun +from courses.models import CourseRun, Program, ProgramRun User = get_user_model() @@ -20,7 +20,7 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-p", "--program", @@ -119,7 +119,7 @@ def backfill_runs_for_program(self, program): for run_tag in run_tags_to_create ] - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" programs = Program.objects.prefetch_related("programruns", "courses") if options["program"]: diff --git a/courses/management/commands/create_enrollment.py b/courses/management/commands/create_enrollment.py index 36aef2bd6..b38aafb42 100644 --- a/courses/management/commands/create_enrollment.py +++ b/courses/management/commands/create_enrollment.py @@ -1,6 +1,6 @@ """Management command to change enrollment status""" -from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError from django.db import transaction from courses.api import create_run_enrollments @@ -12,7 +12,7 @@ latest_product_version, redeem_coupon, ) -from ecommerce.models import Coupon, Product, ProductCouponAssignment, Order, Line +from ecommerce.models import Coupon, Line, Order, Product, ProductCouponAssignment from users.api import fetch_user User = get_user_model() @@ -23,7 +23,7 @@ class Command(BaseCommand): help = "Creates an enrollment for a course run" - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--user", type=str, @@ -47,9 +47,8 @@ def add_arguments(self, parser): help="If provided, enrollment records will be kept even if edX enrollment fails", ) super().add_arguments(parser) - # pylint: disable=too-many-locals - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" user = fetch_user(options["user"]) @@ -57,7 +56,7 @@ def handle(self, *args, **options): run = CourseRun.objects.filter(courseware_id=options["run"]).first() if run is None: raise CommandError( - "Could not find course run with courseware_id={}".format(options["run"]) + "Could not find course run with courseware_id={}".format(options["run"]) # noqa: EM103 ) product = Product.objects.filter( @@ -65,7 +64,7 @@ def handle(self, *args, **options): ).first() if product is None: raise CommandError( - "No product found for that course with courseware_id={}".format( + "No product found for that course with courseware_id={}".format( # noqa: EM103 options["run"] ) ) @@ -73,7 +72,7 @@ def handle(self, *args, **options): coupon = Coupon.objects.filter(coupon_code=options["code"]).first() if not coupon: raise CommandError( - "That enrollment code {} does not exist".format(options["code"]) + "That enrollment code {} does not exist".format(options["code"]) # noqa: EM103 ) # Check if the coupon is valid for the product @@ -110,9 +109,9 @@ def handle(self, *args, **options): order=order, ) if not successful_enrollments: - raise EdxEnrollmentCreateError + raise EdxEnrollmentCreateError # noqa: TRY301 except EdxEnrollmentCreateError: - raise CommandError("Failed to create the enrollment record") + raise CommandError("Failed to create the enrollment record") # noqa: B904, EM101, TRY200 ProductCouponAssignment.objects.filter( email__iexact=user.email, redeemed=False, product_coupon__coupon=coupon @@ -134,7 +133,7 @@ def handle(self, *args, **options): self.stdout.write( self.style.SUCCESS( - "Order {} with line {} is created for user {} ".format( + "Order {} with line {} is created for user {} ".format( # noqa: UP032 order, line, user ) ) diff --git a/courses/management/commands/defer_enrollment.py b/courses/management/commands/defer_enrollment.py index f74a66298..4b49e0687 100644 --- a/courses/management/commands/defer_enrollment.py +++ b/courses/management/commands/defer_enrollment.py @@ -1,7 +1,7 @@ """Management command to change enrollment status""" +from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.management.base import CommandError -from django.contrib.auth import get_user_model from courses.api import defer_enrollment from courses.management.utils import EnrollmentChangeCommand, enrollment_summary @@ -16,7 +16,7 @@ class Command(EnrollmentChangeCommand): help = "Sets a user's enrollment to 'deferred' and creates an enrollment for a different course run" - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--user", type=str, @@ -44,7 +44,7 @@ def add_arguments(self, parser): ) super().add_arguments(parser) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" user = fetch_user(options["user"]) from_courseware_id = options["from_run"] @@ -64,25 +64,25 @@ def handle(self, *args, **options): from_courseware_id ) elif isinstance(exc, CourseRun.DoesNotExist): - message = "'to' course does not exist ({})".format(to_courseware_id) + message = "'to' course does not exist ({})".format(to_courseware_id) # noqa: UP032 else: message = str(exc) - raise CommandError(message) + raise CommandError(message) # noqa: B904, TRY200 except ValidationError as exc: - raise CommandError("Invalid enrollment deferral - {}".format(exc)) + raise CommandError("Invalid enrollment deferral - {}".format(exc)) # noqa: B904, EM103, TRY200, UP032 else: if not to_enrollment: raise CommandError( - "Failed to create/update the target enrollment ({})".format( + "Failed to create/update the target enrollment ({})".format( # noqa: EM103, UP032 to_courseware_id ) ) self.stdout.write( self.style.SUCCESS( - "Deferred enrollment for user: {}\n" - "Enrollment deactivated: {}\n" - "Enrollment created/updated: {}".format( + "Deferred enrollment for user: {}\n" # noqa: UP032, RUF100 + "Enrollment deactivated: {}\n" # noqa: UP032, RUF100 + "Enrollment created/updated: {}".format( # noqa: UP032, RUF100 user, enrollment_summary(from_enrollment), enrollment_summary(to_enrollment), diff --git a/courses/management/commands/manage_program_certificates.py b/courses/management/commands/manage_program_certificates.py index d139185b4..bdf671112 100644 --- a/courses/management/commands/manage_program_certificates.py +++ b/courses/management/commands/manage_program_certificates.py @@ -22,7 +22,7 @@ class Command(BaseCommand): help = "Create program certificate, for a single user or all users in the program." - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--user", type=str, @@ -38,18 +38,20 @@ def add_arguments(self, parser): super().add_arguments(parser) def handle( - self, *args, **options - ): # pylint: disable=too-many-locals,too-many-branches + self, + *args, # noqa: ARG002 + **options, + ): """Handle command execution""" program = options.get("program") if not program: - raise CommandError("Please provide a valid program readable_id.") + raise CommandError("Please provide a valid program readable_id.") # noqa: EM101 try: program = Program.objects.get(readable_id=program) except Program.DoesNotExist: - raise CommandError( - f"Could not find any program with provided readable_id={program}" + raise CommandError( # noqa: B904, TRY200 + f"Could not find any program with provided readable_id={program}" # noqa: EM102 ) user = options.get("user") and fetch_user(options["user"]) @@ -64,7 +66,7 @@ def handle( ) if not enrollments: raise CommandError( - f"Could not find course enrollment(s) with provided program readable_id={program.readable_id}" + f"Could not find course enrollment(s) with provided program readable_id={program.readable_id}" # noqa: EM102 ) results = [] diff --git a/courses/management/commands/refund_enrollment.py b/courses/management/commands/refund_enrollment.py index 219e9622a..f0e32fb12 100644 --- a/courses/management/commands/refund_enrollment.py +++ b/courses/management/commands/refund_enrollment.py @@ -2,8 +2,8 @@ from django.contrib.auth import get_user_model from courses.api import deactivate_program_enrollment, deactivate_run_enrollment -from courses.management.utils import EnrollmentChangeCommand, enrollment_summaries from courses.constants import ENROLL_CHANGE_STATUS_REFUNDED +from courses.management.utils import EnrollmentChangeCommand, enrollment_summaries from ecommerce.models import Order from users.api import fetch_user @@ -15,7 +15,7 @@ class Command(EnrollmentChangeCommand): help = "Sets a user's enrollment to 'refunded' and deactivates it" - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--user", type=str, @@ -46,7 +46,7 @@ def add_arguments(self, parser): super().add_arguments(parser) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" user = fetch_user(options["user"]) keep_failed_enrollments = options["keep_failed_enrollments"] @@ -74,7 +74,7 @@ def handle(self, *args, **options): enrollment.user.username, enrollment.user.email, enrollment_summaries( - filter(bool, [program_enrollment] + run_enrollments) + filter(bool, [program_enrollment] + run_enrollments) # noqa: RUF005 ), ) @@ -95,7 +95,7 @@ def handle(self, *args, **options): else: self.stdout.write( self.style.ERROR( - "Failed to refund the enrollment – 'for' user: {} ({}) from course / program ({})\n".format( + "Failed to refund the enrollment – 'for' user: {} ({}) from course / program ({})\n".format( # noqa: RUF001 user.username, user.email, options["run"] or options["program"] ) ) diff --git a/courses/management/commands/revoke_certificate.py b/courses/management/commands/revoke_certificate.py index 51c3d488c..d9128cafa 100644 --- a/courses/management/commands/revoke_certificate.py +++ b/courses/management/commands/revoke_certificate.py @@ -2,7 +2,8 @@ Management command to revoke and un revoke a certificate for a course run or program for the given user. """ from django.core.management.base import BaseCommand, CommandError -from courses.utils import revoke_program_certificate, revoke_course_run_certificate + +from courses.utils import revoke_course_run_certificate, revoke_program_certificate from users.api import fetch_user @@ -13,7 +14,7 @@ class Command(BaseCommand): help = "Revoke and un revoke a certificate for a specified user against a program or course run." - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--user", type=str, @@ -45,7 +46,7 @@ def add_arguments(self, parser): super().add_arguments(parser) - def handle(self, *args, **options): # pylint: disable=too-many-locals + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" user = fetch_user(options["user"]) if options["user"] else None @@ -56,13 +57,13 @@ def handle(self, *args, **options): # pylint: disable=too-many-locals if program and run: raise CommandError( - "Either 'program' or 'run' should be provided, not both." + "Either 'program' or 'run' should be provided, not both." # noqa: EM101 ) if not program and not run: - raise CommandError("Either 'program' or 'run' must be provided.") + raise CommandError("Either 'program' or 'run' must be provided.") # noqa: EM101 if (program or run) and not user: - raise CommandError("A valid user must be provided.") + raise CommandError("A valid user must be provided.") # noqa: EM101 updated = False if program: @@ -79,7 +80,7 @@ def handle(self, *args, **options): # pylint: disable=too-many-locals if updated: msg = "Certificate for {} has been {}".format( - "run: {}".format(run) if run else "program: {}".format(program), + "run: {}".format(run) if run else "program: {}".format(program), # noqa: UP032 "revoked" if revoke else "un-revoked", ) self.stdout.write(self.style.SUCCESS(msg)) diff --git a/courses/management/commands/sync_courseruns.py b/courses/management/commands/sync_courseruns.py index 3bcc6a5b1..03ecc4d0e 100644 --- a/courses/management/commands/sync_courseruns.py +++ b/courses/management/commands/sync_courseruns.py @@ -16,7 +16,7 @@ class Command(BaseCommand): help = "Sync dates and title for all or a specific course run from edX." - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--run", type=str, @@ -25,15 +25,15 @@ def add_arguments(self, parser): ) super().add_arguments(parser) - def handle(self, *args, **options): # pylint: disable=too-many-locals + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" runs = [] if options["run"]: try: runs = [CourseRun.objects.get(courseware_id=options["run"])] except CourseRun.DoesNotExist: - raise CommandError( - "Could not find run with courseware_id={}".format(options["run"]) + raise CommandError( # noqa: B904, TRY200 + "Could not find run with courseware_id={}".format(options["run"]) # noqa: EM103 ) else: # We pick up all the course runs that do not have an expiration date (implies not having diff --git a/courses/management/commands/sync_grades_and_certificates.py b/courses/management/commands/sync_grades_and_certificates.py index 7dbf06fab..823ceeba2 100644 --- a/courses/management/commands/sync_grades_and_certificates.py +++ b/courses/management/commands/sync_grades_and_certificates.py @@ -6,8 +6,8 @@ from courses.models import CourseRun from courses.utils import ensure_course_run_grade, process_course_run_grade_certificate from courseware.api import get_edx_grades_with_users -from users.api import fetch_user from mitxpro.utils import now_in_utc +from users.api import fetch_user class Command(BaseCommand): @@ -17,7 +17,7 @@ class Command(BaseCommand): help = "Sync grades and certificates for a course run for all enrolled users or a specified user." - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--user", type=str, @@ -51,25 +51,27 @@ def add_arguments(self, parser): ) super().add_arguments(parser) - def handle( - self, *args, **options - ): # pylint: disable=too-many-locals,too-many-branches + def handle( # noqa: C901, PLR0915 + self, + *args, # noqa: ARG002 + **options, + ): """Handle command execution""" # Grade override for all users for the course run. Disallowed. if options["grade"] is not None and not options["user"]: raise CommandError( - "No user supplied with override grade. Overwrite of grade is not supported for all users. Grade should only be supplied when a specific user is targeted." + "No user supplied with override grade. Overwrite of grade is not supported for all users. Grade should only be supplied when a specific user is targeted." # noqa: EM101 ) try: run = CourseRun.objects.get(courseware_id=options["run"]) except CourseRun.DoesNotExist: - raise CommandError( - "Could not find run with courseware_id={}".format(options["run"]) + raise CommandError( # noqa: B904, TRY200 + "Could not find run with courseware_id={}".format(options["run"]) # noqa: EM103 ) now = now_in_utc() if not options.get("force") and (run.end_date is None or run.end_date > now): raise CommandError( - "The given course run has not yet finished, so the course grades should not be " + "The given course run has not yet finished, so the course grades should not be " # noqa: EM103 "considered final (courseware_id={}, end_date={}).\n" "Add the -f/--force flag if grades/certificates should be synced anyway.".format( options["run"], @@ -83,8 +85,8 @@ def handle( if options["grade"] is not None: override_grade = float(options["grade"]) - if override_grade and (override_grade < 0.0 or override_grade > 1.0): - raise CommandError("Invalid value for grade. Allowed range: 0.0 - 1.0") + if override_grade and (override_grade < 0.0 or override_grade > 1.0): # noqa: PLR2004 + raise CommandError("Invalid value for grade. Allowed range: 0.0 - 1.0") # noqa: EM101 edx_grade_user_iter = get_edx_grades_with_users(run, user=user) @@ -112,7 +114,7 @@ def handle( _, created_cert, deleted_cert = process_course_run_grade_certificate( course_run_grade=course_run_grade ) - except Exception as e: + except Exception as e: # noqa: BLE001 self.stdout.write( self.style.ERROR( f"Course certificate creation failed for {user} due to following reason(s),\n{e}" @@ -127,10 +129,10 @@ def handle( else: grade_status = "already exists" - grade_summary = ["passed: {}".format(course_run_grade.passed)] + grade_summary = ["passed: {}".format(course_run_grade.passed)] # noqa: UP032 if override_grade is not None: grade_summary.append( - "value override: {}".format(course_run_grade.grade) + "value override: {}".format(course_run_grade.grade) # noqa: UP032 ) if created_cert: diff --git a/courses/management/commands/test_manage_program_certificates.py b/courses/management/commands/test_manage_program_certificates.py index 98007a918..2a265361e 100644 --- a/courses/management/commands/test_manage_program_certificates.py +++ b/courses/management/commands/test_manage_program_certificates.py @@ -1,18 +1,20 @@ """Tests for Program Certificates management command""" -import pytest from itertools import product -from courses.management.commands import manage_program_certificates -from courses.models import ProgramCertificate + +import pytest from django.core.management.base import CommandError + from courses.factories import ( CourseFactory, - CourseRunFactory, - CourseRunGradeFactory, CourseRunCertificateFactory, CourseRunEnrollmentFactory, + CourseRunFactory, + CourseRunGradeFactory, ProgramFactory, ) +from courses.management.commands import manage_program_certificates +from courses.models import ProgramCertificate from users.factories import UserFactory pytestmark = [pytest.mark.django_db] @@ -40,7 +42,7 @@ def test_program_certificate_management_no_program_argument(user): """Test that command generates error when the program is not passed""" with pytest.raises(CommandError) as command_error: manage_program_certificates.Command().handle(user=user) - assert str(command_error.value) == f"Please provide a valid program readable_id." + assert str(command_error.value) == "Please provide a valid program readable_id." def test_program_certificate_creation_multiple_users(): @@ -51,9 +53,9 @@ def test_program_certificate_creation_multiple_users(): def create_course_certificates(args): run, user = args - CourseRunEnrollmentFactory.create(user=user, run=run), - CourseRunGradeFactory.create(course_run=run, user=user, passed=True, grade=1), - CourseRunCertificateFactory.create(course_run=run, user=user), + (CourseRunEnrollmentFactory.create(user=user, run=run),) + (CourseRunGradeFactory.create(course_run=run, user=user, passed=True, grade=1),) + (CourseRunCertificateFactory.create(course_run=run, user=user),) program = ProgramFactory.create(readable_id="test") users = UserFactory.create_batch(size=3) diff --git a/courses/management/commands/transfer_enrollment.py b/courses/management/commands/transfer_enrollment.py index b17f0939d..745f76790 100644 --- a/courses/management/commands/transfer_enrollment.py +++ b/courses/management/commands/transfer_enrollment.py @@ -1,10 +1,10 @@ """Management command to change enrollment status""" -from django.core.management.base import CommandError from django.contrib.auth import get_user_model +from django.core.management.base import CommandError from courses.api import deactivate_program_enrollment, deactivate_run_enrollment -from courses.management.utils import EnrollmentChangeCommand, enrollment_summaries from courses.constants import ENROLL_CHANGE_STATUS_TRANSFERRED +from courses.management.utils import EnrollmentChangeCommand, enrollment_summaries from courses.models import CourseRunEnrollment from users.api import fetch_user @@ -16,7 +16,7 @@ class Command(EnrollmentChangeCommand): help = "Sets a user's enrollment to 'transferred' and creates an enrollment for a different user" - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--from-user", type=str, @@ -53,7 +53,7 @@ def add_arguments(self, parser): super().add_arguments(parser) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002, D102 from_user = fetch_user(options["from_user"]) to_user = fetch_user(options["to_user"]) keep_failed_enrollments = options["keep_failed_enrollments"] @@ -67,7 +67,7 @@ def handle(self, *args, **options): ) if len(to_user_existing_enrolled_run_ids) > 0: raise CommandError( - "'to' user is already enrolled in program runs ({})".format( + "'to' user is already enrolled in program runs ({})".format( # noqa: EM103 list(to_user_existing_enrolled_run_ids) ) ) @@ -105,14 +105,14 @@ def handle(self, *args, **options): if new_program_enrollment or new_run_enrollments: self.stdout.write( self.style.SUCCESS( - "Transferred enrollment – 'from' user: {} ({}), 'to' user: {} ({})\n" + "Transferred enrollment – 'from' user: {} ({}), 'to' user: {} ({})\n" # noqa: RUF001 "Enrollments created/updated: {}".format( from_user.username, from_user.email, to_user.username, to_user.email, enrollment_summaries( - filter(bool, [new_program_enrollment] + new_run_enrollments) + filter(bool, [new_program_enrollment] + new_run_enrollments) # noqa: RUF005 ), ) ) @@ -120,7 +120,7 @@ def handle(self, *args, **options): else: self.stdout.write( self.style.ERROR( - "Failed to transfer enrollment – 'from' user: {} ({}), 'to' user: {} ({})\n".format( + "Failed to transfer enrollment – 'from' user: {} ({}), 'to' user: {} ({})\n".format( # noqa: RUF001 from_user.username, from_user.email, to_user.username, diff --git a/courses/management/utils.py b/courses/management/utils.py index e37c63c02..58f43212a 100644 --- a/courses/management/utils.py +++ b/courses/management/utils.py @@ -2,12 +2,12 @@ from django.core.management.base import BaseCommand, CommandError from courses.models import CourseRun, CourseRunEnrollment, Program, ProgramEnrollment +from courseware.api import enroll_in_edx_course_runs from courseware.exceptions import ( EdxApiEnrollErrorException, - UnknownEdxApiEnrollException, NoEdxApiAuthError, + UnknownEdxApiEnrollException, ) -from courseware.api import enroll_in_edx_course_runs from ecommerce import mail_api from mitxpro.utils import has_equal_properties @@ -61,7 +61,7 @@ def create_or_update_enrollment(model_cls, defaults=None, **kwargs): class EnrollmentChangeCommand(BaseCommand): """Base class for management commands that change enrollment status""" - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-f", "--force", @@ -70,7 +70,7 @@ def add_arguments(self, parser): help="Ignores validation when performing the desired status change", ) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: D102 pass @staticmethod @@ -92,10 +92,10 @@ def fetch_enrollment(user, command_options): if program_property and run_property: raise CommandError( - "Either 'program' or 'run' should be provided, not both." + "Either 'program' or 'run' should be provided, not both." # noqa: EM101 ) if not program_property and not run_property: - raise CommandError("Either 'program' or 'run' must be provided.") + raise CommandError("Either 'program' or 'run' must be provided.") # noqa: EM101 query_params = {"user": user} if order_property: @@ -113,10 +113,10 @@ def fetch_enrollment(user, command_options): enrollment = CourseRunEnrollment.all_objects.filter(**query_params).first() if not enrollment: - raise CommandError("Enrollment not found for: {}".format(enrolled_obj)) + raise CommandError("Enrollment not found for: {}".format(enrolled_obj)) # noqa: EM103, UP032 if not enrollment.active and not force: raise CommandError( - "The given enrollment is not active ({}).\n" + "The given enrollment is not active ({}).\n" # noqa: EM103, UP032, RUF100 "Add the -f/--force flag if you want to change the status anyway.".format( enrollment.id ) @@ -129,7 +129,7 @@ def create_program_enrollment( existing_enrollment, to_program=None, to_user=None, - keep_failed_enrollments=False, + keep_failed_enrollments=False, # noqa: FBT002 ): """ Helper method to create a new ProgramEnrollment based on an existing enrollment @@ -147,8 +147,8 @@ def create_program_enrollment( """ to_user = to_user or existing_enrollment.user to_program = to_program or existing_enrollment.program - enrollment_params = dict(user=to_user, program=to_program) - enrollment_defaults = dict( + enrollment_params = dict(user=to_user, program=to_program) # noqa: C408 + enrollment_defaults = dict( # noqa: C408 company=existing_enrollment.company, order=existing_enrollment.order ) existing_run_enrollments = existing_enrollment.get_run_enrollments() @@ -178,7 +178,7 @@ def create_run_enrollment( existing_enrollment, to_run=None, to_user=None, - keep_failed_enrollments=False, + keep_failed_enrollments=False, # noqa: FBT002 ): """ Helper method to create a CourseRunEnrollment based on an existing enrollment @@ -196,8 +196,8 @@ def create_run_enrollment( """ to_user = to_user or existing_enrollment.user to_run = to_run or existing_enrollment.run - enrollment_params = dict(user=to_user, run=to_run) - enrollment_defaults = dict( + enrollment_params = dict(user=to_user, run=to_run) # noqa: C408 + enrollment_defaults = dict( # noqa: C408 company=existing_enrollment.company, order=existing_enrollment.order ) run_enrollment, created = create_or_update_enrollment( @@ -236,7 +236,7 @@ def enroll_in_edx(self, user, course_runs): """ try: enroll_in_edx_course_runs(user, course_runs) - return True + return True # noqa: TRY300 except ( EdxApiEnrollErrorException, UnknownEdxApiEnrollException, diff --git a/courses/management/utils_test.py b/courses/management/utils_test.py index 811860d16..0c4cd1c81 100644 --- a/courses/management/utils_test.py +++ b/courses/management/utils_test.py @@ -1,18 +1,22 @@ """Tests for command utils""" from datetime import timedelta + import pytest from django.contrib.auth import get_user_model -from courses.factories import CourseRunEnrollmentFactory, ProgramEnrollmentFactory +from courses.factories import ( + CourseRunEnrollmentFactory, + CourseRunFactory, + ProgramEnrollmentFactory, +) from courses.management.utils import EnrollmentChangeCommand -from courses.factories import CourseRunFactory from courseware.exceptions import ( - UnknownEdxApiEnrollException, EdxApiEnrollErrorException, + UnknownEdxApiEnrollException, ) -from users.factories import UserFactory from mitxpro.test_utils import MockHttpError from mitxpro.utils import now_in_utc +from users.factories import UserFactory User = get_user_model() @@ -49,10 +53,10 @@ def test_fetch_enrollment(order): @pytest.mark.django_db @pytest.mark.parametrize("keep_failed_enrollments", [True, False]) @pytest.mark.parametrize( - "exception_cls,inner_exception", + "exception_cls,inner_exception", # noqa: PT006 [ - [EdxApiEnrollErrorException, MockHttpError()], - [UnknownEdxApiEnrollException, Exception()], + [EdxApiEnrollErrorException, MockHttpError()], # noqa: PT007 + [UnknownEdxApiEnrollException, Exception()], # noqa: PT007 ], ) def test_create_run_enrollment_edx_failure( diff --git a/courses/migrations/0001_create_course_models.py b/courses/migrations/0001_create_course_models.py index b51a7e651..e2afe9041 100644 --- a/courses/migrations/0001_create_course_models.py +++ b/courses/migrations/0001_create_course_models.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-03-05 22:15 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/courses/migrations/0002_course_run_date_blanks.py b/courses/migrations/0002_course_run_date_blanks.py index 1983af322..92e1a6ecc 100644 --- a/courses/migrations/0002_course_run_date_blanks.py +++ b/courses/migrations/0002_course_run_date_blanks.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0001_create_course_models")] operations = [ diff --git a/courses/migrations/0003_course_program_defaults.py b/courses/migrations/0003_course_program_defaults.py index 7c962b0af..820943e8a 100644 --- a/courses/migrations/0003_course_program_defaults.py +++ b/courses/migrations/0003_course_program_defaults.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-03-13 20:05 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("courses", "0002_course_run_date_blanks")] operations = [ diff --git a/courses/migrations/0004_change_courseware_url_field.py b/courses/migrations/0004_change_courseware_url_field.py index fb3e002e2..95bb9e20c 100644 --- a/courses/migrations/0004_change_courseware_url_field.py +++ b/courses/migrations/0004_change_courseware_url_field.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0003_course_program_defaults")] operations = [ diff --git a/courses/migrations/0005_remove_desc_and_thumbnail_fields.py b/courses/migrations/0005_remove_desc_and_thumbnail_fields.py index 2b454c8cf..c43917e69 100644 --- a/courses/migrations/0005_remove_desc_and_thumbnail_fields.py +++ b/courses/migrations/0005_remove_desc_and_thumbnail_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0004_change_courseware_url_field")] operations = [ diff --git a/courses/migrations/0006_update_related_name.py b/courses/migrations/0006_update_related_name.py index 1bc747543..3ef987506 100644 --- a/courses/migrations/0006_update_related_name.py +++ b/courses/migrations/0006_update_related_name.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-17 18:59 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("courses", "0005_remove_desc_and_thumbnail_fields")] operations = [ diff --git a/courses/migrations/0007_add_enrollment_models.py b/courses/migrations/0007_add_enrollment_models.py index cd36faae1..c09524802 100644 --- a/courses/migrations/0007_add_enrollment_models.py +++ b/courses/migrations/0007_add_enrollment_models.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-08 19:07 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("courses", "0006_update_related_name"), diff --git a/courses/migrations/0008_enrollment_company.py b/courses/migrations/0008_enrollment_company.py index 00cbcbfc7..25f0c1408 100644 --- a/courses/migrations/0008_enrollment_company.py +++ b/courses/migrations/0008_enrollment_company.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-15 13:15 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0010_remove_ecommerce_course_run_enrollment"), ("courses", "0007_add_enrollment_models"), diff --git a/courses/migrations/0009_enrollment_statuses.py b/courses/migrations/0009_enrollment_statuses.py index a6053be31..85a1266f8 100644 --- a/courses/migrations/0009_enrollment_statuses.py +++ b/courses/migrations/0009_enrollment_statuses.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0008_enrollment_company")] operations = [ diff --git a/courses/migrations/0010_enrollment_audit_tables.py b/courses/migrations/0010_enrollment_audit_tables.py index 7d1f90696..6d650d41e 100644 --- a/courses/migrations/0010_enrollment_audit_tables.py +++ b/courses/migrations/0010_enrollment_audit_tables.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-06-13 19:18 -from django.conf import settings import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("courses", "0009_enrollment_statuses"), diff --git a/courses/migrations/0011_enrollment_change_status_fields.py b/courses/migrations/0011_enrollment_change_status_fields.py index 08e4f6252..e9d47d073 100644 --- a/courses/migrations/0011_enrollment_change_status_fields.py +++ b/courses/migrations/0011_enrollment_change_status_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0010_enrollment_audit_tables")] operations = [ diff --git a/courses/migrations/0013_readable_id_required.py b/courses/migrations/0013_readable_id_required.py index 4b0740eaf..3a5c8aeba 100644 --- a/courses/migrations/0013_readable_id_required.py +++ b/courses/migrations/0013_readable_id_required.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0012_backpopulate_readable_id")] operations = [ diff --git a/courses/migrations/0014_enrollment_blankable.py b/courses/migrations/0014_enrollment_blankable.py index ded6208f2..c1db4b294 100644 --- a/courses/migrations/0014_enrollment_blankable.py +++ b/courses/migrations/0014_enrollment_blankable.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-06-18 21:34 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("courses", "0013_readable_id_required")] operations = [ diff --git a/courses/migrations/0015_courseware_not_null.py b/courses/migrations/0015_courseware_not_null.py index 15691c0ad..c05a6d7c2 100644 --- a/courses/migrations/0015_courseware_not_null.py +++ b/courses/migrations/0015_courseware_not_null.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0014_enrollment_blankable")] operations = [ diff --git a/courses/migrations/0016_enrollment_order.py b/courses/migrations/0016_enrollment_order.py index f8266e045..92957acee 100644 --- a/courses/migrations/0016_enrollment_order.py +++ b/courses/migrations/0016_enrollment_order.py @@ -1,9 +1,9 @@ # Generated by Django 2.1.7 on 2019-06-26 16:17 import logging +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion log = logging.getLogger() @@ -80,7 +80,6 @@ def get_order(user, product): class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("ecommerce", "0013_coupon_assignment_email_index"), diff --git a/courses/migrations/0017_courserun_expiration_date.py b/courses/migrations/0017_courserun_expiration_date.py index 15f0abf41..23eacb665 100644 --- a/courses/migrations/0017_courserun_expiration_date.py +++ b/courses/migrations/0017_courserun_expiration_date.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0016_enrollment_order")] operations = [ diff --git a/courses/migrations/0018_backpopulate_courserun_expiration_date.py b/courses/migrations/0018_backpopulate_courserun_expiration_date.py index 0d492d31d..09dcef9ce 100644 --- a/courses/migrations/0018_backpopulate_courserun_expiration_date.py +++ b/courses/migrations/0018_backpopulate_courserun_expiration_date.py @@ -18,7 +18,6 @@ def backpopulate_expiration_date(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("courses", "0017_courserun_expiration_date")] operations = [ diff --git a/courses/migrations/0019_courseruncertificate_courserungrade_courserungradeaudit_programcertificate.py b/courses/migrations/0019_courseruncertificate_courserungrade_courserungradeaudit_programcertificate.py index 496e8545b..b03a4ce0f 100644 --- a/courses/migrations/0019_courseruncertificate_courserungrade_courserungradeaudit_programcertificate.py +++ b/courses/migrations/0019_courseruncertificate_courserungrade_courserungradeaudit_programcertificate.py @@ -1,15 +1,15 @@ # Generated by Django 2.2.3 on 2019-08-16 18:40 -from django.conf import settings +import uuid + import django.contrib.postgres.fields.jsonb import django.core.validators -from django.db import migrations, models import django.db.models.deletion -import uuid +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("courses", "0018_backpopulate_courserun_expiration_date"), diff --git a/courses/migrations/0020_course_run_grade_optional_letter_grade.py b/courses/migrations/0020_course_run_grade_optional_letter_grade.py index d94bc6cb0..3ec5d0a04 100644 --- a/courses/migrations/0020_course_run_grade_optional_letter_grade.py +++ b/courses/migrations/0020_course_run_grade_optional_letter_grade.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ( "courses", diff --git a/courses/migrations/0021_courserungrade_set_by_admin.py b/courses/migrations/0021_courserungrade_set_by_admin.py index 7d48ca41b..1c0762fb2 100644 --- a/courses/migrations/0021_courserungrade_set_by_admin.py +++ b/courses/migrations/0021_courserungrade_set_by_admin.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0020_course_run_grade_optional_letter_grade")] operations = [ diff --git a/courses/migrations/0022_topics.py b/courses/migrations/0022_topics.py index 9ffd58daa..d75c1e429 100644 --- a/courses/migrations/0022_topics.py +++ b/courses/migrations/0022_topics.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0021_courserungrade_set_by_admin")] operations = [ diff --git a/courses/migrations/0023_revoke_certificate.py b/courses/migrations/0023_revoke_certificate.py index 54041c08f..1351b5bc9 100644 --- a/courses/migrations/0023_revoke_certificate.py +++ b/courses/migrations/0023_revoke_certificate.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0022_topics")] operations = [ diff --git a/courses/migrations/0024_programrun.py b/courses/migrations/0024_programrun.py index a0a0ad61f..a8031af87 100644 --- a/courses/migrations/0024_programrun.py +++ b/courses/migrations/0024_programrun.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.8 on 2020-02-07 18:50 import django.core.validators -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("courses", "0023_revoke_certificate")] operations = [ diff --git a/courses/migrations/0025_run_tag.py b/courses/migrations/0025_run_tag.py index f21c18711..df6916eb2 100644 --- a/courses/migrations/0025_run_tag.py +++ b/courses/migrations/0025_run_tag.py @@ -4,13 +4,12 @@ from django.db import migrations, models - MAX_RUN_TAG_LENGTH = 10 ENROLLABLE_ITEM_ID_SEPARATOR = "+" TEXT_ID_RUN_SUFFIX_PATTERN = r"\+(?PR\d+)$" -def backfill_run_tags_from_text_id(course_run_qset): +def backfill_run_tags_from_text_id(course_run_qset): # noqa: D103 run_tag_map = defaultdict(list) for run in course_run_qset: potential_run_tag = run.courseware_id.split(ENROLLABLE_ITEM_ID_SEPARATOR)[-1] @@ -27,13 +26,13 @@ def backfill_run_tags_from_text_id(course_run_qset): run_to_update.save() -def backfill_run_tags_from_id(course_run_qset): +def backfill_run_tags_from_id(course_run_qset): # noqa: D103 for run in course_run_qset: run.run_tag = str(run.id) run.save() -def backfill_course_run_run_tags(apps, schema_editor): +def backfill_course_run_run_tags(apps, schema_editor): # noqa: D103 Course = apps.get_model("courses", "Course") CourseRun = apps.get_model("courses", "CourseRun") @@ -56,7 +55,6 @@ def backfill_course_run_run_tags(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("courses", "0024_programrun")] operations = [ diff --git a/courses/migrations/0026_nullify_expiration_date.py b/courses/migrations/0026_nullify_expiration_date.py index 529e5e641..db808f875 100644 --- a/courses/migrations/0026_nullify_expiration_date.py +++ b/courses/migrations/0026_nullify_expiration_date.py @@ -14,7 +14,6 @@ def nullify_courserun_expiration_dates(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("courses", "0025_run_tag")] operations = [ diff --git a/courses/migrations/0027_jsonField_from_django_models.py b/courses/migrations/0027_jsonField_from_django_models.py index c9a03d2e9..b3f145a06 100644 --- a/courses/migrations/0027_jsonField_from_django_models.py +++ b/courses/migrations/0027_jsonField_from_django_models.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("courses", "0026_nullify_expiration_date")] operations = [ diff --git a/courses/migrations/0028_certificate_page_revision_for_course_and_program.py b/courses/migrations/0028_certificate_page_revision_for_course_and_program.py index 92062698a..3ed9e9d2b 100644 --- a/courses/migrations/0028_certificate_page_revision_for_course_and_program.py +++ b/courses/migrations/0028_certificate_page_revision_for_course_and_program.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.14 on 2022-08-31 14:14 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models def set_current_certificate_revision(apps, schema_editor): @@ -41,7 +41,6 @@ def set_current_certificate_revision(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0062_comment_models_and_pagesubscription"), ("courses", "0027_jsonField_from_django_models"), diff --git a/courses/migrations/0029_revert_certificates_prior_aug_8.py b/courses/migrations/0029_revert_certificates_prior_aug_8.py index 8b9dd235b..578587de9 100644 --- a/courses/migrations/0029_revert_certificates_prior_aug_8.py +++ b/courses/migrations/0029_revert_certificates_prior_aug_8.py @@ -1,11 +1,12 @@ # Generated by Django 3.2.15 on 2022-10-26 15:40 -from django.db import migrations from datetime import datetime, timezone + +from django.db import migrations + from cms.models import CertificatePage from courses.models import CourseRunCertificate, ProgramCertificate - AUGUST_8_2022 = datetime(2022, 8, 8, tzinfo=timezone.utc) SEPTEMBER_20_2022 = datetime(2022, 9, 20, tzinfo=timezone.utc) diff --git a/courses/migrations/0030_add_courseware_external_fields.py b/courses/migrations/0030_add_courseware_external_fields.py index 0eb946778..0bbbd6348 100644 --- a/courses/migrations/0030_add_courseware_external_fields.py +++ b/courses/migrations/0030_add_courseware_external_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("courses", "0029_revert_certificates_prior_aug_8"), ] diff --git a/courses/migrations/0031_create_topics_sublevel.py b/courses/migrations/0031_create_topics_sublevel.py index 8d91fcd6e..f08207337 100644 --- a/courses/migrations/0031_create_topics_sublevel.py +++ b/courses/migrations/0031_create_topics_sublevel.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.18 on 2023-03-24 11:36 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0030_add_courseware_external_fields"), ] diff --git a/courses/migrations/0032_remove_external_marketing_url_field.py b/courses/migrations/0032_remove_external_marketing_url_field.py index 84a06d393..cd81441d6 100644 --- a/courses/migrations/0032_remove_external_marketing_url_field.py +++ b/courses/migrations/0032_remove_external_marketing_url_field.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("courses", "0031_create_topics_sublevel"), ] diff --git a/courses/migrations/0033_remove_course_coursetopic_association.py b/courses/migrations/0033_remove_course_coursetopic_association.py index fe76debe7..9a1d22349 100644 --- a/courses/migrations/0033_remove_course_coursetopic_association.py +++ b/courses/migrations/0033_remove_course_coursetopic_association.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("courses", "0032_remove_external_marketing_url_field"), ] diff --git a/courses/migrations/0034_make_certificate_revision_nullable.py b/courses/migrations/0034_make_certificate_revision_nullable.py index 1a8c14831..bc2f3c529 100644 --- a/courses/migrations/0034_make_certificate_revision_nullable.py +++ b/courses/migrations/0034_make_certificate_revision_nullable.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.20 on 2023-08-10 10:50 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("wagtailcore", "0062_comment_models_and_pagesubscription"), ("courses", "0033_remove_course_coursetopic_association"), diff --git a/courses/migrations/0035_limit_certificate_revision_choices.py b/courses/migrations/0035_limit_certificate_revision_choices.py index 03bff65cd..fe12dee47 100644 --- a/courses/migrations/0035_limit_certificate_revision_choices.py +++ b/courses/migrations/0035_limit_certificate_revision_choices.py @@ -1,12 +1,12 @@ # Generated by Django 3.2.20 on 2023-08-10 10:51 -import courses.models -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models +import courses.models -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ ("wagtailcore", "0062_comment_models_and_pagesubscription"), ("courses", "0034_make_certificate_revision_nullable"), diff --git a/courses/migrations/0036_add_platform_model.py b/courses/migrations/0036_add_platform_model.py index 1c2e6d184..764c93643 100644 --- a/courses/migrations/0036_add_platform_model.py +++ b/courses/migrations/0036_add_platform_model.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("courses", "0035_limit_certificate_revision_choices"), ] diff --git a/courses/migrations/0037_make_platform_non_nullable.py b/courses/migrations/0037_make_platform_non_nullable.py index df1fc9f81..541d32409 100644 --- a/courses/migrations/0037_make_platform_non_nullable.py +++ b/courses/migrations/0037_make_platform_non_nullable.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.21 on 2023-10-05 23:44 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models from courses.constants import DEFAULT_PLATFORM_NAME @@ -33,7 +33,6 @@ def populate_courseware_platform(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("courses", "0036_add_platform_model"), ] diff --git a/courses/migrations/0038_alter_certificate_page_revision.py b/courses/migrations/0038_alter_certificate_page_revision.py index 693c2f47a..800307349 100644 --- a/courses/migrations/0038_alter_certificate_page_revision.py +++ b/courses/migrations/0038_alter_certificate_page_revision.py @@ -1,12 +1,12 @@ # Generated by Django 3.2.23 on 2023-11-21 11:28 -import courses.models -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models +import courses.models -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ ("wagtailcore", "0089_log_entry_data_json_null_to_object"), ("courses", "0037_make_platform_non_nullable"), diff --git a/courses/models.py b/courses/models.py index f6900494c..8607a2a76 100644 --- a/courses/models.py +++ b/courses/models.py @@ -1,7 +1,6 @@ """ Course models """ -# pylint: disable=too-many-lines import logging import operator as op import uuid @@ -31,7 +30,6 @@ serialize_model_object, ) - User = get_user_model() log = logging.getLogger(__name__) @@ -50,7 +48,7 @@ def get_queryset(self): return super().get_queryset().filter(is_revoked=False) -class ProgramQuerySet(models.QuerySet): # pylint: disable=missing-docstring +class ProgramQuerySet(models.QuerySet): # noqa: D101 def live(self): """Applies a filter for Programs with live=True""" return self.filter(live=True) @@ -60,13 +58,13 @@ def with_text_id(self, text_id): return self.filter(readable_id=text_id) -class CourseQuerySet(models.QuerySet): # pylint: disable=missing-docstring +class CourseQuerySet(models.QuerySet): # noqa: D101 def live(self): """Applies a filter for Courses with live=True""" return self.filter(live=True) -class CourseRunQuerySet(models.QuerySet): # pylint: disable=missing-docstring +class CourseRunQuerySet(models.QuerySet): # noqa: D101 def live(self): """Applies a filter for Course runs with live=True""" return self.filter(live=True) @@ -145,7 +143,7 @@ def parent_topics_with_annotated_course_counts(self): ), ) ) - return topics_queryset + return topics_queryset # noqa: RET504 class ActiveEnrollmentManager(models.Manager): @@ -201,7 +199,7 @@ def catalog_image_url(self): validate_url_path_field = RegexValidator( - r"^[{}]+$".format(detail_path_char_pattern), + r"^[{}]+$".format(detail_path_char_pattern), # noqa: UP032 "This field is used to produce URL paths. It must contain only characters that match this pattern: [{}]".format( detail_path_char_pattern ), @@ -242,7 +240,7 @@ class Program(TimestampedModel, PageProperties, ValidateOnSaveMixin): """Model for a course program""" objects = ProgramQuerySet.as_manager() - title = models.CharField(max_length=255) + title = models.CharField(max_length=255) # noqa: DJ012 readable_id = models.CharField( max_length=255, unique=True, validators=[validate_url_path_field] ) @@ -276,7 +274,7 @@ def next_run_date(self): ), None, ) - if first_course: + if first_course: # noqa: RET503 return first_course.next_run_date @property @@ -288,7 +286,7 @@ def is_catalog_visible(self): @property def current_price(self): """Gets the price if it exists""" - product = list(self.products.all())[0] if self.products.all() else None + product = list(self.products.all())[0] if self.products.all() else None # noqa: RUF015 if not product: return None latest_version = product.latest_version @@ -307,7 +305,7 @@ def first_unexpired_run(self): ), None, ) - if first_course: + if first_course: # noqa: RET503 return first_course.first_unexpired_run @property @@ -321,7 +319,7 @@ def first_course_unexpired_runs(self): ), None, ) - if first_course: + if first_course: # noqa: RET503 return first_course.unexpired_runs @property @@ -348,9 +346,9 @@ def course_runs(self): """All course runs related to a program""" return [run for course in self.courses.all() for run in course.courseruns.all()] - def __str__(self): + def __str__(self): # noqa: DJ012 title = f"{self.readable_id} | {self.title}" - return title if len(title) <= 100 else title[:97] + "..." + return title if len(title) <= 100 else title[:97] + "..." # noqa: PLR2004 class ProgramRun(TimestampedModel, ValidateOnSaveMixin): @@ -439,7 +437,7 @@ class Course(TimestampedModel, PageProperties, ValidateOnSaveMixin): """Model for a course""" objects = CourseQuerySet.as_manager() - program = models.ForeignKey( + program = models.ForeignKey( # noqa: DJ012 Program, on_delete=models.CASCADE, null=True, blank=True, related_name="courses" ) position_in_program = models.PositiveSmallIntegerField(null=True, blank=True) @@ -513,7 +511,7 @@ def first_unexpired_run(self): ] return first_matching_item( sorted(eligible_course_runs, key=lambda course_run: course_run.start_date), - lambda course_run: True, + lambda course_run: True, # noqa: ARG005 ) @property @@ -572,10 +570,10 @@ def available_runs(self, user): ).values_list("run__id", flat=True) return [run for run in self.unexpired_runs if run.id not in enrolled_runs] - class Meta: + class Meta: # noqa: DJ012 ordering = ("program", "title") - def save(self, *args, **kwargs): # pylint: disable=signature-differs + def save(self, *args, **kwargs): # noqa: DJ012 """Overridden save method""" # If adding a Course to a Program without position specified, set it as the highest position + 1. # WARNING: This is open to a race condition. Two near-simultaneous queries could end up with @@ -591,9 +589,9 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs self.position_in_program = 1 if not last_position else last_position + 1 return super().save(*args, **kwargs) - def __str__(self): + def __str__(self): # noqa: DJ012 title = f"{self.readable_id} | {self.title}" - return title if len(title) <= 100 else title[:97] + "..." + return title if len(title) <= 100 else title[:97] + "..." # noqa: PLR2004 class CourseRun(TimestampedModel): @@ -610,7 +608,7 @@ class CourseRun(TimestampedModel): max_length=10, help_text="A string that identifies the set of runs that this run belongs to (example: 'R2')", ) - courseware_url_path = models.CharField(max_length=500, blank=True, null=True) + courseware_url_path = models.CharField(max_length=500, blank=True, null=True) # noqa: DJ001 start_date = models.DateTimeField(null=True, blank=True, db_index=True) end_date = models.DateTimeField(null=True, blank=True, db_index=True) enrollment_start = models.DateTimeField(null=True, blank=True, db_index=True) @@ -683,7 +681,7 @@ def courseware_url(self): @property def current_price(self): """Gets the price if it exists""" - product = list(self.products.all())[0] if self.products.all() else None + product = list(self.products.all())[0] if self.products.all() else None # noqa: RUF015 if not product: return None latest_version = product.latest_version @@ -703,7 +701,7 @@ def instructors(self): def __str__(self): title = f"{self.courseware_id} | {self.title}" - return title if len(title) <= 100 else title[:97] + "..." + return title if len(title) <= 100 else title[:97] + "..." # noqa: PLR2004 def clean(self): """ @@ -719,13 +717,17 @@ def clean(self): return if self.start_date and self.expiration_date < self.start_date: - raise ValidationError("Expiration date must be later than start date.") + raise ValidationError("Expiration date must be later than start date.") # noqa: EM101 if self.end_date and self.expiration_date < self.end_date: - raise ValidationError("Expiration date must be later than end date.") + raise ValidationError("Expiration date must be later than end date.") # noqa: EM101 def save( - self, force_insert=False, force_update=False, using=None, update_fields=None + self, + force_insert=False, # noqa: FBT002 + force_update=False, # noqa: FBT002 + using=None, + update_fields=None, ): """ Overriding the save method to inject clean into it @@ -750,7 +752,7 @@ class Meta: "ecommerce.Company", null=True, blank=True, on_delete=models.PROTECT ) order = models.ForeignKey("ecommerce.Order", null=True, on_delete=models.PROTECT) - change_status = models.CharField( + change_status = models.CharField( # noqa: DJ001 choices=ENROLL_CHANGE_STATUS_CHOICES, max_length=20, null=True, blank=True ) active = models.BooleanField( @@ -762,14 +764,14 @@ class Meta: all_objects = models.Manager() @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 raise NotImplementedError @classmethod - def objects_for_audit(cls): + def objects_for_audit(cls): # noqa: D102 return cls.all_objects - def to_dict(self): + def to_dict(self): # noqa: D102 return { **serialize_model_object(self), "username": self.user.username, @@ -778,13 +780,13 @@ def to_dict(self): "company_name": self.company.name if self.company else None, } - def deactivate_and_save(self, change_status, no_user=False): + def deactivate_and_save(self, change_status, no_user=False): # noqa: FBT002 """Sets an enrollment to inactive, sets the status, and saves""" self.active = False self.change_status = change_status return self.save_and_log(None if no_user else self.user) - def reactivate_and_save(self, no_user=False): + def reactivate_and_save(self, no_user=False): # noqa: FBT002 """Sets an enrollment to be active again and saves""" self.active = True self.change_status = None @@ -811,7 +813,7 @@ def is_ended(self): return self.run.is_past @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 return CourseRunEnrollmentAudit @classmethod @@ -826,12 +828,12 @@ def get_program_run_enrollments(cls, user, program, order_id=None): Returns: queryset of CourseRunEnrollment: Course run enrollments associated with a user/program """ - added_filters = {} if order_id is None else dict(order_id=order_id) + added_filters = {} if order_id is None else dict(order_id=order_id) # noqa: C408 return cls.objects.filter( user=user, run__course__program=program, **added_filters ) - def to_dict(self): + def to_dict(self): # noqa: D102 return {**super().to_dict(), "text_id": self.run.courseware_id} def __str__(self): @@ -846,7 +848,7 @@ class CourseRunEnrollmentAudit(AuditModel): ) @classmethod - def get_related_field_name(cls): + def get_related_field_name(cls): # noqa: D102 return "enrollment" @@ -866,7 +868,7 @@ def is_ended(self): return all(enrollment.run.is_past for enrollment in self.get_run_enrollments()) @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 return ProgramEnrollmentAudit def get_run_enrollments(self, order_id=None): @@ -879,12 +881,12 @@ def get_run_enrollments(self, order_id=None): Returns: queryset of CourseRunEnrollment: Associated course run enrollments """ - added_filters = {} if order_id is None else dict(order_id=order_id) + added_filters = {} if order_id is None else dict(order_id=order_id) # noqa: C408 return CourseRunEnrollment.get_program_run_enrollments( user=self.user, program=self.program, **added_filters ) - def to_dict(self): + def to_dict(self): # noqa: D102 return {**super().to_dict(), "text_id": self.program.readable_id} def __str__(self): @@ -899,7 +901,7 @@ class ProgramEnrollmentAudit(AuditModel): ) @classmethod - def get_related_field_name(cls): + def get_related_field_name(cls): # noqa: D102 return "enrollment" @@ -913,7 +915,7 @@ class CourseRunGrade(TimestampedModel, AuditableModel, ValidateOnSaveMixin): grade = models.FloatField( null=False, validators=[MinValueValidator(0.0), MaxValueValidator(1.0)] ) - letter_grade = models.CharField(max_length=6, blank=True, null=True) + letter_grade = models.CharField(max_length=6, blank=True, null=True) # noqa: DJ001 passed = models.BooleanField(default=False) set_by_admin = models.BooleanField(default=False) @@ -921,10 +923,10 @@ class Meta: unique_together = ("user", "course_run") @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 return CourseRunGradeAudit - def to_dict(self): + def to_dict(self): # noqa: D102 return serialize_model_object(self) @property @@ -948,7 +950,7 @@ class CourseRunGradeAudit(AuditModel): ) @classmethod - def get_related_field_name(cls): + def get_related_field_name(cls): # noqa: D102 return "course_run_grade" @@ -1007,19 +1009,19 @@ class CourseRunCertificate(TimestampedModel, BaseCertificate): ) objects = ActiveCertificates() - all_objects = models.Manager() + all_objects = models.Manager() # noqa: DJ012 class Meta: unique_together = ("user", "course_run") - def get_certified_object_id(self): + def get_certified_object_id(self): # noqa: D102 return self.course_run_id def get_courseware_object_id(self): """Gets the course id instead of the course run id""" return self.course_run.course_id - def get_courseware_object_readable_id(self): + def get_courseware_object_readable_id(self): # noqa: D102 return self.course_run.courseware_id @property @@ -1029,14 +1031,14 @@ def link(self): Format: /certificate// Example: /certificate/93ebd74e-5f88-4b47-bb09-30a6d575328f/ """ - return "/certificate/{}/".format(str(self.uuid)) + return f"/certificate/{str(self.uuid)}/" # noqa: RUF010 @property def start_end_dates(self): """Returns the start and end date for courseware object duration""" return self.course_run.start_date, self.course_run.end_date - def __str__(self): + def __str__(self): # noqa: DJ012 return ( 'CourseRunCertificate for user={user}, run={course_run} ({uuid})"'.format( user=self.user.username, @@ -1045,7 +1047,7 @@ def __str__(self): ) ) - def save(self, *args, **kwargs): # pylint: disable=signature-differs + def save(self, *args, **kwargs): # noqa: D102, DJ012 if not self.certificate_page_revision: certificate_page = ( self.course_run.course.page.certificate_page @@ -1056,7 +1058,7 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs self.certificate_page_revision = certificate_page.get_latest_revision() super().save(*args, **kwargs) - def clean(self): + def clean(self): # noqa: D102 from cms.models import CertificatePage, CoursePage # If user has not selected a revision, Let create the certificate since we have made the revision nullable @@ -1093,19 +1095,19 @@ class ProgramCertificate(TimestampedModel, BaseCertificate): ) objects = ActiveCertificates() - all_objects = models.Manager() + all_objects = models.Manager() # noqa: DJ012 class Meta: unique_together = ("user", "program") - def get_certified_object_id(self): + def get_certified_object_id(self): # noqa: D102 return self.program_id def get_courseware_object_id(self): """Gets the program id""" return self.program_id - def get_courseware_object_readable_id(self): + def get_courseware_object_readable_id(self): # noqa: D102 return self.program.readable_id @property @@ -1115,7 +1117,7 @@ def link(self): Format: /certificate/program// Example: /certificate/program/93ebd74e-5f88-4b47-bb09-30a6d575328f/ """ - return "/certificate/program/{}/".format(str(self.uuid)) + return f"/certificate/program/{str(self.uuid)}/" # noqa: RUF010 @property def start_end_dates(self): @@ -1132,12 +1134,12 @@ def start_end_dates(self): ) return dates["start_date"], dates["end_date"] - def __str__(self): + def __str__(self): # noqa: DJ012 return 'ProgramCertificate for user={user}, program={program} ({uuid})"'.format( user=self.user.username, program=self.program.text_id, uuid=self.uuid ) - def clean(self): + def clean(self): # noqa: D102 from cms.models import CertificatePage, ProgramPage # If user has not selected a revision, Let create the certificate since we have made the revision nullable @@ -1158,7 +1160,7 @@ def clean(self): } ) - def save(self, *args, **kwargs): # pylint: disable=signature-differs + def save(self, *args, **kwargs): # noqa: D102, DJ012 if not self.certificate_page_revision: certificate_page = ( self.program.page.certificate_page if self.program.page else None diff --git a/courses/models_test.py b/courses/models_test.py index 3d9ae52e0..9cdc36e9e 100644 --- a/courses/models_test.py +++ b/courses/models_test.py @@ -30,7 +30,6 @@ from mitxpro.utils import now_in_utc from users.factories import UserFactory - pytestmark = [pytest.mark.django_db] @@ -223,7 +222,7 @@ def test_courseware_url(settings): assert course_run_no_path.courseware_url is None -@pytest.mark.parametrize("end_days,expected", [[-1, True], [1, False], [None, False]]) +@pytest.mark.parametrize("end_days,expected", [[-1, True], [1, False], [None, False]]) # noqa: PT006, PT007 def test_course_run_past(end_days, expected): """ Test that CourseRun.is_past returns the expected boolean value @@ -234,7 +233,8 @@ def test_course_run_past(end_days, expected): @pytest.mark.parametrize( - "start_delta, end_delta, expiration_delta", [[-1, 2, 3], [1, 3, 4], [10, 20, 30]] + "start_delta, end_delta, expiration_delta", # noqa: PT006 + [[-1, 2, 3], [1, 3, 4], [10, 20, 30]], # noqa: PT007 ) def test_course_run_expiration_date(start_delta, end_delta, expiration_delta): """ @@ -253,7 +253,8 @@ def test_course_run_expiration_date(start_delta, end_delta, expiration_delta): @pytest.mark.parametrize( - "start_delta, end_delta, expiration_delta", [[1, 2, 1], [1, 2, -1]] + "start_delta, end_delta, expiration_delta", # noqa: PT006 + [[1, 2, 1], [1, 2, -1]], # noqa: PT007 ) def test_course_run_invalid_expiration_date(start_delta, end_delta, expiration_delta): """ @@ -269,16 +270,16 @@ def test_course_run_invalid_expiration_date(start_delta, end_delta, expiration_d @pytest.mark.parametrize( - "end_days, enroll_start_days, enroll_end_days, expected", + "end_days, enroll_start_days, enroll_end_days, expected", # noqa: PT006 [ - [None, None, None, True], - [None, None, 1, True], - [None, None, -1, False], - [1, None, None, True], - [-1, None, None, False], - [1, None, -1, False], - [None, 1, None, False], - [None, -1, None, True], + [None, None, None, True], # noqa: PT007 + [None, None, 1, True], # noqa: PT007 + [None, None, -1, False], # noqa: PT007 + [1, None, None, True], # noqa: PT007 + [-1, None, None, False], # noqa: PT007 + [1, None, -1, False], # noqa: PT007 + [None, 1, None, False], # noqa: PT007 + [None, -1, None, True], # noqa: PT007 ], ) def test_course_run_not_beyond_enrollment( @@ -307,7 +308,8 @@ def test_course_run_not_beyond_enrollment( @pytest.mark.parametrize( - "end_days,enroll_days,expected", [[-1, 1, False], [1, -1, False], [1, 1, True]] + "end_days,enroll_days,expected", # noqa: PT006 + [[-1, 1, False], [1, -1, False], [1, 1, True]], # noqa: PT007 ) def test_course_run_unexpired(end_days, enroll_days, expected): """ @@ -568,7 +570,7 @@ def test_course_unexpired_runs(): def test_course_available_runs(): - """enrolled runs for a user should not be in the list of available runs""" + """Enrolled runs for a user should not be in the list of available runs""" user = UserFactory.create() course = CourseFactory.create() runs = CourseRunFactory.create_batch(2, course=course, live=True) diff --git a/courses/serializers.py b/courses/serializers.py index 411266a46..7118a52ad 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -88,7 +88,7 @@ def get_instructors(self, instance): class Meta: model = models.CourseRun - fields = BaseCourseRunSerializer.Meta.fields + [ + fields = BaseCourseRunSerializer.Meta.fields + [ # noqa: RUF005 "product_id", "instructors", "current_price", @@ -119,11 +119,11 @@ def get_url(self, instance): return page.get_full_url() if page else None def get_external_marketing_url(self, instance): - """Returns the external marketing URL for the course that's set in CMS page""" + """Return the external marketing URL for the course that's set in CMS page""" return instance.page.external_marketing_url if instance.page else None def get_marketing_hubspot_form_id(self, instance): - """Returns the marketing HubSpot form ID associated with the course that's set in CMS page""" + """Return the marketing HubSpot form ID associated with the course that's set in CMS page""" return instance.page.marketing_hubspot_form_id if instance.page else None def get_thumbnail_url(self, instance): @@ -187,7 +187,7 @@ def get_credits(self, instance): else None ) - def get_format(self, instance): # pylint: disable=unused-argument + def get_format(self, instance): """Returns the format of the course""" return instance.page.format if instance.page and instance.page.format else None @@ -341,11 +341,11 @@ def get_url(self, instance): return page.get_full_url() if page else None def get_external_marketing_url(self, instance): - """Returns the external marketing URL for this program that's set in CMS page""" + """Return the external marketing URL for this program that's set in CMS page""" return instance.page.external_marketing_url if instance.page else None def get_marketing_hubspot_form_id(self, instance): - """Returns the marketing HubSpot form ID associated with the program that's set in CMS page""" + """Return the marketing HubSpot form ID associated with the program that's set in CMS page""" return instance.page.marketing_hubspot_form_id if instance.page else None def get_instructors(self, instance): @@ -354,7 +354,7 @@ def get_instructors(self, instance): def get_topics(self, instance): """List all topics in all courses in the program""" - topics = set( + topics = set( # noqa: C401 topic.name for course in instance.courses.all() if course.page @@ -382,7 +382,7 @@ def get_credits(self, instance): else None ) - def get_format(self, instance): # pylint: disable=unused-argument + def get_format(self, instance): """Returns the format of the program""" return instance.page.format if instance.page and instance.page.format else None @@ -517,7 +517,7 @@ def get_receipt(self, enrollment): """ Resolve a receipt for this enrollment """ - if enrollment.order: + if enrollment.order: # noqa: RET503 return ( enrollment.order_id if enrollment.order @@ -527,7 +527,7 @@ def get_receipt(self, enrollment): ) def __init__(self, *args, **kwargs): - assert ( + assert ( # noqa: PT018, S101 "context" in kwargs and "course_run_enrollments" in kwargs["context"] ), "An iterable of course run enrollments must be passed in the context (key: course_run_enrollments)" super().__init__(*args, **kwargs) diff --git a/courses/serializers_test.py b/courses/serializers_test.py index fa67ee41b..762d5b679 100644 --- a/courses/serializers_test.py +++ b/courses/serializers_test.py @@ -1,15 +1,14 @@ """ Tests for course serializers """ -# pylint: disable=unused-argument, redefined-outer-name from datetime import datetime, timedelta, timezone import factory import pytest from django.contrib.auth.models import AnonymousUser -from cms.factories import FacultyMembersPageFactory from cms.constants import FORMAT_ONLINE, FORMAT_OTHER +from cms.factories import FacultyMembersPageFactory from courses.factories import ( CourseFactory, CourseRunEnrollmentFactory, @@ -54,7 +53,7 @@ def test_base_program_serializer(): @pytest.mark.parametrize("is_external", [True, False]) @pytest.mark.parametrize("program_format", [FORMAT_ONLINE, FORMAT_OTHER]) @pytest.mark.parametrize( - "duration, time_commitment, video_url, ceus, external_marketing_url, marketing_hubspot_form_id", + "duration, time_commitment, video_url, ceus, external_marketing_url, marketing_hubspot_form_id", # noqa: PT006 [ ( "2 Months", @@ -74,7 +73,7 @@ def test_base_program_serializer(): ), ], ) -def test_serialize_program( +def test_serialize_program( # noqa: PLR0913 mock_context, has_product, is_external, @@ -85,7 +84,7 @@ def test_serialize_program( ceus, external_marketing_url, marketing_hubspot_form_id, -): # pylint: disable=too-many-arguments,too-many-locals +): """Test Program serialization""" program = ProgramFactory.create( @@ -183,7 +182,7 @@ def test_base_course_serializer(): @pytest.mark.parametrize("course_page", [True, False]) @pytest.mark.parametrize("course_format", [FORMAT_ONLINE, FORMAT_OTHER]) @pytest.mark.parametrize( - "duration, time_commitment, video_url, ceus, external_marketing_url, marketing_hubspot_form_id", + "duration, time_commitment, video_url, ceus, external_marketing_url, marketing_hubspot_form_id", # noqa: PT006 [ ( "2 Months", @@ -203,7 +202,7 @@ def test_base_course_serializer(): ), ], ) -def test_serialize_course( +def test_serialize_course( # noqa: PLR0913 mock_context, is_anonymous, all_runs, @@ -216,7 +215,7 @@ def test_serialize_course( ceus, external_marketing_url, marketing_hubspot_form_id, -): # pylint: disable=too-many-arguments,too-many-locals +): """Test Course serialization""" now = datetime.now(tz=timezone.utc) if is_anonymous: @@ -266,11 +265,7 @@ def test_serialize_course( ProductVersionFactory.create(product__content_object=run) data = CourseSerializer(instance=course, context=mock_context).data - - if all_runs or is_anonymous: - expected_runs = unexpired_runs - else: - expected_runs = [course_run] + expected_runs = unexpired_runs if all_runs or is_anonymous else [course_run] assert_drf_json_equal( data, @@ -362,8 +357,8 @@ def test_serialize_course_run_detail(): @pytest.mark.parametrize( - "has_company, receipts_enabled", - [[True, False], [False, False], [False, True], [True, True]], + "has_company, receipts_enabled", # noqa: PT006 + [[True, False], [False, False], [False, True], [True, True]], # noqa: PT007 ) def test_serialize_course_run_enrollments(settings, has_company, receipts_enabled): """Test that CourseRunEnrollmentSerializer has correct data""" @@ -394,8 +389,8 @@ def test_serialize_program_enrollments_assert(): @pytest.mark.parametrize( - "has_company, receipts_enabled", - [[True, False], [False, False], [False, True], [True, True]], + "has_company, receipts_enabled", # noqa: PT006 + [[True, False], [False, False], [False, True], [True, True]], # noqa: PT007 ) def test_serialize_program_enrollments(settings, has_company, receipts_enabled): """Test that ProgramEnrollmentSerializer has correct data""" diff --git a/courses/signals.py b/courses/signals.py index 4738f0255..7b979afe9 100644 --- a/courses/signals.py +++ b/courses/signals.py @@ -15,8 +15,11 @@ dispatch_uid="courseruncertificate_post_save", ) def handle_create_course_run_certificate( - sender, instance, created, **kwargs -): # pylint: disable=unused-argument + sender, # noqa: ARG001 + instance, + created, + **kwargs, # noqa: ARG001 +): """ When a CourseRunCertificate model is created. """ diff --git a/courses/signals_test.py b/courses/signals_test.py index 88c85c52c..801f3f441 100644 --- a/courses/signals_test.py +++ b/courses/signals_test.py @@ -2,19 +2,19 @@ Tests for signals """ from unittest.mock import patch + import pytest + from courses.factories import ( - CourseRunFactory, + CourseFactory, CourseRunCertificateFactory, + CourseRunFactory, UserFactory, - CourseFactory, ) - pytestmark = pytest.mark.django_db -# pylint: disable=unused-argument @patch("courses.signals.transaction.on_commit", side_effect=lambda callback: callback()) @patch("courses.signals.generate_program_certificate", autospec=True) def test_create_course_certificate(generate_program_cert_mock, mock_on_commit): @@ -34,7 +34,6 @@ def test_create_course_certificate(generate_program_cert_mock, mock_on_commit): ) -# pylint: disable=unused-argument @patch("courses.signals.transaction.on_commit", side_effect=lambda callback: callback()) @patch("courses.signals.generate_program_certificate", autospec=True) def test_generate_program_certificate_not_called( diff --git a/courses/tasks.py b/courses/tasks.py index 689719234..5d482cb54 100644 --- a/courses/tasks.py +++ b/courses/tasks.py @@ -83,12 +83,12 @@ def exception_logging_generator(generator): while True: try: yield next(generator) - except StopIteration: + except StopIteration: # noqa: PERF203 return except HTTPError as exc: - log.exception("EdX API error for fetching user grades %s:", exc) - except Exception as exp: # pylint: disable=broad-except - log.exception("Error fetching user grades from edX %s:", exp) + log.exception("EdX API error for fetching user grades %s:", exc) # noqa: TRY401 + except Exception as exp: + log.exception("Error fetching user grades from edX %s:", exp) # noqa: TRY401 @app.task diff --git a/courses/tasks_test.py b/courses/tasks_test.py index f17e58cff..97907095e 100644 --- a/courses/tasks_test.py +++ b/courses/tasks_test.py @@ -2,8 +2,8 @@ import pytest -from courses.tasks import sync_courseruns_data from courses.factories import CourseRunFactory +from courses.tasks import sync_courseruns_data pytestmark = [pytest.mark.django_db] diff --git a/courses/urls.py b/courses/urls.py index e5729a3b8..e18910851 100644 --- a/courses/urls.py +++ b/courses/urls.py @@ -1,11 +1,9 @@ """Course API URL routes""" -from django.urls import path -from django.urls import include, re_path +from django.urls import include, path, re_path from rest_framework import routers from courses.views import v1 - router = routers.SimpleRouter() router.register(r"programs", v1.ProgramViewSet, basename="programs_api") router.register(r"courses", v1.CourseViewSet, basename="courses_api") diff --git a/courses/utils.py b/courses/utils.py index 722e472de..90c47d006 100644 --- a/courses/utils.py +++ b/courses/utils.py @@ -21,11 +21,10 @@ from courseware.api import get_edx_api_course_detail_client from mitxpro.utils import has_equal_properties, now_in_utc - log = logging.getLogger(__name__) -def ensure_course_run_grade(user, course_run, edx_grade, should_update=False): +def ensure_course_run_grade(user, course_run, edx_grade, should_update=False): # noqa: FBT002 """ Ensure that the local grades repository has the grade for the User/CourseRun combination supplied. @@ -271,17 +270,17 @@ def sync_course_runs(runs): course_id=run.courseware_id, username=settings.OPENEDX_SERVICE_WORKER_USERNAME, ) - except HTTPError as e: + except HTTPError as e: # noqa: PERF203 failure_count += 1 if e.response.status_code == HTTP_404_NOT_FOUND: - log.error( + log.error( # noqa: TRY400 "Course not found on edX for readable id: %s", run.courseware_id ) else: - log.error("%s: %s", str(e), run.courseware_id) - except Exception as e: # pylint: disable=broad-except + log.error("%s: %s", str(e), run.courseware_id) # noqa: TRY400 + except Exception as e: # noqa: BLE001 failure_count += 1 - log.error("%s: %s", str(e), run.courseware_id) + log.error("%s: %s", str(e), run.courseware_id) # noqa: TRY400 else: # Reset the expiration_date so it is calculated automatically and # does not raise a validation error now that the start or end date @@ -301,9 +300,9 @@ def sync_course_runs(runs): run.save() success_count += 1 log.info("Updated course run: %s", run.courseware_id) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # noqa: BLE001 # Report any validation or otherwise model errors - log.error("%s: %s", str(e), run.courseware_id) + log.error("%s: %s", str(e), run.courseware_id) # noqa: TRY400 failure_count += 1 return success_count, failure_count diff --git a/courses/utils_test.py b/courses/utils_test.py index 68b85eb5f..f9f1919b0 100644 --- a/courses/utils_test.py +++ b/courses/utils_test.py @@ -1,13 +1,12 @@ -# pylint:disable=redefined-outer-name """ Tests for signals """ from unittest.mock import Mock +import pytest from edx_api.course_detail import CourseDetail, CourseDetails from requests.exceptions import HTTPError -import pytest from courses.factories import ( CourseFactory, CourseRunCertificateFactory, @@ -27,34 +26,33 @@ pytestmark = pytest.mark.django_db -@pytest.fixture() +@pytest.fixture def user(): """User object fixture""" return UserFactory.create() -@pytest.fixture() +@pytest.fixture def program(): """User object fixture""" return ProgramFactory.create() -@pytest.fixture() +@pytest.fixture def course(): """Course object fixture""" return CourseFactory.create() -# pylint: disable=too-many-arguments @pytest.mark.parametrize( - "grade, passed, exp_certificate, exp_created, exp_deleted", + "grade, passed, exp_certificate, exp_created, exp_deleted", # noqa: PT006 [ - [0.25, True, True, True, False], - [0.0, True, False, False, False], - [1.0, False, False, False, False], + [0.25, True, True, True, False], # noqa: PT007 + [0.0, True, False, False, False], # noqa: PT007 + [1.0, False, False, False, False], # noqa: PT007 ], ) -def test_course_run_certificate( +def test_course_run_certificate( # noqa: PLR0913 user, course, grade, passed, exp_certificate, exp_created, exp_deleted ): """ @@ -153,9 +151,9 @@ def test_generate_program_certificate_success(user, program): @pytest.mark.parametrize( - "mocked_api_response, expect_success", + "mocked_api_response, expect_success", # noqa: PT006 [ - [ + [ # noqa: PT007 CourseDetail( { "id": "course-v1:edX+DemoX+2020_T1", @@ -168,7 +166,7 @@ def test_generate_program_certificate_success(user, program): ), True, ], - [ + [ # noqa: PT007 CourseDetail( { "id": "course-v1:edX+DemoX+2020_T1", @@ -181,9 +179,9 @@ def test_generate_program_certificate_success(user, program): ), False, ], - [HTTPError(response=Mock(status_code=404)), False], - [HTTPError(response=Mock(status_code=400)), False], - [ConnectionError(), False], + [HTTPError(response=Mock(status_code=404)), False], # noqa: PT007 + [HTTPError(response=Mock(status_code=400)), False], # noqa: PT007 + [ConnectionError(), False], # noqa: PT007 ], ) def test_sync_course_runs(settings, mocker, mocked_api_response, expect_success): @@ -191,7 +189,7 @@ def test_sync_course_runs(settings, mocker, mocked_api_response, expect_success) Test that sync_course_runs fetches data from edX API. Should fail on API responding with an error, as well as trying to set the course run title to None """ - settings.OPENEDX_SERVICE_WORKER_API_TOKEN = "mock_api_token" + settings.OPENEDX_SERVICE_WORKER_API_TOKEN = "mock_api_token" # noqa: S105 mocker.patch.object(CourseDetails, "get_detail", side_effect=[mocked_api_response]) course_run = CourseRunFactory.create() diff --git a/courses/views/v1/__init__.py b/courses/views/v1/__init__.py index 995b1930d..a6185902d 100644 --- a/courses/views/v1/__init__.py +++ b/courses/views/v1/__init__.py @@ -1,5 +1,5 @@ """Course views verson 1""" -from django.db.models import Count, Prefetch, Q +from django.db.models import Prefetch, Q from mitol.digitalcredentials.mixins import DigitalCredentialsRequestViewSetMixin from rest_framework import status, viewsets from rest_framework.authentication import SessionAuthentication @@ -27,7 +27,6 @@ ProgramSerializer, ) from ecommerce.models import Product -from mitxpro.utils import now_in_utc class ProgramViewSet(viewsets.ReadOnlyModelViewSet): @@ -68,7 +67,7 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [] serializer_class = CourseSerializer - def get_queryset(self): + def get_queryset(self): # noqa: D102 queryset = ( Course.objects.filter(live=True) .select_related("coursepage", "externalcoursepage") @@ -112,7 +111,7 @@ class UserEnrollmentsView(APIView): authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) - def get(self, request, *args, **kwargs): # pylint: disable=unused-argument + def get(self, request, *args, **kwargs): # noqa: ARG002 """Read-only access""" user = request.user user_enrollments = get_user_enrollments(user) @@ -142,7 +141,7 @@ def _serialize_course_enrollments(self, enrollments): return CourseRunEnrollmentSerializer(enrollments, many=True).data def _serialize_program_enrollments(self, programs, program_runs): - """helper method to serialize program enrollments""" + """Helper method to serialize program enrollments""" return ProgramEnrollmentSerializer( programs, many=True, context={"course_run_enrollments": list(program_runs)} diff --git a/courses/views_test.py b/courses/views_test.py index fe8402144..47bc27e8c 100644 --- a/courses/views_test.py +++ b/courses/views_test.py @@ -1,7 +1,6 @@ """ Tests for course views """ -# pylint: disable=unused-argument, redefined-outer-name import operator as op from datetime import timedelta @@ -39,11 +38,10 @@ from mitxpro.test_utils import assert_drf_json_equal from mitxpro.utils import now_in_utc - pytestmark = [pytest.mark.django_db] -@pytest.fixture() +@pytest.fixture def programs(): """Fixture for a set of Programs in the database""" programs = ProgramFactory.create_batch(3) @@ -52,13 +50,13 @@ def programs(): return programs -@pytest.fixture() +@pytest.fixture def courses(): """Fixture for a set of Courses in the database""" return CourseFactory.create_batch(3) -@pytest.fixture() +@pytest.fixture def course_runs(): """Fixture for a set of CourseRuns in the database""" return CourseRunFactory.create_batch(3) @@ -229,12 +227,11 @@ def test_delete_course_run(user_drf_client, course_runs): assert resp.status_code == status.HTTP_405_METHOD_NOT_ALLOWED -# pylint: disable=too-many-arguments @pytest.mark.parametrize("is_enrolled", [True, False]) @pytest.mark.parametrize("has_unexpired_run", [True, False]) @pytest.mark.parametrize("has_product", [True, False]) @pytest.mark.parametrize("is_anonymous", [True, False]) -def test_course_view( +def test_course_view( # noqa: PLR0913 client, user, home_page, is_enrolled, has_unexpired_run, has_product, is_anonymous ): """ @@ -285,11 +282,11 @@ def test_course_view( class_name = "enrolled" assert ( - f'
'.encode("utf-8") + f''.encode("utf-8") # noqa: UP012 in resp.content ) is has_button assert ( - "Please Sign In to MITx PRO to enroll in a course".encode("utf-8") + "Please Sign In to MITx PRO to enroll in a course".encode("utf-8") # noqa: UP012 in resp.content ) is (is_anonymous and has_product and has_unexpired_run) @@ -298,7 +295,7 @@ def test_course_view( @pytest.mark.parametrize("has_product", [True, False]) @pytest.mark.parametrize("has_unexpired_run", [True, False]) @pytest.mark.parametrize("is_anonymous", [True, False]) -def test_program_view( +def test_program_view( # noqa: PLR0913 client, user, home_page, is_enrolled, has_product, has_unexpired_run, is_anonymous ): """ @@ -351,11 +348,11 @@ def test_program_view( class_name = "enrolled" assert ( - f''.encode("utf-8") + f''.encode("utf-8") # noqa: UP012 in resp.content ) is has_button assert ( - "Please Sign In to MITx PRO to enroll in a course".encode("utf-8") + "Please Sign In to MITx PRO to enroll in a course".encode("utf-8") # noqa: UP012 in resp.content ) is (is_anonymous and has_product and has_unexpired_run) @@ -487,7 +484,7 @@ def test_course_runs_without_product_in_programs_api(client, has_product): @pytest.mark.parametrize( - "factory, serializer_cls, api_name", + "factory, serializer_cls, api_name", # noqa: PT006 [ ( CourseRunCertificateFactory, @@ -501,7 +498,7 @@ def test_course_runs_without_product_in_programs_api(client, has_product): ), ], ) -def test_course_run_certificate_api( +def test_course_run_certificate_api( # noqa: PLR0913 settings, user, user_drf_client, factory, serializer_cls, api_name ): """Verify that the certificates APIs function as expected""" @@ -514,14 +511,14 @@ def test_course_run_certificate_api( assert resp.json() == [serializer_cls(cert).data] resp = user_drf_client.get( - reverse(f"{api_name}-detail", kwargs=dict(uuid=cert.uuid)) + reverse(f"{api_name}-detail", kwargs=dict(uuid=cert.uuid)) # noqa: C408 ) assert resp.json() == serializer_cls(cert).data assert DigitalCredentialRequest.objects.count() == 0 resp = user_drf_client.post( - reverse(f"{api_name}-request_digital_credentials", kwargs=dict(uuid=cert.uuid)) + reverse(f"{api_name}-request_digital_credentials", kwargs=dict(uuid=cert.uuid)) # noqa: C408 ) assert DigitalCredentialRequest.objects.count() == 1 @@ -534,9 +531,7 @@ def test_course_run_certificate_api( assert resp.json() == DigitalCredentialRequestSerializer(dcr).data -def test_course_topics_api( - client, django_assert_num_queries -): # pylint:disable=too-many-locals +def test_course_topics_api(client, django_assert_num_queries): """ Test that course topics API returns the expected topics and correct course count. """ diff --git a/courses/wagtail_hooks.py b/courses/wagtail_hooks.py index c2839712a..8daa7d464 100644 --- a/courses/wagtail_hooks.py +++ b/courses/wagtail_hooks.py @@ -6,4 +6,7 @@ @hooks.register("register_admin_viewset") def register_viewset(): + """ + Register `CourseTopicViewSet` in wagtail + """ return CourseTopicViewSet("topics") diff --git a/courseware/__init__.py b/courseware/__init__.py index d835a5f92..e69de29bb 100644 --- a/courseware/__init__.py +++ b/courseware/__init__.py @@ -1 +0,0 @@ -# pylint: disable=missing-docstring,invalid-name diff --git a/courseware/api.py b/courseware/api.py index a289a2ba1..763c4f56f 100644 --- a/courseware/api.py +++ b/courseware/api.py @@ -42,26 +42,25 @@ now_in_utc, ) - log = logging.getLogger(__name__) User = get_user_model() OPENEDX_USER_ACCOUNT_DETAIL_PATH = "/api/user/v1/accounts" OPENEDX_REGISTER_USER_PATH = "/user_api/v1/account/registration/" -OPENEDX_REQUEST_DEFAULTS = dict(country="US", honor_code=True) +OPENEDX_REQUEST_DEFAULTS = dict(country="US", honor_code=True) # noqa: C408 OPENEDX_SOCIAL_LOGIN_XPRO_PATH = "/auth/login/mitxpro-oauth2/?auth_entry=login" OPENEDX_OAUTH2_AUTHORIZE_PATH = "/oauth2/authorize" -OPENEDX_OAUTH2_ACCESS_TOKEN_PATH = "/oauth2/access_token" +OPENEDX_OAUTH2_ACCESS_TOKEN_PATH = "/oauth2/access_token" # noqa: S105 OPENEDX_OAUTH2_SCOPES = ["read", "write"] -OPENEDX_OAUTH2_ACCESS_TOKEN_PARAM = "code" +OPENEDX_OAUTH2_ACCESS_TOKEN_PARAM = "code" # noqa: S105 OPENEDX_OAUTH2_ACCESS_TOKEN_EXPIRY_MARGIN_SECONDS = 10 OPENEDX_AUTH_DEFAULT_TTL_IN_SECONDS = 60 OPENEDX_AUTH_MAX_TTL_IN_SECONDS = 60 * 60 -ACCESS_TOKEN_HEADER_NAME = "X-Access-Token" -AUTH_TOKEN_HEADER_NAME = "Authorization" +ACCESS_TOKEN_HEADER_NAME = "X-Access-Token" # noqa: S105 +AUTH_TOKEN_HEADER_NAME = "Authorization" # noqa: S105 API_KEY_HEADER_NAME = "X-EdX-Api-Key" @@ -124,7 +123,7 @@ def get_existing_openedx_user(user): in the settings module. """ if settings.OPENEDX_SERVICE_WORKER_API_TOKEN is None: - raise ImproperlyConfigured("OPENEDX_SERVICE_WORKER_API_TOKEN is not set") + raise ImproperlyConfigured("OPENEDX_SERVICE_WORKER_API_TOKEN is not set") # noqa: EM101 req_session = requests.Session() req_session.headers.update( { @@ -208,14 +207,14 @@ def create_edx_user(user): openedx_user = get_existing_openedx_user(user) if not openedx_user: raise CoursewareUserCreateError( - f"Error creating Open edX user. {get_error_response_summary(resp)}" + f"Error creating Open edX user. {get_error_response_summary(resp)}" # noqa: EM102 ) - if not openedx_user.is_username_match(): + if not openedx_user.is_username_match(): # noqa: SIM102 if not update_xpro_user_username( user, openedx_user.openedx_data["username"] ): raise CoursewareUserCreateError( - f"Error creating Open edX user. {get_error_response_summary(resp)}" + f"Error creating Open edX user. {get_error_response_summary(resp)}" # noqa: EM102 ) @@ -269,7 +268,7 @@ def create_edx_auth_token(user): settings.SITE_BASE_URL, reverse("openedx-private-oauth-complete") ) url = edx_url(OPENEDX_OAUTH2_AUTHORIZE_PATH) - params = dict( + params = dict( # noqa: C408 client_id=settings.OPENEDX_API_CLIENT_ID, scope=" ".join(OPENEDX_OAUTH2_SCOPES), redirect_uri=redirect_uri, @@ -281,16 +280,16 @@ def create_edx_auth_token(user): # Step 5 if not resp.url.startswith(redirect_uri): raise OpenEdXOAuth2Error( - f"Redirected to '{resp.url}', expected: '{redirect_uri}'" + f"Redirected to '{resp.url}', expected: '{redirect_uri}'" # noqa: EM102 ) qs = parse_qs(urlparse(resp.url).query) if not qs.get(OPENEDX_OAUTH2_ACCESS_TOKEN_PARAM): - raise OpenEdXOAuth2Error("Did not receive access_token from Open edX") + raise OpenEdXOAuth2Error("Did not receive access_token from Open edX") # noqa: EM101 # Step 6 auth = _create_tokens_and_update_auth( auth, - dict( + dict( # noqa: C408 code=qs[OPENEDX_OAUTH2_ACCESS_TOKEN_PARAM], grant_type="authorization_code", client_id=settings.OPENEDX_API_CLIENT_ID, @@ -299,7 +298,7 @@ def create_edx_auth_token(user): ), ) - return auth + return auth # noqa: RET504 def update_edx_user_email(user): @@ -328,7 +327,7 @@ def update_edx_user_email(user): settings.SITE_BASE_URL, reverse("openedx-private-oauth-complete") ) url = edx_url(OPENEDX_OAUTH2_AUTHORIZE_PATH) - params = dict( + params = dict( # noqa: C408 client_id=settings.OPENEDX_API_CLIENT_ID, scope=" ".join(OPENEDX_OAUTH2_SCOPES), redirect_uri=redirect_uri, @@ -350,8 +349,8 @@ def _create_tokens_and_update_auth(auth, params): courseware.models.OpenEdxApiAuth: the updated auth records """ - resp = requests.post(edx_url(OPENEDX_OAUTH2_ACCESS_TOKEN_PATH), data=params) - if resp.status_code != 200: + resp = requests.post(edx_url(OPENEDX_OAUTH2_ACCESS_TOKEN_PATH), data=params) # noqa: S113 + if resp.status_code != 200: # noqa: PLR2004 # The auth is likely broken for reasons unknown, delete and return None log.info( "Auth token for user %s failed, creating a new one", auth.user.username @@ -417,14 +416,14 @@ def repair_faulty_courseware_users(): # edX is our only courseware for the time being. If a different courseware is added, this # function will need to be updated. created_user, created_auth_token = repair_faulty_edx_user(user) - except HTTPError as exc: + except HTTPError as exc: # noqa: PERF203 log.exception( "Failed to repair faulty user %s (%s). %s", user.username, user.email, get_error_response_summary(exc.response), ) - except Exception: # pylint: disable=broad-except + except Exception: log.exception( "Failed to repair faulty user %s (%s)", user.username, user.email ) @@ -447,7 +446,7 @@ def get_valid_edx_api_auth(user, ttl_in_seconds=OPENEDX_AUTH_DEFAULT_TTL_IN_SECO auth: updated OpenEdxApiAuth """ - assert ( + assert ( # noqa: S101 ttl_in_seconds < OPENEDX_AUTH_MAX_TTL_IN_SECONDS ), f"ttl_in_seconds must be less than {OPENEDX_AUTH_MAX_TTL_IN_SECONDS}" @@ -490,7 +489,7 @@ def _refresh_edx_api_auth(auth): # Note: this is subject to thundering herd problems, we should address this at some point return _create_tokens_and_update_auth( auth, - dict( + dict( # noqa: C408 refresh_token=auth.refresh_token, grant_type="refresh_token", client_id=settings.OPENEDX_API_CLIENT_ID, @@ -513,8 +512,8 @@ def get_edx_api_client(user, ttl_in_seconds=OPENEDX_AUTH_DEFAULT_TTL_IN_SECONDS) try: auth = get_valid_edx_api_auth(user, ttl_in_seconds=ttl_in_seconds) except OpenEdxApiAuth.DoesNotExist: - raise NoEdxApiAuthError( - "{} does not have an associated OpenEdxApiAuth".format(str(user)) + raise NoEdxApiAuthError( # noqa: B904, TRY200 + "{} does not have an associated OpenEdxApiAuth".format(str(user)) # noqa: EM103, UP032 ) return EdxApi( {"access_token": auth.access_token, "api_key": settings.OPENEDX_API_KEY}, @@ -531,9 +530,9 @@ def get_edx_api_service_client(): EdxApi: edx api service worker client instance """ if settings.OPENEDX_SERVICE_WORKER_API_TOKEN is None: - raise ImproperlyConfigured("OPENEDX_SERVICE_WORKER_API_TOKEN is not set") + raise ImproperlyConfigured("OPENEDX_SERVICE_WORKER_API_TOKEN is not set") # noqa: EM101 - edx_client = EdxApi( + return EdxApi( { "access_token": settings.OPENEDX_SERVICE_WORKER_API_TOKEN, "api_key": settings.OPENEDX_API_KEY, @@ -542,8 +541,6 @@ def get_edx_api_service_client(): timeout=settings.EDX_API_CLIENT_TIMEOUT, ) - return edx_client - def get_edx_api_course_detail_client(): """ @@ -592,7 +589,7 @@ def get_edx_grades_with_users(course_run, user=None): for edx_grade in all_grades: try: user = User.objects.get(username=edx_grade.username) - except User.DoesNotExist: + except User.DoesNotExist: # noqa: PERF203 log.warning("User with username %s not found", edx_grade.username) else: yield edx_grade, user @@ -616,7 +613,7 @@ def get_enrollment(user: User, course_run: CourseRun): ) -def enroll_in_edx_course_runs(user, course_runs, force_enrollment=True): +def enroll_in_edx_course_runs(user, course_runs, force_enrollment=True): # noqa: FBT002 """ Enrolls a user in edx course runs @@ -645,7 +642,7 @@ def enroll_in_edx_course_runs(user, course_runs, force_enrollment=True): force_enrollment=force_enrollment, ) results.append(result) - except HTTPError as exc: + except HTTPError as exc: # noqa: PERF203 # If there is an error message and it indicates that the preferred enrollment mode was the cause of the # error, log an error and try to enroll the user in 'audit' mode as a failover. if not is_json_response(exc.response): @@ -656,7 +653,7 @@ def enroll_in_edx_course_runs(user, course_runs, force_enrollment=True): ) if not is_enroll_mode_error: raise EdxApiEnrollErrorException(user, course_run, exc) from exc - log.error( + log.error( # noqa: TRY400 "Failed to enroll user in %s with '%s' mode. Attempting to enroll with '%s' mode instead. " "(%s)", course_run.courseware_id, @@ -675,12 +672,12 @@ def enroll_in_edx_course_runs(user, course_runs, force_enrollment=True): raise EdxApiEnrollErrorException( user, course_run, inner_exc ) from inner_exc - except Exception as inner_exc: # pylint: disable=broad-except + except Exception as inner_exc: # noqa: BLE001 raise UnknownEdxApiEnrollException( user, course_run, inner_exc ) from inner_exc results.append(result) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # noqa: BLE001 raise UnknownEdxApiEnrollException(user, course_run, exc) from exc return results @@ -722,10 +719,10 @@ def retry_failed_edx_enrollments(): edx_enrollment.mode, ) else: - log.exception(str(exc)) + log.exception(str(exc)) # noqa: TRY401 continue - except Exception as exc: # pylint: disable=broad-except - log.exception(str(exc)) + except Exception as exc: + log.exception(str(exc)) # noqa: TRY401 continue enrollment.edx_enrolled = True enrollment.save_and_log(None) @@ -755,9 +752,9 @@ def unenroll_edx_course_run(run_enrollment): run_enrollment.run.courseware_id ) except HTTPError as exc: - raise EdxApiEnrollErrorException(run_enrollment.user, run_enrollment.run, exc) - except Exception as exc: # pylint: disable=broad-except - raise UnknownEdxApiEnrollException(run_enrollment.user, run_enrollment.run, exc) + raise EdxApiEnrollErrorException(run_enrollment.user, run_enrollment.run, exc) # noqa: B904, TRY200 + except Exception as exc: # noqa: BLE001 + raise UnknownEdxApiEnrollException(run_enrollment.user, run_enrollment.run, exc) # noqa: B904, TRY200 else: return deactivated_enrollment @@ -779,9 +776,10 @@ def update_edx_user_name(user): edx_client = get_edx_api_client(user) try: return edx_client.user_info.update_user_name(user.username, user.name) - except Exception as exc: - raise UserNameUpdateFailedException( - "Error updating user's full name in edX.", exc + except Exception as exc: # noqa: BLE001 + raise UserNameUpdateFailedException( # noqa: B904, TRY200 + "Error updating user's full name in edX.", # noqa: EM101 + exc, ) @@ -805,10 +803,10 @@ def create_oauth_application(): return Application.objects.get_or_create( name=settings.OPENEDX_OAUTH_APP_NAME, - defaults=dict( + defaults=dict( # noqa: C408 redirect_uris=urljoin( settings.OPENEDX_BASE_REDIRECT_URL, - "/auth/complete/{}/".format(settings.MITXPRO_OAUTH_PROVIDER), + "/auth/complete/{}/".format(settings.MITXPRO_OAUTH_PROVIDER), # noqa: UP032 ), client_type="confidential", authorization_grant_type="authorization-code", diff --git a/courseware/api_test.py b/courseware/api_test.py index 4cd5016c8..1681690fe 100644 --- a/courseware/api_test.py +++ b/courseware/api_test.py @@ -1,5 +1,4 @@ """Courseware API tests""" -# pylint: disable=redefined-outer-name import itertools from datetime import timedelta from types import SimpleNamespace @@ -24,8 +23,8 @@ OpenEdxUser, create_edx_auth_token, create_edx_user, - create_user, create_oauth_application, + create_user, enroll_in_edx_course_runs, get_edx_api_client, get_valid_edx_api_auth, @@ -56,18 +55,17 @@ from mitxpro.utils import now_in_utc from users.factories import UserFactory - User = get_user_model() pytestmark = [pytest.mark.django_db] -@pytest.fixture() +@pytest.fixture def application(settings): """Test data and settings needed for create_edx_user tests""" settings.OPENEDX_OAUTH_APP_NAME = "test_app_name" settings.OPENEDX_API_BASE_URL = "http://example.com" settings.MITXPRO_OAUTH_PROVIDER = "test_provider" - settings.MITXPRO_REGISTRATION_ACCESS_TOKEN = "access_token" + settings.MITXPRO_REGISTRATION_ACCESS_TOKEN = "access_token" # noqa: S105 return Application.objects.create( name=settings.OPENEDX_OAUTH_APP_NAME, user=None, @@ -77,16 +75,16 @@ def application(settings): ) -@pytest.fixture() +@pytest.fixture def update_token_response(settings): - """mock response for updating an auth token""" - refresh_token = "abc123" - access_token = "def456" + """Mock response for updating an auth token""" + refresh_token = "abc123" # noqa: S105 + access_token = "def456" # noqa: S105 responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/oauth2/access_token", - json=dict( + json=dict( # noqa: C408 refresh_token=refresh_token, access_token=access_token, expires_in=3600 ), status=status.HTTP_200_OK, @@ -94,16 +92,16 @@ def update_token_response(settings): return SimpleNamespace(refresh_token=refresh_token, access_token=access_token) -@pytest.fixture() +@pytest.fixture def update_token_response_error(settings): - """mock response for updating an auth token""" - refresh_token = "abc123" - access_token = "def456" + """Mock response for updating an auth token""" + refresh_token = "abc123" # noqa: S105 + access_token = "def456" # noqa: S105 responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/oauth2/access_token", - json=dict( + json=dict( # noqa: C408 refresh_token=refresh_token, access_token=access_token, expires_in=3600 ), status=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -111,11 +109,11 @@ def update_token_response_error(settings): return SimpleNamespace(refresh_token=refresh_token, access_token=access_token) -@pytest.fixture() +@pytest.fixture def create_token_responses(settings): """Mock responses for creating an auth token""" - refresh_token = "abc123" - access_token = "def456" + refresh_token = "abc123" # noqa: S105 + access_token = "def456" # noqa: S105 code = "ghi789" responses.add( responses.GET, @@ -138,7 +136,7 @@ def create_token_responses(settings): responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/oauth2/access_token", - json=dict( + json=dict( # noqa: C408 refresh_token=refresh_token, access_token=access_token, expires_in=3600 ), status=status.HTTP_200_OK, @@ -164,7 +162,7 @@ def test_create_edx_user(user, settings, application, access_token_count): responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/user_api/v1/account/registration/", - json=dict(success=True), + json=dict(success=True), # noqa: C408 status=status.HTTP_200_OK, ) @@ -208,7 +206,7 @@ def test_create_edx_user_conflict(settings, user): responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/user_api/v1/account/registration/", - json=dict(username="exists"), + json=dict(username="exists"), # noqa: C408 status=status.HTTP_409_CONFLICT, ) responses.add( @@ -230,7 +228,7 @@ def test_create_edx_auth_token(settings, user, create_token_responses): create_edx_auth_token(user) assert len(responses.calls) == 4 - assert dict(parse_qsl(responses.calls[3].request.body)) == dict( + assert dict(parse_qsl(responses.calls[3].request.body)) == dict( # noqa: C408 code=create_token_responses.code, grant_type="authorization_code", client_id=settings.OPENEDX_API_CLIENT_ID, @@ -279,7 +277,7 @@ def test_update_edx_user_email(settings, user): responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/user_api/v1/account/registration/", - json=dict(success=True), + json=dict(success=True), # noqa: C408 status=status.HTTP_200_OK, ) @@ -342,7 +340,7 @@ def test_get_valid_edx_api_auth_expired(settings, update_token_response): assert updated_auth is not None assert len(responses.calls) == 1 - assert dict(parse_qsl(responses.calls[0].request.body)) == dict( + assert dict(parse_qsl(responses.calls[0].request.body)) == dict( # noqa: C408 refresh_token=auth.refresh_token, grant_type="refresh_token", client_id=settings.OPENEDX_API_CLIENT_ID, @@ -361,7 +359,7 @@ def test_get_valid_edx_api_auth_expired(settings, update_token_response): @freeze_time("2019-03-24 11:50:36") def test_get_valid_edx_api_auth_expired_and_broken( settings, - update_token_response_error, # pylint:disable=unused-argument + update_token_response_error, create_token_responses, ): """Tests get_valid_edx_api_auth fetches and creates new auth credentials if expired/broken""" @@ -371,7 +369,7 @@ def test_get_valid_edx_api_auth_expired_and_broken( assert updated_auth is not None assert len(responses.calls) == 5 - assert dict(parse_qsl(responses.calls[0].request.body)) == dict( + assert dict(parse_qsl(responses.calls[0].request.body)) == dict( # noqa: C408 refresh_token=auth.refresh_token, grant_type="refresh_token", client_id=settings.OPENEDX_API_CLIENT_ID, @@ -507,7 +505,7 @@ def test_enroll_pro_unknown_fail(mocker, user, settings): ) mocker.patch("courseware.api.get_edx_api_client", return_value=mock_client) course_run = CourseRunFactory.build() - settings.OPENEDX_SERVICE_WORKER_API_TOKEN = "mock_api_token" + settings.OPENEDX_SERVICE_WORKER_API_TOKEN = "mock_api_token" # noqa: S105 with pytest.raises(UnknownEdxApiEnrollException): enroll_in_edx_course_runs(user, [course_run]) @@ -552,7 +550,8 @@ def test_retry_failed_edx_enrollments(mocker, exception_raised): "mode", [EDX_ENROLLMENT_PRO_MODE, EDX_ENROLLMENT_AUDIT_MODE, "other"] ) @pytest.mark.parametrize( - "edx_enrollment_exists, is_active", [[False, False], [True, True], [True, False]] + "edx_enrollment_exists, is_active", # noqa: PT006 + [[False, False], [True, True], [True, False]], # noqa: PT007 ) def test_retry_failed_edx_enrollments_exists( mocker, edx_enrollment_exists, is_active, mode @@ -627,7 +626,8 @@ def test_retry_failed_enroll_grace_period(mocker): @pytest.mark.parametrize( - "no_courseware_user,no_edx_auth", itertools.product([True, False], [True, False]) + "no_courseware_user,no_edx_auth", # noqa: PT006 + itertools.product([True, False], [True, False]), ) def test_repair_faulty_edx_user(mocker, user, no_courseware_user, no_edx_auth): """ @@ -727,11 +727,11 @@ def test_unenroll_edx_course_run(mocker): @pytest.mark.parametrize( - "client_exception_raised,expected_exception", + "client_exception_raised,expected_exception", # noqa: PT006 [ - [MockHttpError, EdxApiEnrollErrorException], - [ValueError, UnknownEdxApiEnrollException], - [Exception, UnknownEdxApiEnrollException], + [MockHttpError, EdxApiEnrollErrorException], # noqa: PT007 + [ValueError, UnknownEdxApiEnrollException], # noqa: PT007 + [Exception, UnknownEdxApiEnrollException], # noqa: PT007 ], ) def test_unenroll_edx_course_run_failure( @@ -767,11 +767,11 @@ def test_update_user_edx_name(mocker, user): @pytest.mark.parametrize( - "client_exception_raised,expected_exception", + "client_exception_raised,expected_exception", # noqa: PT006 [ - [MockHttpError, UserNameUpdateFailedException], - [ValueError, UserNameUpdateFailedException], - [Exception, UserNameUpdateFailedException], + [MockHttpError, UserNameUpdateFailedException], # noqa: PT007 + [ValueError, UserNameUpdateFailedException], # noqa: PT007 + [Exception, UserNameUpdateFailedException], # noqa: PT007 ], ) def test_update_edx_user_name_failure( diff --git a/courseware/constants.py b/courseware/constants.py index 850a1b671..fe627a202 100644 --- a/courseware/constants.py +++ b/courseware/constants.py @@ -8,10 +8,8 @@ EDX_ENROLLMENT_PRO_MODE = "no-id-professional" EDX_ENROLLMENT_AUDIT_MODE = "audit" PRO_ENROLL_MODE_ERROR_TEXTS = ( - "The [{}] course mode is expired or otherwise unavailable for course run".format( - EDX_ENROLLMENT_PRO_MODE - ), - "Specified course mode '{}' unavailable for course".format(EDX_ENROLLMENT_PRO_MODE), + f"The [{EDX_ENROLLMENT_PRO_MODE}] course mode is expired or otherwise unavailable for course run", + f"Specified course mode '{EDX_ENROLLMENT_PRO_MODE}' unavailable for course", ) # The amount of minutes after creation that a courseware model record should be eligible for repair COURSEWARE_REPAIR_GRACE_PERIOD_MINS = 5 diff --git a/courseware/exceptions.py b/courseware/exceptions.py index 66c59d51e..61829aaa8 100644 --- a/courseware/exceptions.py +++ b/courseware/exceptions.py @@ -18,7 +18,7 @@ class NoEdxApiAuthError(Exception): """The user was expected to have an OpenEdxApiAuth object but does not""" -class EdxApiEnrollErrorException(Exception): +class EdxApiEnrollErrorException(Exception): # noqa: N818 """An edX enrollment API call resulted in an error response""" def __init__(self, user, course_run, http_error, msg=None): @@ -45,7 +45,7 @@ def __init__(self, user, course_run, http_error, msg=None): super().__init__(msg) -class UnknownEdxApiEnrollException(Exception): +class UnknownEdxApiEnrollException(Exception): # noqa: N818 """An edX enrollment API call failed for an unknown reason""" def __init__(self, user, course_run, base_exc, msg=None): @@ -71,5 +71,5 @@ def __init__(self, user, course_run, base_exc, msg=None): super().__init__(msg) -class UserNameUpdateFailedException(Exception): +class UserNameUpdateFailedException(Exception): # noqa: N818 """Raised if a user's profile name(Full Name) update call is failed""" diff --git a/courseware/factories.py b/courseware/factories.py index 5c52f5b58..d63ead088 100644 --- a/courseware/factories.py +++ b/courseware/factories.py @@ -1,11 +1,11 @@ """Courseware factories""" from datetime import timedelta, timezone -from factory import Faker, SubFactory, Trait, LazyAttribute +from factory import Faker, LazyAttribute, SubFactory, Trait from factory.django import DjangoModelFactory -from courseware.models import OpenEdxApiAuth, CoursewareUser from courseware.constants import PLATFORM_EDX +from courseware.models import CoursewareUser, OpenEdxApiAuth from mitxpro.utils import now_in_utc diff --git a/courseware/management/commands/repair_missing_courseware_records.py b/courseware/management/commands/repair_missing_courseware_records.py index 71ce96df9..3bfe2b2ac 100644 --- a/courseware/management/commands/repair_missing_courseware_records.py +++ b/courseware/management/commands/repair_missing_courseware_records.py @@ -1,8 +1,8 @@ """ Management command to repair missing courseware records """ -from django.core.management import BaseCommand from django.contrib.auth import get_user_model +from django.core.management import BaseCommand from requests.exceptions import HTTPError from courseware.api import repair_faulty_edx_user @@ -18,7 +18,7 @@ class Command(BaseCommand): help = "Repairs missing courseware records" - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Walk all users who are missing records and repair them""" users_to_repair = User.faulty_courseware_users @@ -39,10 +39,10 @@ def handle(self, *args, **options): ) ) error_count += 1 - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # noqa: BLE001 self.stderr.write( self.style.ERROR( - f"{user.username} ({user.email}): Failed to repair (Exception: {str(exc)})" + f"{user.username} ({user.email}): Failed to repair (Exception: {str(exc)})" # noqa: RUF010 ) ) error_count += 1 diff --git a/courseware/management/commands/retry_edx_enrollment.py b/courseware/management/commands/retry_edx_enrollment.py index 319b6714a..a53ce9a4a 100644 --- a/courseware/management/commands/retry_edx_enrollment.py +++ b/courseware/management/commands/retry_edx_enrollment.py @@ -1,12 +1,12 @@ """ Management command to retry edX enrollment for a user's course run enrollments """ -from django.core.management import BaseCommand from django.contrib.auth import get_user_model +from django.core.management import BaseCommand -from users.api import fetch_users -from courseware.api import enroll_in_edx_course_runs from courses.models import CourseRunEnrollment +from courseware.api import enroll_in_edx_course_runs +from users.api import fetch_users User = get_user_model() @@ -41,7 +41,7 @@ def add_arguments(self, parser): ), ) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Run the command""" enrollment_filter = {} if not options["force"]: @@ -67,7 +67,7 @@ def handle(self, *args, **options): course_run = enrollment.run try: enroll_in_edx_course_runs(user, [course_run]) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # noqa: BLE001 self.stderr.write(self.style.ERROR(str(exc))) else: enrollment.edx_enrolled = True diff --git a/courseware/migrations/0002_add_courseware_user.py b/courseware/migrations/0002_add_courseware_user.py index 7dc2ff0e6..52854ea4e 100644 --- a/courseware/migrations/0002_add_courseware_user.py +++ b/courseware/migrations/0002_add_courseware_user.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-03-22 20:23 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/courseware/migrations/0003_add_edx_auth.py b/courseware/migrations/0003_add_edx_auth.py index 9a9afe7b1..102c0bec6 100644 --- a/courseware/migrations/0003_add_edx_auth.py +++ b/courseware/migrations/0003_add_edx_auth.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-03-25 12:27 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("courseware", "0002_add_courseware_user"), diff --git a/courseware/migrations/0004_add_courseware_related_names.py b/courseware/migrations/0004_add_courseware_related_names.py index 94fa5e449..05678712b 100644 --- a/courseware/migrations/0004_add_courseware_related_names.py +++ b/courseware/migrations/0004_add_courseware_related_names.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-06-20 19:58 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("courseware", "0003_add_edx_auth")] operations = [ diff --git a/courseware/models.py b/courseware/models.py index a0593bcd0..450028d54 100644 --- a/courseware/models.py +++ b/courseware/models.py @@ -2,9 +2,8 @@ from django.conf import settings from django.db import models -from mitxpro.models import TimestampedModel - from courseware.constants import COURSEWARE_PLATFORM_CHOICES, PLATFORM_EDX +from mitxpro.models import TimestampedModel class CoursewareUser(TimestampedModel): @@ -40,7 +39,7 @@ class OpenEdxApiAuth(TimestampedModel): ) refresh_token = models.CharField(max_length=128) - access_token = models.CharField(null=True, max_length=128) + access_token = models.CharField(null=True, max_length=128) # noqa: DJ001 access_token_expires_on = models.DateTimeField(null=True) def __str__(self): diff --git a/courseware/tasks.py b/courseware/tasks.py index f4655687f..ffea4dac6 100644 --- a/courseware/tasks.py +++ b/courseware/tasks.py @@ -1,6 +1,6 @@ """Courseware tasks""" -from mitxpro.celery import app from courseware import api +from mitxpro.celery import app from users.api import get_user_by_id from users.models import User diff --git a/courseware/tasks_test.py b/courseware/tasks_test.py index db4126c79..b09ae5d71 100644 --- a/courseware/tasks_test.py +++ b/courseware/tasks_test.py @@ -1,8 +1,8 @@ """Courseware tasks""" import pytest -from users.factories import UserFactory from courseware import tasks +from users.factories import UserFactory @pytest.mark.django_db diff --git a/courseware/views.py b/courseware/views.py index 56aaec986..b57d40ba0 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -3,7 +3,7 @@ from rest_framework import status -def openedx_private_auth_complete(request): +def openedx_private_auth_complete(request): # noqa: ARG001 """Responds with a simple HTTP_200_OK""" # NOTE: this is only meant as a landing endpoint for api.create_edx_auth_token() flow return HttpResponse(status=status.HTTP_200_OK) diff --git a/ecommerce/__init__.py b/ecommerce/__init__.py index d835a5f92..e69de29bb 100644 --- a/ecommerce/__init__.py +++ b/ecommerce/__init__.py @@ -1 +0,0 @@ -# pylint: disable=missing-docstring,invalid-name diff --git a/ecommerce/admin.py b/ecommerce/admin.py index dc5b57150..c7bcfa1d5 100644 --- a/ecommerce/admin.py +++ b/ecommerce/admin.py @@ -39,7 +39,7 @@ class ProductContentTypeListFilter(admin.SimpleListFilter): title = "content type" parameter_name = "content_type" - def lookups(self, request, model_admin): + def lookups(self, request, model_admin): # noqa: ARG002 """ Returns a list of tuples. The first element in each tuple is the coded value for the option that will appear in the URL query. The second element is the human-readable name for the option that will appear @@ -50,7 +50,7 @@ def lookups(self, request, model_admin): ).values_list("model", flat=True) return zip(valid_content_types, valid_content_types) - def queryset(self, request, queryset): + def queryset(self, request, queryset): # noqa: ARG002 """ Returns the filtered queryset based on the value provided in the query string and retrievable via `self.value()`. @@ -69,10 +69,10 @@ class LineAdmin(admin.ModelAdmin): readonly_fields = get_field_names(Line) - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @admin.display( @@ -92,10 +92,10 @@ class LineRunSelectionAdmin(admin.ModelAdmin): list_display = ("id", "line", "get_order", "get_run_courseware_id") readonly_fields = get_field_names(LineRunSelection) - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @admin.display( @@ -124,10 +124,10 @@ class ProgramRunLineAdmin(admin.ModelAdmin): readonly_fields = get_field_names(ProgramRunLine) - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @admin.display( @@ -151,10 +151,10 @@ class OrderAdmin(AuditableModelAdmin, TimestampedModelAdmin): readonly_fields = [name for name in get_field_names(Order) if name != "status"] - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False def save_model(self, request, obj, form, change): @@ -182,10 +182,10 @@ def get_order_user(self, obj): """Returns the related Order's user email""" return obj.order.purchaser.email - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @@ -207,10 +207,10 @@ def get_order_user(self, obj): """Returns the related Order's user email""" return obj.order.purchaser.email if obj.order is not None else None - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @@ -297,7 +297,7 @@ class CouponPaymentVersionAdmin(admin.ModelAdmin): ) raw_id_fields = ("payment",) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @admin.display( @@ -328,7 +328,7 @@ class CouponVersionAdmin(admin.ModelAdmin): list_display = ("id", "get_coupon_code", "get_payment_name") raw_id_fields = ("coupon", "payment_version") - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @admin.display( @@ -418,7 +418,7 @@ class CouponRedemptionAdmin(admin.ModelAdmin): ) raw_id_fields = ("coupon_version", "order") - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002 """Return all active and in_active products""" return self.model.objects.get_queryset().select_related( "coupon_version__coupon" @@ -461,11 +461,11 @@ class ProductVersionAdmin(admin.ModelAdmin): "product__programs__readable_id", ) - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002 """Return all active and in_active products""" return self.model.objects.get_queryset().select_related("product") - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False class Media: @@ -502,10 +502,10 @@ class ProductAdmin(admin.ModelAdmin): @admin.display(description="Text ID") def get_text_id(self, obj): """Return the text id""" - if obj.latest_version: + if obj.latest_version: # noqa: RET503 return obj.latest_version.text_id - def get_queryset(self, request): + def get_queryset(self, request): # noqa: ARG002 """Return all active and in_active products""" return Product.all_objects @@ -527,9 +527,9 @@ class DataConsentAgreementForm(forms.ModelForm): class Meta: model = DataConsentAgreement - fields = "__all__" + fields = "__all__" # noqa: DJ007 - def clean(self): + def clean(self): # noqa: D102 is_global = self.cleaned_data.get("is_global", False) courses = self.cleaned_data.get("courses", Course.objects.none()) company = self.cleaned_data.get("company", None) @@ -542,12 +542,12 @@ def clean(self): .exists() ): raise ValidationError( - "You already have a global consent agreement for this company" + "You already have a global consent agreement for this company" # noqa: EM101 ) # Check that is_global flag is enabled or at least one course is associated with the agreement if not is_global and not courses.all(): raise ValidationError( - "You must either check All Courses box or select courses for the agreement" + "You must either check All Courses box or select courses for the agreement" # noqa: EM101 ) # If is_global flag is true, we will just clean the associated course list if is_global: diff --git a/ecommerce/admin_test.py b/ecommerce/admin_test.py index 13d40804b..d7bb6bf88 100644 --- a/ecommerce/admin_test.py +++ b/ecommerce/admin_test.py @@ -2,8 +2,8 @@ import pytest from courses.factories import CourseFactory -from ecommerce.admin import OrderAdmin, DataConsentAgreementForm -from ecommerce.factories import OrderFactory, DataConsentAgreementFactory +from ecommerce.admin import DataConsentAgreementForm, OrderAdmin +from ecommerce.factories import DataConsentAgreementFactory, OrderFactory from ecommerce.models import OrderAudit from users.factories import UserFactory diff --git a/ecommerce/api.py b/ecommerce/api.py index 5ac043dce..776634585 100644 --- a/ecommerce/api.py +++ b/ecommerce/api.py @@ -10,7 +10,7 @@ from base64 import b64encode from collections import defaultdict from datetime import timedelta -from typing import Iterable, NamedTuple, Optional +from typing import Iterable, NamedTuple, Optional # noqa: UP035 from urllib.parse import quote_plus, urljoin from django.conf import settings @@ -70,7 +70,6 @@ from maxmind.api import ip_to_country_code from mitxpro.utils import case_insensitive_equal, first_or_none, now_in_utc - log = logging.getLogger(__name__) ISO_8601_FORMAT = "%Y-%m-%dT%H:%M:%SZ" @@ -113,7 +112,7 @@ def determine_visitor_country(request: HttpRequest or None) -> str or None: if TaxRate.objects.filter(active=True, country_code=ip_country_code).exists(): return ip_country_code - return profile_country_code + return profile_country_code # noqa: TRY300 except TypeError: return profile_country_code @@ -150,14 +149,13 @@ def calculate_tax( decimal.Decimal(item_price) * (tax_rate.tax_rate / 100) ) - return (tax_rate.tax_rate, resolved_country_code, tax_inclusive_amt) + return (tax_rate.tax_rate, resolved_country_code, tax_inclusive_amt) # noqa: TRY300 except TaxRate.DoesNotExist: pass return (0, "", item_price) -# pylint: disable=too-many-lines def generate_cybersource_sa_signature(payload): """ Generate an HMAC SHA256 signature for the CyberSource Secure Acceptance payload @@ -210,7 +208,7 @@ def get_readable_id(run_or_program): elif isinstance(run_or_program, Program): return run_or_program.readable_id else: - raise Exception(f"Unexpected object {run_or_program}") + raise Exception(f"Unexpected object {run_or_program}") # noqa: EM102, TRY002, TRY004 def sign_cybersource_payload(payload): @@ -224,12 +222,11 @@ def sign_cybersource_payload(payload): dict: A signed payload to be sent to CyberSource """ - field_names = sorted(list(payload.keys()) + ["signed_field_names"]) + field_names = sorted(list(payload.keys()) + ["signed_field_names"]) # noqa: RUF005 payload = {**payload, "signed_field_names": ",".join(field_names)} return {**payload, "signature": generate_cybersource_sa_signature(payload)} -# pylint: disable=too-many-locals def _generate_cybersource_sa_payload(*, order, receipt_url, cancel_url, ip_address): """ Generates a payload dict to send to CyberSource for Secure Acceptance @@ -365,9 +362,14 @@ def latest_coupon_version(coupon): return coupon.versions.order_by("-created_on").first() -def get_valid_coupon_versions( - product, user, auto_only=False, code=None, full_discount=False, company=None -): # pylint:disable=too-many-arguments +def get_valid_coupon_versions( # noqa: PLR0913 + product, + user, + auto_only=False, # noqa: FBT002 + code=None, + full_discount=False, # noqa: FBT002 + company=None, +): """ Given a list of coupon ids, determine which of them are valid based on payment version dates and redemptions. @@ -525,7 +527,7 @@ def get_valid_coupon_versions( return query.order_by("-payment_version__amount") -def best_coupon_for_product(product, user, auto_only=False, code=None): +def best_coupon_for_product(product, user, auto_only=False, code=None): # noqa: FBT002 """ Get the best eligible coupon for a product and user. @@ -788,7 +790,7 @@ def enroll_user_in_order_items(order): ): voucher_target = voucher.product.content_object voucher_enrollment = first_or_none( - ( + ( # noqa: UP034 enrollment for enrollment in successful_run_enrollments if enrollment.run == voucher_target @@ -907,7 +909,7 @@ def get_product_courses(product): list of Course: list of Courses associated with the Product """ - if product.content_type.model == CONTENT_TYPE_MODEL_COURSERUN: + if product.content_type.model == CONTENT_TYPE_MODEL_COURSERUN: # noqa: RET503 return [product.content_object.course] elif product.content_type.model == CONTENT_TYPE_MODEL_COURSE: return [product.content_object] @@ -951,9 +953,9 @@ class ValidatedBasket(NamedTuple): basket: Basket basket_item: BasketItem product_version: ProductVersion - coupon_version: Optional[CouponVersion] - run_selection_ids: Optional[Iterable[int]] - data_consent_users: Optional[Iterable[DataConsentUser]] + coupon_version: Optional[CouponVersion] # noqa: FA100 + run_selection_ids: Optional[Iterable[int]] # noqa: FA100 + data_consent_users: Optional[Iterable[DataConsentUser]] # noqa: FA100 def _validate_basket_contents(basket): @@ -1086,7 +1088,6 @@ def _validate_coupon_selection(basket, product): return coupon_version -# pylint: disable=too-many-branches def validate_basket_for_checkout(user): """ Validate basket for checkout @@ -1239,7 +1240,7 @@ def get_or_create_data_consent_users(basket): return data_consents -def create_coupons( +def create_coupons( # noqa: PLR0913 *, name, product_ids, @@ -1291,10 +1292,7 @@ def create_coupons( A CouponPaymentVersion. Other instances will be created at the same time and linked via foreign keys. """ - if company_id: - company = Company.objects.get(id=company_id) - else: - company = None + company = Company.objects.get(id=company_id) if company_id else None payment = CouponPayment.objects.create(name=name) payment_version = CouponPaymentVersion.objects.create( payment=payment, @@ -1328,7 +1326,7 @@ def create_coupons( coupon_objs = Coupon.objects.bulk_create(coupons) except IntegrityError: log.warning( - "Falling back to create Coupons for coupon payment {} and company {}".format( + "Falling back to create Coupons for coupon payment {} and company {}".format( # noqa: G001, UP032 name, company_id ) ) @@ -1379,7 +1377,7 @@ def determine_order_status_change(order, decision): return None if order.status != Order.CREATED: - raise EcommerceException(f"{order} is expected to have status 'created'") + raise EcommerceException(f"{order} is expected to have status 'created'") # noqa: EM102 if decision != CYBERSOURCE_DECISION_ACCEPT: log.warning( @@ -1472,22 +1470,22 @@ def get_product_from_text_id(text_id): ) if not program: raise Program.DoesNotExist( - f"Could not find Program with readable_id={text_id} " + f"Could not find Program with readable_id={text_id} " # noqa: EM102 "or readable_id={potential_text_id_base} with program run {potential_prog_run_id}" ) program_run = first_or_none(program.matching_program_runs) product = first_or_none(program.products.all()) if not product: - raise Product.DoesNotExist(f"Product for {program} does not exist") + raise Product.DoesNotExist(f"Product for {program} does not exist") # noqa: EM102 return product, program, program_run # This is a "normal" text id that should match a CourseRun/Program else: if is_program_text_id(text_id): content_object_model = Program - content_object_filter = dict(readable_id=text_id) + content_object_filter = dict(readable_id=text_id) # noqa: C408 else: content_object_model = CourseRun - content_object_filter = dict(courseware_id=text_id) + content_object_filter = dict(courseware_id=text_id) # noqa: C408 content_object = ( content_object_model.objects.filter(**content_object_filter) .prefetch_related("products") @@ -1495,11 +1493,11 @@ def get_product_from_text_id(text_id): ) if not content_object: raise content_object_model.DoesNotExist( - f"{content_object_model._meta.model} matching filter {content_object_filter} does not exist" + f"{content_object_model._meta.model} matching filter {content_object_filter} does not exist" # noqa: EM102, SLF001 ) product = first_or_none(content_object.products.all()) if not product: - raise Product.DoesNotExist(f"Product for {content_object} does not exist") + raise Product.DoesNotExist(f"Product for {content_object} does not exist") # noqa: EM102 return product, content_object, None diff --git a/ecommerce/api_test.py b/ecommerce/api_test.py index 365ca1988..6025a0b5d 100644 --- a/ecommerce/api_test.py +++ b/ecommerce/api_test.py @@ -102,11 +102,9 @@ from voucher.factories import VoucherFactory from voucher.models import Voucher - FAKE = faker.Factory.create() pytestmark = pytest.mark.django_db lazy = pytest.lazy_fixture -# pylint: disable=redefined-outer-name,too-many-lines,unused-argument,too-many-arguments CYBERSOURCE_ACCESS_KEY = "access" CYBERSOURCE_PROFILE_ID = "profile" @@ -114,7 +112,7 @@ @pytest.fixture(autouse=True) -def cybersource_settings(settings): +def cybersource_settings(settings): # noqa: PT004 """ Set cybersource settings """ @@ -156,7 +154,6 @@ def test_get_readable_id(): assert get_readable_id(run.course.program) == run.course.program.readable_id -# pylint: disable=too-many-locals @pytest.mark.parametrize("has_coupon", [True, False]) @pytest.mark.parametrize("has_company", [True, False]) @pytest.mark.parametrize("is_program_product", [True, False]) @@ -380,10 +377,10 @@ def test_get_valid_coupon_versions_after_redemption(user, is_global): @pytest.mark.parametrize("is_global", [True, False]) @pytest.mark.parametrize( - "discount_type, amount", + "discount_type, amount", # noqa: PT006 [ - [DISCOUNT_TYPE_DOLLARS_OFF, 100], - [DISCOUNT_TYPE_PERCENT_OFF, 1.0], + [DISCOUNT_TYPE_DOLLARS_OFF, 100], # noqa: PT007 + [DISCOUNT_TYPE_PERCENT_OFF, 1.0], # noqa: PT007 ], ) def test_get_valid_coupon_versions_with_max_redemptions_per_user( @@ -439,10 +436,10 @@ def test_get_valid_coupon_versions_with_max_redemptions_per_user( @pytest.mark.parametrize( - "discount_type, amount", + "discount_type, amount", # noqa: PT006 [ - [DISCOUNT_TYPE_DOLLARS_OFF, 100], - [DISCOUNT_TYPE_PERCENT_OFF, 1.0], + [DISCOUNT_TYPE_DOLLARS_OFF, 100], # noqa: PT007 + [DISCOUNT_TYPE_PERCENT_OFF, 1.0], # noqa: PT007 ], ) def test_global_coupons_apply_all_products(user, discount_type, amount): @@ -473,10 +470,10 @@ def test_global_coupons_apply_all_products(user, discount_type, amount): @pytest.mark.parametrize( - "best_discount_type, best_discount_amount, lesser_coupons_type, lesser_coupons_amounts", + "best_discount_type, best_discount_amount, lesser_coupons_type, lesser_coupons_amounts", # noqa: PT006 [ - [DISCOUNT_TYPE_DOLLARS_OFF, 100, DISCOUNT_TYPE_PERCENT_OFF, [0.1, 0.2, 0.5]], - [DISCOUNT_TYPE_PERCENT_OFF, 1.0, DISCOUNT_TYPE_DOLLARS_OFF, [10, 20, 80]], + [DISCOUNT_TYPE_DOLLARS_OFF, 100, DISCOUNT_TYPE_PERCENT_OFF, [0.1, 0.2, 0.5]], # noqa: PT007 + [DISCOUNT_TYPE_PERCENT_OFF, 1.0, DISCOUNT_TYPE_DOLLARS_OFF, [10, 20, 80]], # noqa: PT007 ], ) def test_best_coupon_return_best_coupon_between_discount_types( @@ -661,13 +658,13 @@ def test_get_product_price(basket_and_coupons): @pytest.mark.parametrize("has_coupon", [True, False]) @pytest.mark.parametrize( - "discount_type, amount, price, discounted_price", + "discount_type, amount, price, discounted_price", # noqa: PT006 [ - [DISCOUNT_TYPE_PERCENT_OFF, 0.5, 100, 50], - [DISCOUNT_TYPE_DOLLARS_OFF, 50, 100, 50], + [DISCOUNT_TYPE_PERCENT_OFF, 0.5, 100, 50], # noqa: PT007 + [DISCOUNT_TYPE_DOLLARS_OFF, 50, 100, 50], # noqa: PT007 ], ) -def test_get_product_version_price_with_discount( +def test_get_product_version_price_with_discount( # noqa: PLR0913 has_coupon, basket_and_coupons, discount_type, amount, price, discounted_price ): """ @@ -706,9 +703,9 @@ def test_get_by_reference_number( same_order = Order.objects.get_by_reference_number(order.reference_number) assert same_order.id == order.id if hubspot_api_key: - assert mock_hubspot_syncs.order.called_with(order.id) + assert mock_hubspot_syncs.order.called_with(order.id) # noqa: PGH005 else: - assert mock_hubspot_syncs.order.not_called() + assert mock_hubspot_syncs.order.not_called() # noqa: PGH005 def test_get_by_reference_number_missing(validated_basket): @@ -726,14 +723,14 @@ def test_get_by_reference_number_missing(validated_basket): @pytest.mark.parametrize("hubspot_api_key", [None, "fake-key"]) @pytest.mark.parametrize("has_coupon", [True, False]) -def test_create_unfulfilled_order( +def test_create_unfulfilled_order( # noqa: PLR0913 settings, validated_basket, has_coupon, basket_and_coupons, hubspot_api_key, mock_hubspot_syncs, -): # pylint: disable=too-many-locals +): """ create_unfulfilled_order should create an Order from a purchasable course """ @@ -772,9 +769,9 @@ def test_create_unfulfilled_order( assert CouponRedemption.objects.count() == 0 if hubspot_api_key: - assert mock_hubspot_syncs.order.called_with(order.id) + assert mock_hubspot_syncs.order.called_with(order.id) # noqa: PGH005 else: - assert mock_hubspot_syncs.order.not_called() + assert mock_hubspot_syncs.order.not_called() # noqa: PGH005 @pytest.mark.parametrize("has_program_run", [True, False]) @@ -798,7 +795,7 @@ def test_create_unfulfilled_order_program_run(validated_basket, has_program_run) assert line.programrunline.program_run == basket_item.program_run else: with pytest.raises(ObjectDoesNotExist): - line.programrunline # pylint: disable=pointless-statement + line.programrunline # noqa: B018 def test_create_unfulfilled_order_affiliate(validated_basket): @@ -1342,7 +1339,7 @@ def test_fetch_and_serialize_unused_coupons(user): past = now - timedelta(days=5) coupons = CouponFactory.create_batch(2) - # Create 3 payment versions – the first 2 will apply to the same coupon, and the + # Create 3 payment versions - the first 2 will apply to the same coupon, and the # second will be the most recent version for the coupon. The last payment version # will be set to expired. payment_versions = CouponPaymentVersionFactory.create_batch( @@ -1474,8 +1471,8 @@ def test_fetch_and_serialize_unused_coupons_for_all_inactive_products(user): @pytest.mark.parametrize( - "use_defaults,num_coupon_codes", - ( + "use_defaults,num_coupon_codes", # noqa: PT006 + ( # noqa: PT007 (True, 12), (False, 1), ), @@ -1548,12 +1545,12 @@ def test_create_coupons(use_defaults, num_coupon_codes): @pytest.mark.parametrize( - "input_text_id,run_text_id,program_text_id,prog_run_tag", + "input_text_id,run_text_id,program_text_id,prog_run_tag", # noqa: PT006 [ - ["course-v1:some+run", "course-v1:some+run", None, None], - ["program-v1:some+program", None, "program-v1:some+program", None], - ["program-v1:some+program+R1", None, "program-v1:some+program+R1", None], - ["program-v1:some+program+R1", None, "program-v1:some+program", "R1"], + ["course-v1:some+run", "course-v1:some+run", None, None], # noqa: PT007 + ["program-v1:some+program", None, "program-v1:some+program", None], # noqa: PT007 + ["program-v1:some+program+R1", None, "program-v1:some+program+R1", None], # noqa: PT007 + ["program-v1:some+program+R1", None, "program-v1:some+program", "R1"], # noqa: PT007 ], ) def test_get_product_from_text_id( @@ -1609,11 +1606,11 @@ def test_get_product_from_text_id_failure(): @pytest.mark.parametrize( - "qs_product_id,exp_text_id", + "qs_product_id,exp_text_id", # noqa: PT006 [ - ["123", None], - ["course-v1:some+id", "course-v1:some+id"], - ["course-v1:some id", "course-v1:some+id"], + ["123", None], # noqa: PT007 + ["course-v1:some+id", "course-v1:some+id"], # noqa: PT007 + ["course-v1:some id", "course-v1:some+id"], # noqa: PT007 ], ) def test_get_product_from_querystring_id(mocker, qs_product_id, exp_text_id): @@ -1644,12 +1641,12 @@ class FakeRequest: @pytest.mark.parametrize( - "is_client_ip_taxable,is_client_location_taxable", + "is_client_ip_taxable,is_client_location_taxable", # noqa: PT006 [ - [True, True], - [True, False], - [False, True], - [False, False], + [True, True], # noqa: PT007 + [True, False], # noqa: PT007 + [False, True], # noqa: PT007 + [False, False], # noqa: PT007 ], ) def test_tax_calc_from_ip(user, is_client_ip_taxable, is_client_location_taxable): diff --git a/ecommerce/apps.py b/ecommerce/apps.py index 3f66ffe2c..a042e478e 100644 --- a/ecommerce/apps.py +++ b/ecommerce/apps.py @@ -9,4 +9,4 @@ class EcommerceConfig(AppConfig): def ready(self): """Application is ready""" - import ecommerce.signals # pylint:disable=unused-import, unused-variable + import ecommerce.signals # noqa: F401 diff --git a/ecommerce/conftest.py b/ecommerce/conftest.py index 5105caaad..01d4e9b94 100644 --- a/ecommerce/conftest.py +++ b/ecommerce/conftest.py @@ -5,30 +5,29 @@ import pytest -# pylint:disable=redefined-outer-name from ecommerce.api import ValidatedBasket from ecommerce.constants import DISCOUNT_TYPE_PERCENT_OFF from ecommerce.factories import ( BasketItemFactory, + CompanyFactory, CouponEligibilityFactory, CouponFactory, CouponPaymentFactory, CouponPaymentVersionFactory, - CouponVersionFactory, CouponSelectionFactory, - CompanyFactory, + CouponVersionFactory, + DataConsentAgreementFactory, DataConsentUserFactory, ProductVersionFactory, - DataConsentAgreementFactory, ) from ecommerce.models import CourseRunSelection -CouponGroup = namedtuple( +CouponGroup = namedtuple( # noqa: PYI024 "CouponGroup", ["coupon", "coupon_version", "payment", "payment_version"] ) -@pytest.fixture() +@pytest.fixture def basket_and_coupons(): """ Sample basket and coupon diff --git a/ecommerce/exceptions.py b/ecommerce/exceptions.py index 80ed5ee37..8642eec3f 100644 --- a/ecommerce/exceptions.py +++ b/ecommerce/exceptions.py @@ -3,25 +3,25 @@ """ -class EcommerceException(Exception): +class EcommerceException(Exception): # noqa: N818 """ General exception regarding ecommerce """ -class EcommerceEdxApiException(Exception): +class EcommerceEdxApiException(Exception): # noqa: N818 """ Exception regarding edx_api_client """ -class EcommerceModelException(Exception): +class EcommerceModelException(Exception): # noqa: N818 """ Exception regarding ecommerce models """ -class ParseException(Exception): +class ParseException(Exception): # noqa: N818 """ Exception regarding parsing CyberSource reference numbers """ diff --git a/ecommerce/factories.py b/ecommerce/factories.py index 26bdb29eb..959eaf2d8 100644 --- a/ecommerce/factories.py +++ b/ecommerce/factories.py @@ -14,7 +14,6 @@ from mitxpro.utils import now_in_utc from users.factories import UserFactory - FAKE = faker.Factory.create() @@ -217,8 +216,7 @@ class DataConsentAgreementFactory(DjangoModelFactory): is_global = False @post_generation - # pylint: disable=unused-argument - def courses(self, create, extracted, **kwargs): + def courses(self, create, extracted, **kwargs): # noqa: ARG002 """Create courses for DCA""" if not create: return diff --git a/ecommerce/mail_api.py b/ecommerce/mail_api.py index 3fb318f91..963f1c8d4 100644 --- a/ecommerce/mail_api.py +++ b/ecommerce/mail_api.py @@ -20,7 +20,6 @@ ) from mitxpro.utils import format_price - log = logging.getLogger() ENROLL_ERROR_EMAIL_SUBJECT = "MIT xPRO enrollment error" EMAIL_DATE_FORMAT = "%b %-d, %Y" @@ -165,7 +164,7 @@ def send_course_run_enrollment_email(enrollment): EMAIL_COURSE_RUN_ENROLLMENT, ) ) - except: # pylint: disable=bare-except + except: # noqa: E722 log.exception("Error sending enrollment success email") @@ -187,8 +186,8 @@ def send_course_run_unenrollment_email(enrollment): EMAIL_COURSE_RUN_UNENROLLMENT, ) ) - except Exception as exp: # pylint: disable=broad-except - log.exception("Error sending unenrollment success email: %s", exp) + except Exception as exp: + log.exception("Error sending unenrollment success email: %s", exp) # noqa: TRY401 def send_b2b_receipt_email(order): @@ -247,7 +246,7 @@ def send_b2b_receipt_email(order): EMAIL_B2B_RECEIPT, ) ) - except: # pylint: disable=bare-except + except: # noqa: E722 log.exception("Error sending receipt email") @@ -331,7 +330,7 @@ def send_ecommerce_order_receipt(order, cyber_source_provided_email=None): ) api.send_messages(messages) - except: # pylint: disable=bare-except + except: # noqa: E722 log.exception("Error sending order receipt email.") @@ -352,7 +351,7 @@ def send_support_email(subject, message): [settings.EMAIL_SUPPORT], connection=connection, ) - except: # pylint: disable=bare-except + except: # noqa: E722 log.exception("Exception sending email to admins") diff --git a/ecommerce/mail_api_test.py b/ecommerce/mail_api_test.py index 9d93e1690..f945a02e3 100644 --- a/ecommerce/mail_api_test.py +++ b/ecommerce/mail_api_test.py @@ -44,13 +44,12 @@ from mitxpro.utils import format_price from users.factories import UserFactory - lazy = pytest.lazy_fixture pytestmark = pytest.mark.django_db -@pytest.fixture() +@pytest.fixture def company(): """Company object fixture""" return CompanyFactory.create(name="MIT") @@ -164,7 +163,7 @@ def test_send_b2b_receipt_email(mocker, settings, has_discount): send_b2b_receipt_email(order) run = order.product_version.product.content_object - download_url = f'{urljoin(settings.SITE_BASE_URL, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' + download_url = f'{urljoin(settings.SITE_BASE_URL, reverse("bulk-enrollment-code-receipt"))}?hash={str(order.unique_id)}' # noqa: RUF010 patched_mail_api.context_for_user.assert_called_once_with( user=None, @@ -247,7 +246,6 @@ def test_send_ecommerce_order_receipt(mocker, receipt_data, settings): ), product_version__product__content_object__course__readable_id="course:/v7/choose-agency", ) - # pylint: disable=expression-not-assigned ( ReceiptFactory.create(order=line.order, data=receipt_data) if receipt_data @@ -294,7 +292,7 @@ def test_send_ecommerce_order_receipt(mocker, receipt_data, settings): "bill_to_email": "doof@mit.edu", }, "purchaser": { - "name": " ".join(["Test", "User"]), + "name": "Test User", "email": "test@example.com", "street_address": ["11 Main Street"], "state_code": "CO", diff --git a/ecommerce/management/commands/invalidate_payment_coupons.py b/ecommerce/management/commands/invalidate_payment_coupons.py index 4d1d8e957..1482aed87 100644 --- a/ecommerce/management/commands/invalidate_payment_coupons.py +++ b/ecommerce/management/commands/invalidate_payment_coupons.py @@ -1,6 +1,6 @@ """ Disables coupons in a batch, either by reading a file or by iterating through a -coupon payment. +coupon payment. Codes that are already disabled will not be re-disabled, and any invalid codes will be skipped. @@ -25,7 +25,7 @@ class Command(BaseCommand): help = "Disables coupons in the system." - def add_arguments(self, parser) -> None: + def add_arguments(self, parser) -> None: # noqa: D102 parser.add_argument( "--payment", "-p", @@ -44,37 +44,34 @@ def add_arguments(self, parser) -> None: dest="codefile", ) - def handle(self, *args, **kwargs): + def handle(self, *args, **kwargs): # noqa: ARG002, D102 if not kwargs["payment"] and not kwargs["codefile"]: raise CommandError( - "Please specify a payment to deactivate or a code file to process." + "Please specify a payment to deactivate or a code file to process." # noqa: EM101 ) if kwargs["payment"] is not None: try: payment = CouponPayment.objects.filter(name=kwargs["payment"]).get() - except Exception: - raise CommandError( - f"Payment name {kwargs['payment']} not found or ambiguous." + except Exception: # noqa: BLE001 + raise CommandError( # noqa: B904, TRY200 + f"Payment name {kwargs['payment']} not found or ambiguous." # noqa: EM102 ) codes = Coupon.objects.filter(enabled=True, payment=payment).all() else: try: - with open(kwargs["codefile"], "r") as file: - procCodes = [] - - for line in file: - procCodes.append(line.strip()) - except Exception as e: - raise CommandError( - f"Specified file {kwargs['codefile']} could not be opened: {e}" + with open(kwargs["codefile"], "r") as file: # noqa: PTH123, UP015 + procCodes = [line.strip() for line in file] + except Exception as e: # noqa: BLE001 + raise CommandError( # noqa: B904, TRY200 + f"Specified file {kwargs['codefile']} could not be opened: {e}" # noqa: EM102 ) codes = Coupon.objects.filter(coupon_code__in=procCodes, enabled=True).all() if len(codes) == 0: - raise CommandError("No codes found.") + raise CommandError("No codes found.") # noqa: EM101 for code in codes: code.enabled = False diff --git a/ecommerce/migrations/0001_initial.py b/ecommerce/migrations/0001_initial.py index 9feab80cd..c863211dc 100644 --- a/ecommerce/migrations/0001_initial.py +++ b/ecommerce/migrations/0001_initial.py @@ -1,13 +1,12 @@ # Generated by Django 2.1.7 on 2019-03-18 18:23 -from django.conf import settings import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/ecommerce/migrations/0002_coupon_additional_fields.py b/ecommerce/migrations/0002_coupon_additional_fields.py index 1ba7d1136..ee08dc91e 100644 --- a/ecommerce/migrations/0002_coupon_additional_fields.py +++ b/ecommerce/migrations/0002_coupon_additional_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0001_initial")] operations = [ diff --git a/ecommerce/migrations/0003_invoice_to_payment.py b/ecommerce/migrations/0003_invoice_to_payment.py index 0e7c385e4..6c5ad17a1 100644 --- a/ecommerce/migrations/0003_invoice_to_payment.py +++ b/ecommerce/migrations/0003_invoice_to_payment.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-04 17:14 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("ecommerce", "0002_coupon_additional_fields")] operations = [ diff --git a/ecommerce/migrations/0004_course_run_links.py b/ecommerce/migrations/0004_course_run_links.py index 1e8838425..22f541ab1 100644 --- a/ecommerce/migrations/0004_course_run_links.py +++ b/ecommerce/migrations/0004_course_run_links.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-17 18:59 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0006_update_related_name"), ("ecommerce", "0003_invoice_to_payment"), diff --git a/ecommerce/migrations/0005_dataconsentagreement.py b/ecommerce/migrations/0005_dataconsentagreement.py index 3e2ecd1e7..4186ae19e 100644 --- a/ecommerce/migrations/0005_dataconsentagreement.py +++ b/ecommerce/migrations/0005_dataconsentagreement.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-04-22 14:39 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ ("courses", "0005_remove_desc_and_thumbnail_fields"), ("ecommerce", "0004_course_run_links"), diff --git a/ecommerce/migrations/0006_versions_related_names.py b/ecommerce/migrations/0006_versions_related_names.py index 7ac1a8791..7416b727c 100644 --- a/ecommerce/migrations/0006_versions_related_names.py +++ b/ecommerce/migrations/0006_versions_related_names.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-24 15:29 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("ecommerce", "0005_dataconsentagreement")] operations = [ diff --git a/ecommerce/migrations/0007_add_bulk_enrollment_delivery.py b/ecommerce/migrations/0007_add_bulk_enrollment_delivery.py index de53122e7..a46f9f084 100644 --- a/ecommerce/migrations/0007_add_bulk_enrollment_delivery.py +++ b/ecommerce/migrations/0007_add_bulk_enrollment_delivery.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-04-25 02:50 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("ecommerce", "0006_versions_related_names")] operations = [ diff --git a/ecommerce/migrations/0008_unique.py b/ecommerce/migrations/0008_unique.py index 2a7f53ae0..48470176c 100644 --- a/ecommerce/migrations/0008_unique.py +++ b/ecommerce/migrations/0008_unique.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("courses", "0006_update_related_name"), ("ecommerce", "0007_add_bulk_enrollment_delivery"), diff --git a/ecommerce/migrations/0009_help_text.py b/ecommerce/migrations/0009_help_text.py index cf04978e1..9ffe293d7 100644 --- a/ecommerce/migrations/0009_help_text.py +++ b/ecommerce/migrations/0009_help_text.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-05-01 15:20 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("ecommerce", "0008_unique")] operations = [ diff --git a/ecommerce/migrations/0010_remove_ecommerce_course_run_enrollment.py b/ecommerce/migrations/0010_remove_ecommerce_course_run_enrollment.py index 9acc8276e..96ba350cb 100644 --- a/ecommerce/migrations/0010_remove_ecommerce_course_run_enrollment.py +++ b/ecommerce/migrations/0010_remove_ecommerce_course_run_enrollment.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0009_help_text")] operations = [ diff --git a/ecommerce/migrations/0011_rename_bulk_enrollment_delivery.py b/ecommerce/migrations/0011_rename_bulk_enrollment_delivery.py index 80c59d4df..ae4a97a85 100644 --- a/ecommerce/migrations/0011_rename_bulk_enrollment_delivery.py +++ b/ecommerce/migrations/0011_rename_bulk_enrollment_delivery.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0010_remove_ecommerce_course_run_enrollment")] operations = [ diff --git a/ecommerce/migrations/0012_coupon_assignment_redeem_flag.py b/ecommerce/migrations/0012_coupon_assignment_redeem_flag.py index 8ad41905a..45cc668ed 100644 --- a/ecommerce/migrations/0012_coupon_assignment_redeem_flag.py +++ b/ecommerce/migrations/0012_coupon_assignment_redeem_flag.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0011_rename_bulk_enrollment_delivery")] operations = [ diff --git a/ecommerce/migrations/0013_coupon_assignment_email_index.py b/ecommerce/migrations/0013_coupon_assignment_email_index.py index 31a64c386..8f467c1c9 100644 --- a/ecommerce/migrations/0013_coupon_assignment_email_index.py +++ b/ecommerce/migrations/0013_coupon_assignment_email_index.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0012_coupon_assignment_redeem_flag")] operations = [ diff --git a/ecommerce/migrations/0014_productversion_text_id.py b/ecommerce/migrations/0014_productversion_text_id.py index 45500a99e..8ea1454b4 100644 --- a/ecommerce/migrations/0014_productversion_text_id.py +++ b/ecommerce/migrations/0014_productversion_text_id.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): - def backpopulate_readable_id(apps, schema_editor): + def backpopulate_readable_id(apps, schema_editor): # noqa: ARG002, N805 """ Set the readable_id for existing ProductVersions. """ @@ -28,7 +28,7 @@ def backpopulate_readable_id(apps, schema_editor): ).courseware_id else: log.error( - f"No matching readable_id found for ProductVersion %s", + f"No matching readable_id found for ProductVersion %s", # noqa: F541, G004 str(version.id), ) version.save() diff --git a/ecommerce/migrations/0015_db_protect.py b/ecommerce/migrations/0015_db_protect.py index e5d480a50..1b5c48b2f 100644 --- a/ecommerce/migrations/0015_db_protect.py +++ b/ecommerce/migrations/0015_db_protect.py @@ -1,10 +1,10 @@ from django.db import migrations from ecommerce.utils import ( - create_update_rule, create_delete_rule, - rollback_update_rule, + create_update_rule, rollback_delete_rule, + rollback_update_rule, ) @@ -26,7 +26,6 @@ def protection_rules(table_name): class Migration(migrations.Migration): - dependencies = [("ecommerce", "0014_productversion_text_id")] operations = [ diff --git a/ecommerce/migrations/0016_payment_type_choices.py b/ecommerce/migrations/0016_payment_type_choices.py index 9585edacd..f40984574 100644 --- a/ecommerce/migrations/0016_payment_type_choices.py +++ b/ecommerce/migrations/0016_payment_type_choices.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0015_db_protect")] operations = [ diff --git a/ecommerce/migrations/0017_order_total_price_paid.py b/ecommerce/migrations/0017_order_total_price_paid.py index 3f90e83f1..90ddb9cb2 100644 --- a/ecommerce/migrations/0017_order_total_price_paid.py +++ b/ecommerce/migrations/0017_order_total_price_paid.py @@ -63,7 +63,6 @@ def populate_total_price(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("ecommerce", "0016_payment_type_choices")] operations = [ diff --git a/ecommerce/migrations/0018_product_is_active.py b/ecommerce/migrations/0018_product_is_active.py index e69b96881..872695ecb 100644 --- a/ecommerce/migrations/0018_product_is_active.py +++ b/ecommerce/migrations/0018_product_is_active.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0017_order_total_price_paid")] operations = [ diff --git a/ecommerce/migrations/0019_amount_more_decimal_places.py b/ecommerce/migrations/0019_amount_more_decimal_places.py index c5feac80c..1d86ad8b9 100644 --- a/ecommerce/migrations/0019_amount_more_decimal_places.py +++ b/ecommerce/migrations/0019_amount_more_decimal_places.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0018_product_is_active")] operations = [ diff --git a/ecommerce/migrations/0020_add_bulk_coupon_assignment.py b/ecommerce/migrations/0020_add_bulk_coupon_assignment.py index 4cb8076a3..1dd757bef 100644 --- a/ecommerce/migrations/0020_add_bulk_coupon_assignment.py +++ b/ecommerce/migrations/0020_add_bulk_coupon_assignment.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.4 on 2019-09-24 20:02 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("ecommerce", "0019_amount_more_decimal_places")] operations = [ diff --git a/ecommerce/migrations/0021_coupon_assignment_statuses_and_flags.py b/ecommerce/migrations/0021_coupon_assignment_statuses_and_flags.py index 0da018c8e..b15de05b8 100644 --- a/ecommerce/migrations/0021_coupon_assignment_statuses_and_flags.py +++ b/ecommerce/migrations/0021_coupon_assignment_statuses_and_flags.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0020_add_bulk_coupon_assignment")] operations = [ diff --git a/ecommerce/migrations/0022_bulk_assignment_metadata_changes.py b/ecommerce/migrations/0022_bulk_assignment_metadata_changes.py index 5a9560e94..aac92e15c 100644 --- a/ecommerce/migrations/0022_bulk_assignment_metadata_changes.py +++ b/ecommerce/migrations/0022_bulk_assignment_metadata_changes.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0021_coupon_assignment_statuses_and_flags")] operations = [ diff --git a/ecommerce/migrations/0023_bulkcouponassignment_assignment_sheet_last_modified.py b/ecommerce/migrations/0023_bulkcouponassignment_assignment_sheet_last_modified.py index dd67ffae2..a9b2cb927 100644 --- a/ecommerce/migrations/0023_bulkcouponassignment_assignment_sheet_last_modified.py +++ b/ecommerce/migrations/0023_bulkcouponassignment_assignment_sheet_last_modified.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0022_bulk_assignment_metadata_changes")] operations = [ diff --git a/ecommerce/migrations/0024_add_product_visible_in_bulk_form_field.py b/ecommerce/migrations/0024_add_product_visible_in_bulk_form_field.py index 51fbea9ab..51722ef04 100644 --- a/ecommerce/migrations/0024_add_product_visible_in_bulk_form_field.py +++ b/ecommerce/migrations/0024_add_product_visible_in_bulk_form_field.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0023_bulkcouponassignment_assignment_sheet_last_modified") ] diff --git a/ecommerce/migrations/0025_add_programrun_models.py b/ecommerce/migrations/0025_add_programrun_models.py index 9d25bc57b..52428203d 100644 --- a/ecommerce/migrations/0025_add_programrun_models.py +++ b/ecommerce/migrations/0025_add_programrun_models.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.8 on 2020-02-07 18:50 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0024_programrun"), ("ecommerce", "0024_add_product_visible_in_bulk_form_field"), diff --git a/ecommerce/migrations/0026_coupon_include_future_runs.py b/ecommerce/migrations/0026_coupon_include_future_runs.py index 5d2ab1cd4..d0bca29c1 100644 --- a/ecommerce/migrations/0026_coupon_include_future_runs.py +++ b/ecommerce/migrations/0026_coupon_include_future_runs.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0025_add_programrun_models")] operations = [ diff --git a/ecommerce/migrations/0027_coupon_code_is_global.py b/ecommerce/migrations/0027_coupon_code_is_global.py index e25e1f1cc..0a9704f91 100644 --- a/ecommerce/migrations/0027_coupon_code_is_global.py +++ b/ecommerce/migrations/0027_coupon_code_is_global.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0026_coupon_include_future_runs")] operations = [ diff --git a/ecommerce/migrations/0028_include_future_runs_default.py b/ecommerce/migrations/0028_include_future_runs_default.py index 4ad06674a..7a6e180cf 100644 --- a/ecommerce/migrations/0028_include_future_runs_default.py +++ b/ecommerce/migrations/0028_include_future_runs_default.py @@ -10,7 +10,6 @@ def reset_existing_include_future_runs(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("ecommerce", "0027_coupon_code_is_global")] operations = [ diff --git a/ecommerce/migrations/0029_bulk_assignment_date_fields.py b/ecommerce/migrations/0029_bulk_assignment_date_fields.py index 12c67a2f3..b56c5c864 100644 --- a/ecommerce/migrations/0029_bulk_assignment_date_fields.py +++ b/ecommerce/migrations/0029_bulk_assignment_date_fields.py @@ -21,7 +21,6 @@ def backfill_last_assignment_date(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("ecommerce", "0028_include_future_runs_default")] operations = [ diff --git a/ecommerce/migrations/0030_linerunselection.py b/ecommerce/migrations/0030_linerunselection.py index a69e5b55e..06c19c587 100644 --- a/ecommerce/migrations/0030_linerunselection.py +++ b/ecommerce/migrations/0030_linerunselection.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.10 on 2020-03-13 20:19 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0026_nullify_expiration_date"), ("ecommerce", "0029_bulk_assignment_date_fields"), diff --git a/ecommerce/migrations/0031_productcouponassignment_original_email.py b/ecommerce/migrations/0031_productcouponassignment_original_email.py index 19638efda..252a858c3 100644 --- a/ecommerce/migrations/0031_productcouponassignment_original_email.py +++ b/ecommerce/migrations/0031_productcouponassignment_original_email.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0030_linerunselection")] operations = [ diff --git a/ecommerce/migrations/0032_add_all_courses_data_consent_flag.py b/ecommerce/migrations/0032_add_all_courses_data_consent_flag.py index 820d82fef..5b3546c4b 100644 --- a/ecommerce/migrations/0032_add_all_courses_data_consent_flag.py +++ b/ecommerce/migrations/0032_add_all_courses_data_consent_flag.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0031_productcouponassignment_original_email")] operations = [ diff --git a/ecommerce/migrations/0033_jsonField_from_django_models.py b/ecommerce/migrations/0033_jsonField_from_django_models.py index adc637bfc..c56963d1e 100644 --- a/ecommerce/migrations/0033_jsonField_from_django_models.py +++ b/ecommerce/migrations/0033_jsonField_from_django_models.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0032_add_all_courses_data_consent_flag")] operations = [ diff --git a/ecommerce/migrations/0034_productversion_requires_enrollment_code.py b/ecommerce/migrations/0034_productversion_requires_enrollment_code.py index 128349377..1a50eedbc 100644 --- a/ecommerce/migrations/0034_productversion_requires_enrollment_code.py +++ b/ecommerce/migrations/0034_productversion_requires_enrollment_code.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("ecommerce", "0033_jsonField_from_django_models")] operations = [ diff --git a/ecommerce/migrations/0035_add_coupon_discount_type.py b/ecommerce/migrations/0035_add_coupon_discount_type.py index aaa22f37d..3b7bb1795 100644 --- a/ecommerce/migrations/0035_add_coupon_discount_type.py +++ b/ecommerce/migrations/0035_add_coupon_discount_type.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0034_productversion_requires_enrollment_code"), ] diff --git a/ecommerce/migrations/0036_amount_validations.py b/ecommerce/migrations/0036_amount_validations.py index f28cfc900..485e5270e 100644 --- a/ecommerce/migrations/0036_amount_validations.py +++ b/ecommerce/migrations/0036_amount_validations.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0035_add_coupon_discount_type"), ] diff --git a/ecommerce/migrations/0037_product_coupon_assignment_index.py b/ecommerce/migrations/0037_product_coupon_assignment_index.py index 4494a2fcd..6c11c66aa 100644 --- a/ecommerce/migrations/0037_product_coupon_assignment_index.py +++ b/ecommerce/migrations/0037_product_coupon_assignment_index.py @@ -1,11 +1,10 @@ # Generated by Django 3.2.15 on 2022-12-02 16:22 -from django.db import migrations, models import django.db.models.functions.text +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0036_amount_validations"), ] diff --git a/ecommerce/migrations/0038_rename_visible_in_bulk_form_product_is_private.py b/ecommerce/migrations/0038_rename_visible_in_bulk_form_product_is_private.py index bf53e3fb5..c054ca43a 100644 --- a/ecommerce/migrations/0038_rename_visible_in_bulk_form_product_is_private.py +++ b/ecommerce/migrations/0038_rename_visible_in_bulk_form_product_is_private.py @@ -10,14 +10,13 @@ def invert_is_private(apps, schema_editor): Product = apps.get_model("ecommerce", "Product") Product.objects.all().update( is_private=models.Case( - models.When(is_private=False, then=models.Value(True)), - default=models.Value(False), + models.When(is_private=False, then=models.Value(True)), # noqa: FBT003 + default=models.Value(False), # noqa: FBT003 ) ) class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0037_product_coupon_assignment_index"), ] diff --git a/ecommerce/migrations/0040_alter_taxrate_tax_rate.py b/ecommerce/migrations/0040_alter_taxrate_tax_rate.py index d9cb13028..51ef6f2e1 100644 --- a/ecommerce/migrations/0040_alter_taxrate_tax_rate.py +++ b/ecommerce/migrations/0040_alter_taxrate_tax_rate.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0039_add_tax_rate_table"), ] diff --git a/ecommerce/migrations/0041_alter_coupon_coupon_code.py b/ecommerce/migrations/0041_alter_coupon_coupon_code.py index 99259e92a..7f1185193 100644 --- a/ecommerce/migrations/0041_alter_coupon_coupon_code.py +++ b/ecommerce/migrations/0041_alter_coupon_coupon_code.py @@ -1,11 +1,11 @@ # Generated by Django 3.2.23 on 2024-03-04 12:48 from django.db import migrations, models + import ecommerce.utils class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0040_alter_taxrate_tax_rate"), ] diff --git a/ecommerce/models.py b/ecommerce/models.py index 35bdfc98c..89bc39a55 100644 --- a/ecommerce/models.py +++ b/ecommerce/models.py @@ -20,9 +20,9 @@ REFERENCE_NUMBER_PREFIX, ) from ecommerce.utils import ( + CouponUtils, get_order_id_by_reference_number, validate_amount, - CouponUtils, ) from mail.constants import MAILGUN_EVENT_CHOICES from mitxpro.models import ( @@ -33,7 +33,6 @@ ) from mitxpro.utils import first_or_none, serialize_model_object - log = logging.getLogger() @@ -49,7 +48,7 @@ def __str__(self): return f"Company {self.name}" -class ProductQuerySet(PrefetchGenericQuerySet): # pylint: disable=missing-docstring +class ProductQuerySet(PrefetchGenericQuerySet): # noqa: D101 def active(self): """Filters for active products only""" return self.filter(is_active=True) @@ -65,7 +64,7 @@ def with_ordered_versions(self): ) -class _ProductManager(models.Manager): # pylint: disable=missing-docstring +class _ProductManager(models.Manager): def get_queryset(self): """Use the custom queryset, and filter by active products by default""" return ProductQuerySet(self.model, using=self._db).active() @@ -127,7 +126,7 @@ def run_queryset(self): elif self.content_type.model == "program": return CourseRun.objects.filter(course__program__id=self.object_id) else: - raise ValueError(f"Unexpected content type for {self.content_type.model}") + raise ValueError(f"Unexpected content type for {self.content_type.model}") # noqa: EM102 @property def type_string(self): @@ -157,7 +156,7 @@ def title(self): elif isinstance(content_object, CourseRun): return content_object.course.title else: - raise ValueError(f"Unexpected content type for {self.content_type.model}") + raise ValueError(f"Unexpected content type for {self.content_type.model}") # noqa: EM102, TRY004 @property def thumbnail_url(self): @@ -175,7 +174,7 @@ def thumbnail_url(self): elif isinstance(content_object, CourseRun): catalog_image_url = content_object.course.catalog_image_url else: - raise ValueError(f"Unexpected product {content_object}") + raise ValueError(f"Unexpected product {content_object}") # noqa: EM102, TRY004 return catalog_image_url or static(DEFAULT_COURSE_IMG_PATH) @property @@ -194,13 +193,12 @@ def start_date(self): elif isinstance(content_object, CourseRun): return content_object.course.next_run_date else: - raise ValueError(f"Unexpected product {content_object}") + raise ValueError(f"Unexpected product {content_object}") # noqa: EM102, TRY004 @property def price(self): """Return the price""" - if self.latest_version: - return self.latest_version.price + return self.latest_version.price if self.latest_version else None def __str__(self): """Description of a product""" @@ -218,7 +216,7 @@ class ProductVersion(TimestampedModel): ) price = models.DecimalField(decimal_places=2, max_digits=20) description = models.TextField() - text_id = models.TextField(null=True) + text_id = models.TextField(null=True) # noqa: DJ001 requires_enrollment_code = models.BooleanField( default=False, help_text="Requires enrollment code will require the learner to enter an enrollment code to enroll in the course at the checkout.", @@ -227,11 +225,11 @@ class ProductVersion(TimestampedModel): class Meta: indexes = [models.Index(fields=["created_on"])] - def save(self, *args, **kwargs): # pylint: disable=signature-differs + def save(self, *args, **kwargs): # noqa: D102 try: - self.text_id = getattr(self.product.content_object, "text_id") + self.text_id = getattr(self.product.content_object, "text_id") # noqa: B009 except AttributeError: - log.error( + log.error( # noqa: TRY400 "The content object for this ProductVersion (%s) does not have a `text_id` property", str(self.id), ) @@ -333,11 +331,11 @@ class Order(OrderAbstract, AuditableModel): ) total_price_paid = models.DecimalField(decimal_places=2, max_digits=20) # These represent the tax collected for the entire order. - tax_country_code = models.CharField(max_length=2, blank=True, null=True) + tax_country_code = models.CharField(max_length=2, blank=True, null=True) # noqa: DJ001 tax_rate = models.DecimalField( max_digits=6, decimal_places=4, null=True, blank=True, default=0 ) - tax_rate_name = models.CharField(max_length=100, null=True, default="VAT") + tax_rate_name = models.CharField(max_length=100, null=True, default="VAT") # noqa: DJ001 objects = OrderManager() @@ -351,7 +349,7 @@ def __str__(self): return f"Order #{self.id}, status={self.status}" @classmethod - def get_audit_class(cls): + def get_audit_class(cls): # noqa: D102 return OrderAudit def to_dict(self): @@ -431,7 +429,7 @@ class OrderAudit(AuditModel): order = models.ForeignKey(Order, null=True, on_delete=models.PROTECT) @classmethod - def get_related_field_name(cls): + def get_related_field_name(cls): # noqa: D102 return "order" @@ -481,7 +479,7 @@ def __str__(self): return f"ProgramRunLine for line: {self.id}, order: {self.line.order.id}, text id: {self.program_run.full_readable_id}" -class CouponPaymentQueryset(models.QuerySet): # pylint: disable=missing-docstring +class CouponPaymentQueryset(models.QuerySet): # noqa: D101 def with_ordered_versions(self): """Prefetches related CouponPaymentVersions in reverse creation order""" return self.prefetch_related( @@ -493,7 +491,7 @@ def with_ordered_versions(self): ).order_by("name") -class CouponPaymentManager(models.Manager): # pylint: disable=missing-docstring +class CouponPaymentManager(models.Manager): # noqa: D101 def get_queryset(self): """Sets the custom queryset""" return CouponPaymentQueryset(self.model, using=self._db) @@ -544,7 +542,7 @@ class CouponPaymentVersion(TimestampedModel): PAYMENT_STAFF = "staff" PAYMENT_TYPES = [PAYMENT_CC, PAYMENT_PO, PAYMENT_MKT, PAYMENT_SALE, PAYMENT_STAFF] - tag = models.CharField(max_length=256, null=True, blank=True) + tag = models.CharField(max_length=256, null=True, blank=True) # noqa: DJ001 payment = models.ForeignKey( CouponPayment, on_delete=models.PROTECT, related_name="versions" ) @@ -579,13 +577,13 @@ class CouponPaymentVersion(TimestampedModel): company = models.ForeignKey( Company, on_delete=models.PROTECT, null=True, blank=True ) - payment_type = models.CharField( + payment_type = models.CharField( # noqa: DJ001 max_length=128, choices=[(paytype, paytype) for paytype in PAYMENT_TYPES], null=True, blank=True, ) - payment_transaction = models.CharField(max_length=256, null=True, blank=True) + payment_transaction = models.CharField(max_length=256, null=True, blank=True) # noqa: DJ001 class Meta: indexes = [models.Index(fields=["created_on"])] @@ -622,9 +620,10 @@ def calculate_discount_amount(self, product_version=None, price=None): return Decimal(0.00) def calculate_discount_percent(self, product_version=None, price=None): - """Vice versa of calculate_discount_amount, it calculates the percentage of discount applied on a specific - product, so in this case we convert the dollars-off to percentage""" - + """ + Vice versa of calculate_discount_amount, it calculates the percentage of discount applied on a specific + product, so in this case we convert the dollars-off to percentage + """ from ecommerce.api import round_half_up price = price or (product_version.price if product_version else None) @@ -795,10 +794,10 @@ def __str__(self): return f"DataConsentUser {self.user} for {self.agreement}, consent date {self.consent_date}" -class BulkCouponAssignment(models.Model): +class BulkCouponAssignment(models.Model): # noqa: DJ008 """Records the bulk creation of ProductCouponAssignments""" - assignment_sheet_id = models.CharField(max_length=100, db_index=True, null=True) + assignment_sheet_id = models.CharField(max_length=100, db_index=True, null=True) # noqa: DJ001 sheet_last_modified_date = models.DateTimeField(null=True, blank=True) last_assignment_date = models.DateTimeField(null=True, blank=True) assignments_started_date = models.DateTimeField(null=True, blank=True) @@ -814,10 +813,10 @@ class ProductCouponAssignment(TimestampedModel): """ email = models.EmailField(blank=False) - original_email = models.EmailField(null=True, blank=True) + original_email = models.EmailField(null=True, blank=True) # noqa: DJ001 product_coupon = models.ForeignKey(CouponEligibility, on_delete=models.PROTECT) redeemed = models.BooleanField(default=False) - message_status = models.CharField( + message_status = models.CharField( # noqa: DJ001 choices=MAILGUN_EVENT_CHOICES, max_length=15, null=True, blank=True ) message_status_date = models.DateTimeField(null=True, blank=True) @@ -850,7 +849,7 @@ class TaxRate(TimestampedModel): country_code = models.CharField(max_length=2) tax_rate = models.DecimalField(max_digits=6, decimal_places=4, default=0) - tax_rate_name = models.CharField(max_length=100, null=True, default="VAT") + tax_rate_name = models.CharField(max_length=100, null=True, default="VAT") # noqa: DJ001 active = models.BooleanField(default=True) def to_dict(self): diff --git a/ecommerce/models_test.py b/ecommerce/models_test.py index 35b7fa3c1..a2eb7674a 100644 --- a/ecommerce/models_test.py +++ b/ecommerce/models_test.py @@ -22,7 +22,6 @@ from mitxpro.utils import serialize_model_object from users.factories import UserFactory - pytestmark = pytest.mark.django_db @@ -127,7 +126,7 @@ def test_latest_version(): versions_to_create = 4 product = ProductFactory.create() versions = ProductVersionFactory.create_batch(versions_to_create, product=product) - assert str(product) == "Product for {}".format(str(product.content_object)) + assert str(product) == f"Product for {str(product.content_object)}" # noqa: RUF010 assert str(versions[0]) == "ProductVersion for {}, ${}".format( versions[0].description, versions[0].price ) @@ -253,7 +252,7 @@ def test_product_version_save_text_id_badproduct(mocker): product=ProductFactory.create(content_object=LineFactory()) ) assert product_version.text_id is None - assert mock_log.called_once_with( + assert mock_log.called_once_with( # noqa: PGH005 f"The content object for this ProductVersion ({product_version.id}) does not have a `text_id` property" ) diff --git a/ecommerce/permissions.py b/ecommerce/permissions.py index d937889f4..b3c24b6ac 100644 --- a/ecommerce/permissions.py +++ b/ecommerce/permissions.py @@ -13,7 +13,7 @@ class IsSignedByCyberSource(BasePermission): Confirms that the message is signed by CyberSource """ - def has_permission(self, request, view): + def has_permission(self, request, view): # noqa: ARG002 """ Returns true if request params are signed by CyberSource """ diff --git a/ecommerce/serializers.py b/ecommerce/serializers.py index 6b7aacad9..b9d798409 100644 --- a/ecommerce/serializers.py +++ b/ecommerce/serializers.py @@ -1,11 +1,11 @@ -""" ecommerce serializers """ -# pylint: disable=too-many-lines +"""ecommerce serializers""" import logging from decimal import Decimal from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.core.validators import MaxValueValidator, MinValueValidator -from django.db import models as dj_models, transaction +from django.db import models as dj_models +from django.db import transaction from django.templatetags.static import static from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -27,12 +27,11 @@ ) from ecommerce.constants import CYBERSOURCE_CARD_TYPES, DISCOUNT_TYPES from ecommerce.models import Basket, TaxRate -from ecommerce.utils import validate_amount, CouponUtils +from ecommerce.utils import CouponUtils, validate_amount from mitxpro.serializers import WriteableSerializerMethodField from mitxpro.utils import now_in_utc from users.serializers import ExtendedLegalAddressSerializer - log = logging.getLogger(__name__) @@ -80,7 +79,7 @@ def get_type(self, instance): return instance.product.content_type.model class Meta: - fields = BaseProductVersionSerializer.Meta.fields + [ + fields = BaseProductVersionSerializer.Meta.fields + [ # noqa: RUF005 "id", "object_id", "product_id", @@ -121,7 +120,7 @@ def get_courses(self, instance): courses, many=True, context={**self.context, "filter_products": False} ).data else: - raise ValueError(f"Unexpected product for {model_class}") + raise ValueError(f"Unexpected product for {model_class}") # noqa: EM102 def get_thumbnail_url(self, instance): """Return the thumbnail for the courserun or program""" @@ -131,7 +130,7 @@ def get_thumbnail_url(self, instance): elif isinstance(content_object, CourseRun): catalog_image_url = content_object.course.catalog_image_url else: - raise ValueError(f"Unexpected product {content_object}") + raise ValueError(f"Unexpected product {content_object}") # noqa: EM102, TRY004 return catalog_image_url or static(DEFAULT_COURSE_IMG_PATH) def get_run_tag(self, instance): @@ -157,7 +156,7 @@ def get_start_date(self, instance): return None class Meta: - fields = ProductVersionSerializer.Meta.fields + [ + fields = ProductVersionSerializer.Meta.fields + [ # noqa: RUF005 "start_date", "thumbnail_url", "run_tag", @@ -211,8 +210,9 @@ def to_representation(self, value): return ProgramProductContentObjectSerializer(instance=value).data elif isinstance(value, CourseRun): return CourseRunProductContentObjectSerializer(instance=value).data - raise Exception( - "Unexpected to find type for Product.content_object:", value.__class__ + raise Exception( # noqa: TRY002 + "Unexpected to find type for Product.content_object:", # noqa: EM101 + value.__class__, ) @@ -435,17 +435,17 @@ def _get_applicable_coupon_version(cls, basket, product, coupons): return coupon_version @classmethod - def _update_basket_data( + def _update_basket_data( # noqa: PLR0913 cls, basket, updated_product=None, updated_run_ids=None, program_run=None, - should_update_program_run=False, + should_update_program_run=False, # noqa: FBT002 coupon_version=None, data_consents=None, - clear_all=False, - ): # pylint: disable=too-many-arguments + clear_all=False, # noqa: FBT002 + ): """ Creates/updates/deletes Basket-related data @@ -476,7 +476,7 @@ def _update_basket_data( basket.couponselection_set.all().delete() if updated_product is not None or should_update_program_run is True: - update_dict = dict(quantity=1) + update_dict = dict(quantity=1) # noqa: C408 if updated_product is not None: update_dict["product"] = updated_product if should_update_program_run is True: @@ -518,18 +518,18 @@ def _fetch_and_validate_product(self, items): ) self._validate_internal_product(product_content_obj) - if isinstance(product_content_obj, CourseRun): + if isinstance(product_content_obj, CourseRun): # noqa: SIM102 if ( product_content_obj.end_date and product_content_obj.end_date < now_in_utc() ): raise ValidationError( - "We're sorry, this course or program is no longer available for enrollment." + "We're sorry, this course or program is no longer available for enrollment." # noqa: EM101 ) except (ObjectDoesNotExist, MultipleObjectsReturned) as exc: if isinstance(exc, MultipleObjectsReturned): - log.error( + log.error( # noqa: TRY400 "Multiple Products found with identical ids: %s", request_product_id ) raise ValidationError( @@ -553,7 +553,7 @@ def _validate_internal_product(self, product_content_obj): ) if course_or_program.is_external: raise ValidationError( - "We're sorry, This product cannot be purchased on this web site." + "We're sorry, This product cannot be purchased on this web site." # noqa: EM101 ) def _validate_and_compare_runs(self, basket, items, product): @@ -577,7 +577,7 @@ def _validate_and_compare_runs(self, basket, items, product): ) return run_ids if run_ids != existing_run_ids else None - def update(self, instance, validated_data): # pylint: disable=too-many-locals + def update(self, instance, validated_data): # noqa: D102 items = validated_data.get("items") coupons = validated_data.get("coupons") data_consents = validated_data.get("data_consents") @@ -586,7 +586,7 @@ def update(self, instance, validated_data): # pylint: disable=too-many-locals self._validate_coupons(coupons) if items is None and coupons is None and data_consents is None: - raise ValidationError("Invalid request") + raise ValidationError("Invalid request") # noqa: EM101 if items == []: self._update_basket_data( @@ -700,11 +700,11 @@ def validate_items(self, items): """Validate some basic things about items""" if items: if len(items) > 1: - raise ValidationError("Basket cannot contain more than one item") + raise ValidationError("Basket cannot contain more than one item") # noqa: EM101 item = items[0] product_id = item.get("product_id") if product_id is None: - raise ValidationError("Invalid request") + raise ValidationError("Invalid request") # noqa: EM101 return {"items": items} def validate_coupons(self, coupons): @@ -722,7 +722,7 @@ def validate_data_consents(self, data_consents): invalid_consent_ids = set(data_consents) - valid_consent_ids if invalid_consent_ids: raise ValidationError( - f"Invalid data consent id {','.join([str(consent_id) for consent_id in invalid_consent_ids])}" + f"Invalid data consent id {','.join([str(consent_id) for consent_id in invalid_consent_ids])}" # noqa: EM102 ) return {"data_consents": data_consents} @@ -831,7 +831,7 @@ def validate(self, attrs): ) return attrs - def create(self, validated_data): + def create(self, validated_data): # noqa: D102 return create_coupons( company_id=validated_data.get("company"), tag=validated_data.get("tag"), @@ -968,7 +968,6 @@ def get_receipt(self, instance): def get_lines(self, instance): """Get product information along with applied discounts""" - # pylint: disable=too-many-locals coupon_redemption = instance.couponredemption_set.first() lines = [] for line in instance.lines.all(): @@ -1009,9 +1008,7 @@ def get_lines(self, instance): if certificate_page: CEUs = certificate_page.CEUs - for ( - override - ) in certificate_page.overrides: # pylint: disable=not-an-iterable + for override in certificate_page.overrides: if ( override.value.get("readable_id") == line.product_version.text_id @@ -1035,7 +1032,7 @@ def get_lines(self, instance): def get_order(self, instance): """Get order-specific information""" - return dict( + return dict( # noqa: C408 id=instance.id, created_on=instance.created_on, reference_number=instance.reference_number, diff --git a/ecommerce/serializers_test.py b/ecommerce/serializers_test.py index fe1cf0a6f..be83f594a 100644 --- a/ecommerce/serializers_test.py +++ b/ecommerce/serializers_test.py @@ -1,7 +1,6 @@ """ Tests for ecommerce serializers """ -# pylint: disable=unused-argument, redefined-outer-name from decimal import Decimal import pytest @@ -47,7 +46,6 @@ ) from mitxpro.test_utils import any_instance_of - pytestmark = [pytest.mark.django_db] datetime_format = "%Y-%m-%dT%H:%M:%SZ" @@ -123,7 +121,7 @@ def test_serialize_basket_product_version_programrun(mock_context): product_version = ProductVersionFactory.create( product=ProductFactory(content_object=program_run.program) ) - context = {**mock_context, **{"program_run": program_run}} + context = {**mock_context, **{"program_run": program_run}} # noqa: PIE800 data = FullProductVersionSerializer(instance=product_version, context=context).data assert data["object_id"] == program_run.program.id @@ -262,13 +260,13 @@ def test_serialize_coupon_single_use( @pytest.mark.parametrize( - "too_high, expected_message", + "too_high, expected_message", # noqa: PT006 [ - [ + [ # noqa: PT007 True, "The amount should be between (0 - 1) when discount type is percent-off.", ], - [ + [ # noqa: PT007 False, "The amount is invalid, please specify a value greater than 0.", ], @@ -328,13 +326,13 @@ def test_serialize_coupon_promo( @pytest.mark.parametrize( - "too_high, expected_message", + "too_high, expected_message", # noqa: PT006 [ - [ + [ # noqa: PT007 True, "The amount should be between (0 - 1) when discount type is percent-off.", ], - [ + [ # noqa: PT007 False, "The amount is invalid, please specify a value greater than 0.", ], @@ -388,7 +386,7 @@ def test_serialize_coupon_payment_version_serializer(basket_and_coupons): def test_coupon_payment_serializer(): """Test that the CouponPaymentSerializer has correct data""" payment = CouponPaymentFactory.build() - assert str(payment) == "CouponPayment {}".format(payment.name) + assert str(payment) == f"CouponPayment {payment.name}" serialized = CouponPaymentSerializer(payment).data assert serialized == { "name": payment.name, @@ -542,9 +540,7 @@ def test_serialize_coupon(): name = "Some Coupon" code = "1234" coupon = CouponFactory.build(payment__name=name, coupon_code=code, enabled=True) - assert str(coupon) == "Coupon {} for {}".format( - coupon.coupon_code, str(coupon.payment) - ) + assert str(coupon) == f"Coupon {coupon.coupon_code} for {str(coupon.payment)}" # noqa: RUF010 serialized_data = CouponSerializer(instance=coupon).data assert serialized_data == { "id": None, diff --git a/ecommerce/signals.py b/ecommerce/signals.py index 22d646e89..2f66f2a18 100644 --- a/ecommerce/signals.py +++ b/ecommerce/signals.py @@ -2,14 +2,13 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from ecommerce.models import ProductVersion, Product, CouponEligibility -from hubspot_xpro.task_helpers import sync_hubspot_product - from courses.models import CourseRun +from ecommerce.models import CouponEligibility, Product, ProductVersion +from hubspot_xpro.task_helpers import sync_hubspot_product @receiver(post_save, sender=ProductVersion, dispatch_uid="product_version_post_save") -def sync_product(sender, instance, created, **kwargs): # pylint:disable=unused-argument +def sync_product(sender, instance, created, **kwargs): # noqa: ARG001 """ Sync product to hubspot """ @@ -18,8 +17,11 @@ def sync_product(sender, instance, created, **kwargs): # pylint:disable=unused- @receiver(post_save, sender=Product, dispatch_uid="product_post_save") def apply_coupon_on_all_runs( - sender, instance, created, **kwargs -): # pylint:disable=unused-argument + sender, # noqa: ARG001 + instance, + created, + **kwargs, # noqa: ARG001 +): """ Apply coupons to all courseruns of a course, if `include_future_runs = True` """ diff --git a/ecommerce/signals_test.py b/ecommerce/signals_test.py index 25e327b65..f93254e98 100644 --- a/ecommerce/signals_test.py +++ b/ecommerce/signals_test.py @@ -4,10 +4,9 @@ import pytest from courses.factories import CourseFactory, CourseRunFactory -from ecommerce.factories import CouponFactory, CouponEligibilityFactory, ProductFactory +from ecommerce.factories import CouponEligibilityFactory, CouponFactory, ProductFactory from ecommerce.models import CouponEligibility - pytestmark = pytest.mark.django_db diff --git a/ecommerce/test_utils.py b/ecommerce/test_utils.py index 4cf5bb2c8..2fd26a4bb 100644 --- a/ecommerce/test_utils.py +++ b/ecommerce/test_utils.py @@ -1,15 +1,15 @@ """Functions used in testing ecommerce""" from contextlib import contextmanager -from django.db import connection import faker +from django.db import connection from ecommerce.api import generate_cybersource_sa_signature from ecommerce.utils import ( - create_update_rule, create_delete_rule, - rollback_update_rule, + create_update_rule, rollback_delete_rule, + rollback_update_rule, ) FAKE = faker.Factory.create() diff --git a/ecommerce/urls.py b/ecommerce/urls.py index 46afb922d..360b564c6 100644 --- a/ecommerce/urls.py +++ b/ecommerce/urls.py @@ -1,6 +1,5 @@ """URLs for ecommerce""" -from django.urls import path -from django.urls import include, re_path +from django.urls import include, path, re_path from rest_framework.routers import SimpleRouter from ecommerce.views import ( @@ -16,7 +15,6 @@ coupon_code_csv_view, ) - router = SimpleRouter() router.register(r"products", ProductViewSet, basename="products_api") router.register( diff --git a/ecommerce/utils.py b/ecommerce/utils.py index 7577709f2..207307d3f 100644 --- a/ecommerce/utils.py +++ b/ecommerce/utils.py @@ -1,6 +1,6 @@ """Utility functions for ecommerce""" import logging -from urllib.parse import urljoin, urlencode +from urllib.parse import urlencode, urljoin from django.conf import settings from django.core.exceptions import ValidationError @@ -54,11 +54,11 @@ def get_order_id_by_reference_number(*, reference_number, prefix): prefix_with_dash, reference_number, ) - raise ParseException(f"Reference number must start with {prefix_with_dash}") + raise ParseException(f"Reference number must start with {prefix_with_dash}") # noqa: EM102 try: order_id = int(reference_number[len(prefix_with_dash) :]) except ValueError: - raise ParseException("Unable to parse order number") + raise ParseException("Unable to parse order number") # noqa: B904, EM101, TRY200 return order_id @@ -102,7 +102,7 @@ def validate_amount(discount_type, amount): if amount <= 0: return "The amount is invalid, please specify a value greater than 0." - if discount_type == DISCOUNT_TYPE_PERCENT_OFF and amount > 1: + if discount_type == DISCOUNT_TYPE_PERCENT_OFF and amount > 1: # noqa: RET503 return "The amount should be between (0 - 1) when discount type is percent-off." @@ -112,13 +112,17 @@ def positive_or_zero(number): class CouponUtils: + """ + Common Utils for Coupon and B2BCoupon + """ + @staticmethod def validate_unique_coupon_code(value): """ Validate the uniqueness of coupon codes in Coupon and B2BCoupon models. """ if CouponUtils.is_existing_coupon_code(value): - raise ValidationError("Coupon code already exists in the platform.") + raise ValidationError("Coupon code already exists in the platform.") # noqa: EM101 @staticmethod def is_existing_coupon_code(value): diff --git a/ecommerce/utils_test.py b/ecommerce/utils_test.py index c92a35e2a..34795ea8c 100644 --- a/ecommerce/utils_test.py +++ b/ecommerce/utils_test.py @@ -1,13 +1,13 @@ """Tests for utility functions for ecommerce""" import pytest -from ecommerce.constants import DISCOUNT_TYPE_PERCENT_OFF, DISCOUNT_TYPE_DOLLARS_OFF +from ecommerce.constants import DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF from ecommerce.exceptions import ParseException from ecommerce.utils import get_order_id_by_reference_number, validate_amount @pytest.mark.parametrize( - "reference_number, error", + "reference_number, error", # noqa: PT006 [ ("XYZ-1-3", "Reference number must start with MITXPRO-cyb-prefix-"), ("MITXPRO-cyb-prefix-NaN", "Unable to parse order number"), @@ -25,7 +25,7 @@ def test_get_order_id_by_reference_number_parse_error(reference_number, error): @pytest.mark.parametrize( - "discount_type, amount, error", + "discount_type, amount, error", # noqa: PT006 [ ( DISCOUNT_TYPE_PERCENT_OFF, diff --git a/ecommerce/views.py b/ecommerce/views.py index 0f36a2c96..adfbae27f 100644 --- a/ecommerce/views.py +++ b/ecommerce/views.py @@ -63,7 +63,6 @@ now_in_utc, ) - log = logging.getLogger(__name__) @@ -79,7 +78,7 @@ class ProductViewSet(ReadOnlyModelViewSet): filter_backends = (filters.DjangoFilterBackend,) filterset_class = ProductFilter - def get_queryset(self): + def get_queryset(self): # noqa: D102 now = now_in_utc() expired_courseruns = CourseRun.objects.filter( enrollment_end__lt=now @@ -154,7 +153,7 @@ class ProgramRunsViewSet(ReadOnlyModelViewSet): permission_classes = () serializer_class = ProgramRunSerializer - def get_queryset(self): + def get_queryset(self): # noqa: D102 return ProgramRun.objects.filter( program__products=self.kwargs["program_product_id"] ) @@ -180,8 +179,11 @@ class CheckoutView(APIView): permission_classes = (IsAuthenticated,) def post( - self, request, *args, **kwargs - ): # pylint: disable=too-many-locals,unused-argument + self, + request, + *args, # noqa: ARG002 + **kwargs, # noqa: ARG002 + ): """ Create a new unfulfilled Order from the user's basket and return information used to submit to CyberSource. @@ -211,11 +213,11 @@ def post( # for GTM in order to track these purchases as well. Actual tracking # call is sent from the frontend. payload = { - "transaction_id": "T-{}".format(order.id), + "transaction_id": f"T-{order.id}", "transaction_total": 0.00, "product_type": product.type_string, "courseware_id": text_id, - "reference_number": "REF-{}".format(order.id), + "reference_number": f"REF-{order.id}", } # This redirects the user to our order success page @@ -248,7 +250,7 @@ class OrderFulfillmentView(APIView): authentication_classes = () permission_classes = (IsSignedByCyberSource,) - def post(self, request, *args, **kwargs): # pylint: disable=unused-argument + def post(self, request, *args, **kwargs): # noqa: ARG002 """ Confirmation from CyberSource which fulfills an existing Order. """ @@ -260,7 +262,7 @@ def post(self, request, *args, **kwargs): # pylint: disable=unused-argument fulfill_order(request.data) else: raise ParseException( - f"Unknown prefix '{reference_number}' for reference number" + f"Unknown prefix '{reference_number}' for reference number" # noqa: EM102 ) except: # Not sure what would cause an error here but make sure we save the receipt @@ -281,7 +283,7 @@ class OrderReceiptView(RetrieveAPIView): serializer_class = OrderReceiptSerializer - def get_queryset(self): + def get_queryset(self): # noqa: D102 return Order.objects.filter(purchaser=self.request.user, status=Order.FULFILLED) def get(self, request, *args, **kwargs): @@ -312,7 +314,7 @@ class CouponListView(APIView): permission_classes = (IsAdminUser,) authentication_classes = (SessionAuthentication,) - def post(self, request, *args, **kwargs): # pylint: disable=unused-argument + def post(self, request, *args, **kwargs): # noqa: ARG002 """Create coupon(s) and related objects""" # Determine what kind of coupon this is. if request.data.get("coupon_type") == CouponPaymentVersion.SINGLE_USE: diff --git a/ecommerce/views_test.py b/ecommerce/views_test.py index 08fe93809..6ac161d49 100644 --- a/ecommerce/views_test.py +++ b/ecommerce/views_test.py @@ -6,9 +6,9 @@ import factory import faker import pytest -import rest_framework.status as status # pylint: disable=useless-import-alias from django.db.models import Count, Q from django.urls import reverse +from rest_framework import status from rest_framework.renderers import JSONRenderer from rest_framework.test import APIClient @@ -63,7 +63,6 @@ from mitxpro.utils import dict_without_keys, now_in_utc from users.factories import UserFactory - CYBERSOURCE_SECURE_ACCEPTANCE_URL = "http://fake" CYBERSOURCE_ACCESS_KEY = "access" CYBERSOURCE_PROFILE_ID = "profile" @@ -75,9 +74,6 @@ pytestmark = pytest.mark.django_db -# pylint: disable=redefined-outer-name,unused-argument,too-many-lines,too-many-arguments - - def render_json(serializer): """ Convert serializer data to a JSON object. @@ -93,7 +89,7 @@ def render_json(serializer): @pytest.fixture(autouse=True) -def ecommerce_settings(settings): +def ecommerce_settings(settings): # noqa: PT004 """ Set cybersource settings """ @@ -105,7 +101,6 @@ def ecommerce_settings(settings): settings.EDXORG_BASE_URL = "http://edx_base" -# pylint: disable=redefined-outer-name @pytest.fixture def basket_client(basket_and_coupons): """DRF Client with logged in user with basket""" @@ -174,14 +169,14 @@ def test_creates_order(basket_client, mocker, basket_and_coupons): @pytest.mark.parametrize("hubspot_api_key", [None, "fake-key"]) -def test_zero_price_checkout( +def test_zero_price_checkout( # noqa: PLR0913 basket_client, mocker, basket_and_coupons, mock_hubspot_syncs, settings, hubspot_api_key, -): # pylint:disable=too-many-arguments +): """ If the order total is $0, we should just fulfill the order and direct the user to our order receipt page """ @@ -209,11 +204,11 @@ def test_zero_price_checkout( assert resp.status_code == status.HTTP_200_OK assert resp.json() == { "payload": { - "transaction_id": "T-{}".format(order.id), + "transaction_id": f"T-{order.id}", "transaction_total": 0.0, "product_type": line.product_version.product.type_string, "courseware_id": text_id, - "reference_number": "REF-{}".format(order.id), + "reference_number": f"REF-{order.id}", }, "url": f"http://testserver/dashboard/?status=purchased&purchased={quote_plus(text_id)}", "method": "GET", @@ -228,13 +223,13 @@ def test_zero_price_checkout( assert CourseRunSelection.objects.filter(basket__user=user).count() == 0 assert CouponSelection.objects.filter(basket__user=user).count() == 0 if hubspot_api_key: - assert mock_hubspot_syncs.order.called_with(order.id) + assert mock_hubspot_syncs.order.called_with(order.id) # noqa: PGH005 else: - assert mock_hubspot_syncs.order.not_called() + assert mock_hubspot_syncs.order.not_called() # noqa: PGH005 @pytest.mark.parametrize("hubspot_api_key", [None, "fake-key"]) -def test_order_fulfilled( +def test_order_fulfilled( # noqa: PLR0913 mocker, settings, basket_client, @@ -242,7 +237,7 @@ def test_order_fulfilled( validated_basket, hubspot_api_key, mock_hubspot_syncs, -): # pylint:disable=too-many-arguments +): """ Test the happy case """ @@ -271,7 +266,7 @@ def test_order_fulfilled( assert order.status == Order.FULFILLED assert order.receipt_set.count() == 1 receipt = order.receipt_set.first() - assert str(receipt) == "Receipt for order {}".format(receipt.order.id) + assert str(receipt) == f"Receipt for order {receipt.order.id}" assert receipt.data == data enroll_user.assert_called_with(order) @@ -290,9 +285,9 @@ def test_order_fulfilled( assert CouponSelection.objects.filter(basket__user=user).count() == 0 if hubspot_api_key: - assert mock_hubspot_syncs.order.called_with(order.id) + assert mock_hubspot_syncs.order.called_with(order.id) # noqa: PGH005 else: - assert mock_hubspot_syncs.order.not_called() + assert mock_hubspot_syncs.order.not_called() # noqa: PGH005 def test_order_affiliate(basket_client, mocker, basket_and_coupons): @@ -333,7 +328,7 @@ def test_missing_fields(basket_client, mocker): mocker.patch( "ecommerce.views.IsSignedByCyberSource.has_permission", return_value=True ) - try: + try: # noqa: SIM105 # Missing fields from Cybersource POST will cause a ParseException. # In this test we just care that we saved the data in Receipt for later # analysis. @@ -389,10 +384,10 @@ def test_ignore_duplicate_cancel( @pytest.mark.parametrize( - "order_status, decision", + "order_status, decision", # noqa: PT006 [(Order.FAILED, "ERROR"), (Order.FULFILLED, "ERROR"), (Order.FULFILLED, "SUCCESS")], ) -def test_error_on_duplicate_order( +def test_error_on_duplicate_order( # noqa: PLR0913 mocker, validated_basket, basket_client, basket_and_coupons, order_status, decision ): """If there is a duplicate message (except for CANCEL), raise an exception""" @@ -438,17 +433,17 @@ def test_get_basket(basket_client, basket_and_coupons, mock_context, mocker): @pytest.mark.parametrize( - "receipts_enabled, order_status, expected_status_code", + "receipts_enabled, order_status, expected_status_code", # noqa: PT006 [ - [True, Order.FULFILLED, status.HTTP_200_OK], - [True, Order.CREATED, status.HTTP_404_NOT_FOUND], - [True, Order.REFUNDED, status.HTTP_404_NOT_FOUND], - [False, Order.FULFILLED, status.HTTP_404_NOT_FOUND], - [False, Order.CREATED, status.HTTP_404_NOT_FOUND], - [False, Order.REFUNDED, status.HTTP_404_NOT_FOUND], + [True, Order.FULFILLED, status.HTTP_200_OK], # noqa: PT007 + [True, Order.CREATED, status.HTTP_404_NOT_FOUND], # noqa: PT007 + [True, Order.REFUNDED, status.HTTP_404_NOT_FOUND], # noqa: PT007 + [False, Order.FULFILLED, status.HTTP_404_NOT_FOUND], # noqa: PT007 + [False, Order.CREATED, status.HTTP_404_NOT_FOUND], # noqa: PT007 + [False, Order.REFUNDED, status.HTTP_404_NOT_FOUND], # noqa: PT007 ], ) -def test_get_order_configuration( # pylint: disable=too-many-arguments +def test_get_order_configuration( # noqa: PLR0913 settings, user, user_client, receipts_enabled, order_status, expected_status_code ): """Test the view that handles order receipts functions as expected""" @@ -461,7 +456,7 @@ def test_get_order_configuration( # pylint: disable=too-many-arguments def test_get_basket_new_user(basket_and_coupons, user, user_drf_client): """Test that the view creates a basket returns a 200 if a user doesn't already have a basket""" basket = Basket.objects.all().first() - assert str(basket) == "Basket for {}".format(str(basket.user)) + assert str(basket) == f"Basket for {str(basket.user)}" # noqa: RUF010 assert Basket.objects.filter(user=user).exists() is False resp = user_drf_client.get(reverse("basket_api")) assert resp.status_code == 200 @@ -744,7 +739,7 @@ def test_patch_basket_update_invalid_product(basket_client, basket_and_coupons): resp = basket_client.patch(reverse("basket_api"), type="json", data=data) assert resp.status_code == status.HTTP_400_BAD_REQUEST resp_data = resp.json() - assert "Invalid product id {}".format(bad_id) in resp_data["errors"]["items"] + assert f"Invalid product id {bad_id}" in resp_data["errors"]["items"] def test_patch_basket_update_active_inactive_product(basket_client, basket_and_coupons): @@ -756,10 +751,7 @@ def test_patch_basket_update_active_inactive_product(basket_client, basket_and_c resp = basket_client.patch(reverse("basket_api"), type="json", data=data) assert resp.status_code == status.HTTP_400_BAD_REQUEST resp_data = resp.json() - assert ( - "Invalid product id {product_id}".format(product_id=product.id) - in resp_data["errors"]["items"] - ) + assert f"Invalid product id {product.id}" in resp_data["errors"]["items"] product.is_active = True product.save() @@ -775,16 +767,13 @@ def test_patch_basket_update_inactive_product(basket_client, basket_and_coupons) resp = basket_client.patch(reverse("basket_api"), type="json", data=data) assert resp.status_code == status.HTTP_400_BAD_REQUEST resp_data = resp.json() - assert ( - "Invalid product id {product_id}".format(product_id=text_id) - in resp_data["errors"]["items"] - ) + assert f"Invalid product id {text_id}" in resp_data["errors"]["items"] @pytest.mark.parametrize("section", ["items", "coupons"]) def test_patch_basket_update_invalid_data(basket_client, basket_and_coupons, section): """Test that invalid product data is rejected with no changes to basket""" - data = dict() + data = dict() # noqa: C408 data[section] = [{"foo": "bar"}] resp = basket_client.patch(reverse("basket_api"), type="json", data=data) assert resp.status_code == status.HTTP_400_BAD_REQUEST @@ -856,7 +845,7 @@ def test_patch_basket_invalid_run( # If the product is a course, create a new run on a different course which is invalid. # If the product is a program, create a new run on a different program. course_run_params = ( - dict(course__program=product.content_object.course.program) + dict(course__program=product.content_object.course.program) # noqa: C408 if not is_program else {} ) @@ -1026,7 +1015,8 @@ def test_patch_basket_external_product(basket_and_coupons): @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_post_singleuse_coupons(admin_drf_client, single_use_coupon_json): """Test that the correct model objects are created for a batch of single-use coupons""" @@ -1050,7 +1040,8 @@ def test_post_singleuse_coupons(admin_drf_client, single_use_coupon_json): @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_post_global_singleuse_coupons(admin_drf_client, single_use_coupon_json): """Test that the correct model objects are created for a batch of single-use coupons (global coupon)""" @@ -1078,10 +1069,10 @@ def test_post_global_singleuse_coupons(admin_drf_client, single_use_coupon_json) @pytest.mark.parametrize( - "discount_type, amount", + "discount_type, amount", # noqa: PT006 [ - [DISCOUNT_TYPE_PERCENT_OFF, 0.5], - [DISCOUNT_TYPE_DOLLARS_OFF, 50], + [DISCOUNT_TYPE_PERCENT_OFF, 0.5], # noqa: PT007 + [DISCOUNT_TYPE_DOLLARS_OFF, 50], # noqa: PT007 ], ) def test_post_promo_coupon(admin_drf_client, promo_coupon_json, discount_type, amount): @@ -1109,7 +1100,8 @@ def test_post_promo_coupon(admin_drf_client, promo_coupon_json, discount_type, a @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_post_global_promo_coupon(admin_drf_client, promo_coupon_json): """Test that the correct model objects are created for a promo coupon (global coupon)""" @@ -1137,24 +1129,25 @@ def test_post_global_promo_coupon(admin_drf_client, promo_coupon_json): @pytest.mark.parametrize( - "attribute,bad_value,error", + "attribute,bad_value,error", # noqa: PT006 [ - [ + [ # noqa: PT007 "product_ids", [9998, 9999], "Product with id(s) 9998,9999 could not be found", ], - [ + [ # noqa: PT007 "product_ids", [], "At least one product must be selected or coupon should be global.", ], - ["name", "AlreadyExists", "This field must be unique."], - ["coupon_code", "AlreadyExists", "Coupon code already exists in the platform."], + ["name", "AlreadyExists", "This field must be unique."], # noqa: PT007 + ["coupon_code", "AlreadyExists", "Coupon code already exists in the platform."], # noqa: PT007 ], ) @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_create_promo_coupon_bad_product( admin_drf_client, promo_coupon_json, attribute, bad_value, error @@ -1170,7 +1163,8 @@ def test_create_promo_coupon_bad_product( @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_create_promo_coupon_no_payment_info(admin_drf_client, promo_coupon_json): """Test that a promo CouponPaymentVersion can be created without payment info""" @@ -1186,7 +1180,8 @@ def test_create_promo_coupon_no_payment_info(admin_drf_client, promo_coupon_json @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_create_singleuse_coupon_no_payment_info( admin_drf_client, single_use_coupon_json @@ -1205,7 +1200,8 @@ def test_create_singleuse_coupon_no_payment_info( @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_create_coupon_permission(user_drf_client, promo_coupon_json): """Test that non-admins cannot create coupons""" @@ -1215,7 +1211,8 @@ def test_create_coupon_permission(user_drf_client, promo_coupon_json): @pytest.mark.parametrize( - "discount_type", (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF) + "discount_type", + (DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF), # noqa: PT007 ) def test_coupon_csv_view(admin_client, admin_drf_client, single_use_coupon_json): """Test that a valid csv response is returned for a CouponPaymentVersion""" @@ -1273,17 +1270,17 @@ def test_bulk_assignment_csv_view(settings, admin_client, admin_drf_client): @pytest.mark.parametrize( - "url_name,url_kwarg_name,test_client,expected_status_code", + "url_name,url_kwarg_name,test_client,expected_status_code", # noqa: PT006 [ - ["coupons_csv", "version_id", lazy("admin_client"), status.HTTP_404_NOT_FOUND], - ["coupons_csv", "version_id", lazy("user_client"), status.HTTP_403_FORBIDDEN], - [ + ["coupons_csv", "version_id", lazy("admin_client"), status.HTTP_404_NOT_FOUND], # noqa: PT007 + ["coupons_csv", "version_id", lazy("user_client"), status.HTTP_403_FORBIDDEN], # noqa: PT007 + [ # noqa: PT007 "bulk_assign_csv", "bulk_assignment_id", lazy("admin_client"), status.HTTP_404_NOT_FOUND, ], - [ + [ # noqa: PT007 "bulk_assign_csv", "bulk_assignment_id", lazy("user_client"), @@ -1575,7 +1572,7 @@ def test_companies_viewset_detail(user_drf_client): response = user_drf_client.get( reverse("companies_api-detail", kwargs={"pk": company.id}) ) - assert str(company) == "Company {}".format(company.name) + assert str(company) == f"Company {company.name}" assert response.status_code == status.HTTP_200_OK assert response.json() == CompanySerializer(instance=company).data diff --git a/ecommerce/wagtail_hooks.py b/ecommerce/wagtail_hooks.py index 0d75c3785..2b3125636 100644 --- a/ecommerce/wagtail_hooks.py +++ b/ecommerce/wagtail_hooks.py @@ -6,4 +6,7 @@ @hooks.register("register_admin_viewset") def register_viewset(): + """ + Wagtail hook to register the ProductViewSetGroup. + """ return ProductViewSetGroup() diff --git a/ecommerce/wagtail_views.py b/ecommerce/wagtail_views.py index dd57ce943..c8bc61e63 100644 --- a/ecommerce/wagtail_views.py +++ b/ecommerce/wagtail_views.py @@ -59,7 +59,7 @@ class ProductInspectView(InspectView): InspectView for Product. """ - def get_object(self, queryset=None): + def get_object(self, queryset=None): # noqa: ARG002 """ Get the object using the custom Product manager. By default, `get_object` uses the default manager. """ diff --git a/fixtures/autouse.py b/fixtures/autouse.py index 338d745e8..893353124 100644 --- a/fixtures/autouse.py +++ b/fixtures/autouse.py @@ -3,6 +3,6 @@ @pytest.fixture(autouse=True) -def disable_hubspot_api(settings): +def disable_hubspot_api(settings): # noqa: PT004 """Disable Hubspot API by default for tests""" settings.MITOL_HUBSPOT_API_PRIVATE_TOKEN = None diff --git a/fixtures/common.py b/fixtures/common.py index 0bde19665..2268e5f6c 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -1,5 +1,4 @@ """Common fixtures""" -# pylint: disable=unused-argument, redefined-outer-name import pytest import responses @@ -11,19 +10,19 @@ @pytest.fixture -def user(db): +def user(db): # noqa: ARG001 """Creates a user""" return UserFactory.create() @pytest.fixture -def staff_user(db): +def staff_user(db): # noqa: ARG001 """Staff user fixture""" return UserFactory.create(is_staff=True) @pytest.fixture -def super_user(db): +def super_user(db): # noqa: ARG001 """Super user fixture""" return UserFactory.create(is_staff=True, is_superuser=True) @@ -81,13 +80,13 @@ def mock_context(mocker, user): return {"request": mocker.Mock(user=user)} -@pytest.fixture() +@pytest.fixture def wagtail_site(): """Fixture for Wagtail default site""" return Site.objects.get(is_default_site=True) -@pytest.fixture() +@pytest.fixture def home_page(wagtail_site): """Fixture for the home page""" return wagtail_site.root_page @@ -96,7 +95,7 @@ def home_page(wagtail_site): @pytest.fixture def valid_address_dict(): """Yields a dict that will deserialize into a valid legal address""" - return dict( + return dict( # noqa: C408 first_name="Test", last_name="User", street_address_1="1 Main St", @@ -107,7 +106,7 @@ def valid_address_dict(): ) -@pytest.fixture() -def nplusone_fail(settings): +@pytest.fixture +def nplusone_fail(settings): # noqa: PT004 """Configures the nplusone app to raise errors""" settings.NPLUSONE_RAISE = True diff --git a/fixtures/cybersource.py b/fixtures/cybersource.py index f0daae5c1..92c432b7b 100644 --- a/fixtures/cybersource.py +++ b/fixtures/cybersource.py @@ -1,8 +1,7 @@ """Fxitures for CyberSource tests""" -# pylint: disable=redefined-outer-name -from nacl.public import PrivateKey import pytest +from nacl.public import PrivateKey from compliance.test_utils import ( get_cybersource_test_settings, diff --git a/hubspot_xpro/api.py b/hubspot_xpro/api.py index f01615641..29a3422ab 100644 --- a/hubspot_xpro/api.py +++ b/hubspot_xpro/api.py @@ -1,4 +1,4 @@ -""" Generate Hubspot message bodies for various model objects""" +"""Generate Hubspot message bodies for various model objects""" import logging import re from decimal import Decimal @@ -28,7 +28,6 @@ from ecommerce.models import Line, Order, Product, ProductVersion from users.models import User - log = logging.getLogger(__name__) @@ -365,7 +364,7 @@ def sync_deal_line_hubspot_ids_to_db(order, hubspot_order_id) -> bool: def get_hubspot_id_for_object( obj: Order or B2BOrder or Product or Line or B2BLine or User, - raise_error: bool = False, + raise_error: bool = False, # noqa: FBT001, FBT002 ) -> str: """ Get the hubspot id for an object, querying Hubspot if necessary @@ -412,7 +411,7 @@ def get_hubspot_id_for_object( price=serialized_product["price"], raise_count_error=raise_error, ) - if hubspot_obj and hubspot_obj.id: + if hubspot_obj and hubspot_obj.id: # noqa: RET503 HubspotObject.objects.update_or_create( object_id=obj.id, content_type=content_type, diff --git a/hubspot_xpro/api_test.py b/hubspot_xpro/api_test.py index 0a43c0e79..33be1e1a5 100644 --- a/hubspot_xpro/api_test.py +++ b/hubspot_xpro/api_test.py @@ -18,7 +18,6 @@ ) from users.factories import UserFactory - pytestmark = [pytest.mark.django_db] diff --git a/hubspot_xpro/conftest.py b/hubspot_xpro/conftest.py index 2be588a15..1bed44be9 100644 --- a/hubspot_xpro/conftest.py +++ b/hubspot_xpro/conftest.py @@ -14,9 +14,6 @@ from ecommerce.models import Order, Product from users.models import User - -# pylint: disable=redefined-outer-name - TIMESTAMPS = [ datetime(2017, 1, 1, tzinfo=timezone.utc), datetime(2017, 1, 2, tzinfo=timezone.utc), @@ -42,7 +39,7 @@ def mocked_celery(mocker): group_mock = mocker.patch("celery.group", autospec=True) chain_mock = mocker.patch("celery.chain", autospec=True) - yield SimpleNamespace( + return SimpleNamespace( replace=replace_mock, group=group_mock, chain=chain_mock, @@ -53,7 +50,7 @@ def mocked_celery(mocker): @pytest.fixture def mock_logger(mocker): """Mock the logger""" - yield mocker.patch("hubspot_xpro.tasks.log.error") + return mocker.patch("hubspot_xpro.tasks.log.error") @pytest.fixture @@ -120,4 +117,4 @@ def mock_hubspot_api(mocker): mock_api.return_value.crm.objects.basic_api.create.return_value = ( SimplePublicObject(id=FAKE_HUBSPOT_ID) ) - yield mock_api + return mock_api diff --git a/hubspot_xpro/management/commands/configure_hubspot_properties.py b/hubspot_xpro/management/commands/configure_hubspot_properties.py index 0a2bff2eb..4be4824b0 100644 --- a/hubspot_xpro/management/commands/configure_hubspot_properties.py +++ b/hubspot_xpro/management/commands/configure_hubspot_properties.py @@ -16,7 +16,6 @@ from ecommerce import models from hubspot_xpro.serializers import ORDER_TYPE_B2B, ORDER_TYPE_B2C - CUSTOM_ECOMMERCE_PROPERTIES = { # defines which hubspot properties are mapped with which local properties when objects are synced. # See https://developers.hubspot.com/docs/methods/ecomm-bridge/ecomm-bridge-overview for more details @@ -314,13 +313,13 @@ def add_arguments(self, parser): help="Delete custom hubspot properties/groups", ) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002, D102 if options["delete"]: - print("Uninstalling custom groups and properties...") + print("Uninstalling custom groups and properties...") # noqa: T201 delete_custom_properties() - print("Uninstall successful") + print("Uninstall successful") # noqa: T201 return else: - print("Configuring custom groups and properties...") + print("Configuring custom groups and properties...") # noqa: T201 upsert_custom_properties() - print("Custom properties configured") + print("Custom properties configured") # noqa: T201 diff --git a/hubspot_xpro/management/commands/sync_db_to_hubspot.py b/hubspot_xpro/management/commands/sync_db_to_hubspot.py index 538afc19f..c80ad32bb 100644 --- a/hubspot_xpro/management/commands/sync_db_to_hubspot.py +++ b/hubspot_xpro/management/commands/sync_db_to_hubspot.py @@ -13,7 +13,6 @@ from hubspot_xpro.tasks import ( batch_upsert_associations, batch_upsert_hubspot_b2b_deals, - batch_upsert_hubspot_deals, batch_upsert_hubspot_objects, ) from users.models import User @@ -39,7 +38,7 @@ def sync_contacts(self): task = batch_upsert_hubspot_objects.delay( HubspotObjectType.CONTACTS.value, ContentType.objects.get_for_model(User).model, - User._meta.app_label, + User._meta.app_label, # noqa: SLF001 create=self.create, ) start = now_in_utc() @@ -59,14 +58,14 @@ def sync_products(self): task = batch_upsert_hubspot_objects.delay( HubspotObjectType.PRODUCTS.value, ContentType.objects.get_for_model(Product).model, - Product._meta.app_label, + Product._meta.app_label, # noqa: SLF001 create=self.create, ) start = now_in_utc() task.get() total_seconds = (now_in_utc() - start).total_seconds() self.stdout.write( - "Syncing of products to hubspot finished, took {} seconds\n".format( + "Syncing of products to hubspot finished, took {} seconds\n".format( # noqa: UP032 total_seconds ) ) @@ -94,7 +93,7 @@ def sync_deals(self): task = batch_upsert_hubspot_objects.delay( HubspotObjectType.DEALS.value, ContentType.objects.get_for_model(Order).model, - Order._meta.app_label, + Order._meta.app_label, # noqa: SLF001 self.create, object_ids=self.object_ids, ) @@ -115,7 +114,7 @@ def sync_lines(self): task = batch_upsert_hubspot_objects.delay( HubspotObjectType.LINES.value, ContentType.objects.get_for_model(Line).model, - Line._meta.app_label, + Line._meta.app_label, # noqa: SLF001 self.create, object_ids=self.object_ids, ) @@ -214,7 +213,7 @@ def add_arguments(self, parser): help="create or update", ) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002, D102 if not options["mode"]: sys.stderr.write("You must specify mode ('create' or 'update')\n") sys.exit(1) diff --git a/hubspot_xpro/management/commands/sync_hubspot_ids_to_db.py b/hubspot_xpro/management/commands/sync_hubspot_ids_to_db.py index eecc4d9e6..944037b80 100644 --- a/hubspot_xpro/management/commands/sync_hubspot_ids_to_db.py +++ b/hubspot_xpro/management/commands/sync_hubspot_ids_to_db.py @@ -2,7 +2,6 @@ Management command to sync hubspot ids to database """ import sys -from typing import List from django.contrib.contenttypes.models import ContentType from django.core.management import BaseCommand @@ -18,9 +17,9 @@ from users.models import User -def format_missing(missing: List[int]) -> str: +def format_missing(missing: list[int]) -> str: """Return a string of missing ids""" - return f"\n {','.join([str(id) for id in sorted(missing)])}\n\n" + return f"\n {','.join([str(id) for id in sorted(missing)])}\n\n" # noqa: A001 class Command(BaseCommand): @@ -43,7 +42,7 @@ def sync_contacts(self): ).values_list("username", flat=True) if not result and missing.count() > 0: sys.stderr.write( - f"Some users could not be matched with hubspot ids:\n {','.join([username for username in missing])}\n\n" + f"Some users could not be matched with hubspot ids:\n {','.join([username for username in missing])}\n\n" # noqa: C416 ) else: sys.stdout.write("All users matched with hubspot ids\n\n") @@ -145,7 +144,7 @@ def add_arguments(self, parser): help="Sync all orders", ) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002, D102 sys.stdout.write("Syncing hubspot ids...\n") if not ( options["sync_contacts"] diff --git a/hubspot_xpro/migrations/0001_initial.py b/hubspot_xpro/migrations/0001_initial.py index 10d978ef9..b23bdf46a 100644 --- a/hubspot_xpro/migrations/0001_initial.py +++ b/hubspot_xpro/migrations/0001_initial.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/hubspot_xpro/migrations/0002_hubspotlineresync.py b/hubspot_xpro/migrations/0002_hubspotlineresync.py index 6f82e84fc..57d736fe4 100644 --- a/hubspot_xpro/migrations/0002_hubspotlineresync.py +++ b/hubspot_xpro/migrations/0002_hubspotlineresync.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("ecommerce", "0013_coupon_assignment_email_index"), ("hubspot_xpro", "0001_initial"), diff --git a/hubspot_xpro/migrations/0003_delete_hubspot_errors.py b/hubspot_xpro/migrations/0003_delete_hubspot_errors.py index bd4489dae..2facb1d90 100644 --- a/hubspot_xpro/migrations/0003_delete_hubspot_errors.py +++ b/hubspot_xpro/migrations/0003_delete_hubspot_errors.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("hubspot_xpro", "0002_hubspotlineresync"), ] diff --git a/hubspot_xpro/serializers.py b/hubspot_xpro/serializers.py index 4c40639f4..6b80ddca6 100644 --- a/hubspot_xpro/serializers.py +++ b/hubspot_xpro/serializers.py @@ -1,4 +1,4 @@ -""" Serializers for HubSpot""" +"""Serializers for HubSpot""" from django.conf import settings from mitol.hubspot_api.api import format_app_id from rest_framework import serializers @@ -11,7 +11,6 @@ from ecommerce.models import CouponRedemption, CouponVersion, ProductVersion from hubspot_xpro.api import format_product_name, get_hubspot_id_for_object - ORDER_STATUS_MAPPING = { models.Order.FULFILLED: "processed", models.Order.FAILED: "checkout_completed", @@ -92,7 +91,7 @@ def get_unique_app_id(self, instance): ) def get_quantity(self, instance): - """return the number of seats associated with the b2b order""" + """Return the number of seats associated with the b2b order""" return instance.num_seats def get_status(self, instance): @@ -166,7 +165,7 @@ def get_dealstage(self, instance): def get_closedate(self, instance): """Return the updated_on date (as a timestamp in milliseconds) if fulfilled""" - if instance.status == b2b_models.B2BOrder.FULFILLED: + if instance.status == b2b_models.B2BOrder.FULFILLED: # noqa: RET503 return int(instance.updated_on.timestamp() * 1000) def get_amount(self, instance): @@ -175,43 +174,43 @@ def get_amount(self, instance): def get_discount_amount(self, instance): """Get the discount amount if any""" - if instance.discount: + if instance.discount: # noqa: RET503 return round_half_up(instance.discount).to_eng_string() def get_discount_percent(self, instance): """Get the discount percentage if any""" - if instance.coupon: + if instance.coupon: # noqa: RET503 return round_half_up(instance.coupon.discount_percent * 100).to_eng_string() def get_discount_type(self, instance): """We are only supporting percent-off for b2b as of now""" - if instance.coupon: + if instance.coupon: # noqa: RET503 return DISCOUNT_TYPE_PERCENT_OFF def get_company(self, instance): """Get the company id if any""" - if instance.coupon: + if instance.coupon: # noqa: RET503 company = instance.coupon.company - if company: + if company: # noqa: RET503 return company.name def get_coupon_code(self, instance): """Get the coupon code used for the order if any""" - if instance.coupon: + if instance.coupon: # noqa: RET503 return instance.coupon.coupon_code def get_payment_type(self, instance): """Get the payment type""" - if instance.coupon_payment_version: + if instance.coupon_payment_version: # noqa: RET503 payment_type = instance.coupon_payment_version.payment_type - if payment_type: + if payment_type: # noqa: RET503 return payment_type def get_payment_transaction(self, instance): """Get the payment transaction id if any""" - if instance.coupon_payment_version: + if instance.coupon_payment_version: # noqa: RET503 payment_transaction = instance.coupon_payment_version.payment_transaction - if payment_transaction: + if payment_transaction: # noqa: RET503 return payment_transaction class Meta: @@ -291,14 +290,14 @@ def get_dealstage(self, instance): def get_closedate(self, instance): """Return the updated_on date (as a timestamp in milliseconds) if fulfilled""" - if instance.status == models.Order.FULFILLED: + if instance.status == models.Order.FULFILLED: # noqa: RET503 return int(instance.updated_on.timestamp() * 1000) def get_discount_type(self, instance): """Get the discount type of the applied coupon""" coupon_version = self._get_coupon_version(instance) - if coupon_version: + if coupon_version: # noqa: RET503 return coupon_version.payment_version.discount_type def get_amount(self, instance): @@ -335,31 +334,31 @@ def get_discount_percent(self, instance): def get_company(self, instance): """Get the company id if any""" coupon_version = self._get_coupon_version(instance) - if coupon_version: + if coupon_version: # noqa: RET503 company = coupon_version.payment_version.company - if company: + if company: # noqa: RET503 return company.name def get_payment_type(self, instance): """Get the payment type""" coupon_version = self._get_coupon_version(instance) - if coupon_version: + if coupon_version: # noqa: RET503 payment_type = coupon_version.payment_version.payment_type - if payment_type: + if payment_type: # noqa: RET503 return payment_type def get_payment_transaction(self, instance): """Get the payment transaction id if any""" coupon_version = self._get_coupon_version(instance) - if coupon_version: + if coupon_version: # noqa: RET503 payment_transaction = coupon_version.payment_version.payment_transaction - if payment_transaction: + if payment_transaction: # noqa: RET503 return payment_transaction def get_coupon_code(self, instance): """Get the coupon code used for the order if any""" coupon_version = self._get_coupon_version(instance) - if coupon_version: + if coupon_version: # noqa: RET503 return coupon_version.coupon.coupon_code def get_order_type(self, instance): @@ -444,5 +443,5 @@ def get_hubspot_serializer(obj: object) -> serializers.ModelSerializer: elif isinstance(obj, models.Product): serializer_class = ProductSerializer else: - raise NotImplementedError("Not a supported class") + raise NotImplementedError("Not a supported class") # noqa: EM101 return serializer_class(obj) diff --git a/hubspot_xpro/serializers_test.py b/hubspot_xpro/serializers_test.py index 8bbaf8a31..1c1163e9c 100644 --- a/hubspot_xpro/serializers_test.py +++ b/hubspot_xpro/serializers_test.py @@ -1,7 +1,6 @@ """ Tests for hubspot_xpro serializers """ -# pylint: disable=unused-argument, redefined-outer-name from decimal import Decimal @@ -35,16 +34,15 @@ format_product_name, ) - pytestmark = [pytest.mark.django_db] @pytest.mark.parametrize( - "text_id, expected", + "text_id, expected", # noqa: PT006 [ - ["course-v1:xPRO+SysEngxNAV+R1", "Run 1"], - ["course-v1:xPRO+SysEngxNAV+R10", "Run 10"], - ["course-v1:xPRO+SysEngxNAV", "course-v1:xPRO+SysEngxNAV"], + ["course-v1:xPRO+SysEngxNAV+R1", "Run 1"], # noqa: PT007 + ["course-v1:xPRO+SysEngxNAV+R10", "Run 10"], # noqa: PT007 + ["course-v1:xPRO+SysEngxNAV", "course-v1:xPRO+SysEngxNAV"], # noqa: PT007 ], ) def test_serialize_product(text_id, expected): @@ -111,10 +109,10 @@ def test_serialize_order(settings, hubspot_order, status): @pytest.mark.parametrize( - "discount_type, amount", + "discount_type, amount", # noqa: PT006 [ - [DISCOUNT_TYPE_PERCENT_OFF, Decimal(0.75)], - [DISCOUNT_TYPE_DOLLARS_OFF, Decimal(75)], + [DISCOUNT_TYPE_PERCENT_OFF, Decimal(0.75)], # noqa: PT007 + [DISCOUNT_TYPE_DOLLARS_OFF, Decimal(75)], # noqa: PT007 ], ) def test_serialize_order_with_coupon(settings, hubspot_order, discount_type, amount): @@ -237,27 +235,30 @@ def test_serialize_b2b_order_with_coupon(settings, client, mocker): order = B2BOrder.objects.first() discount = round(Decimal(coupon.discount_percent) * 100, 2) serialized_data = B2BOrderToDealSerializer(instance=order).data - assert serialized_data == { - "dealname": f"{B2B_ORDER_PREFIX}-{order.id}", - "dealstage": ORDER_STATUS_MAPPING[order.status], - "discount_amount": discount.to_eng_string(), - "amount": order.total_price.to_eng_string(), - "closedate": ( - int(order.updated_on.timestamp() * 1000) - if order.status == Order.FULFILLED - else None - ), - "coupon_code": coupon.coupon_code, - "company": coupon.company.name, - "payment_type": None, - "payment_transaction": None, - "num_seats": num_seats, - "discount_percent": round( - Decimal(coupon.discount_percent) * 100, 2 - ).to_eng_string(), - "discount_type": DISCOUNT_TYPE_PERCENT_OFF, # B2B Orders only support percent-off discounts - "status": order.status, - "order_type": ORDER_TYPE_B2B, - "pipeline": settings.HUBSPOT_PIPELINE_ID, - "unique_app_id": f"{settings.MITOL_HUBSPOT_API_ID_PREFIX}-{B2B_ORDER_PREFIX}-{order.id}", - } + assert ( + serialized_data + == { + "dealname": f"{B2B_ORDER_PREFIX}-{order.id}", + "dealstage": ORDER_STATUS_MAPPING[order.status], + "discount_amount": discount.to_eng_string(), + "amount": order.total_price.to_eng_string(), + "closedate": ( + int(order.updated_on.timestamp() * 1000) + if order.status == Order.FULFILLED + else None + ), + "coupon_code": coupon.coupon_code, + "company": coupon.company.name, + "payment_type": None, + "payment_transaction": None, + "num_seats": num_seats, + "discount_percent": round( + Decimal(coupon.discount_percent) * 100, 2 + ).to_eng_string(), + "discount_type": DISCOUNT_TYPE_PERCENT_OFF, # B2B Orders only support percent-off discounts + "status": order.status, + "order_type": ORDER_TYPE_B2B, + "pipeline": settings.HUBSPOT_PIPELINE_ID, + "unique_app_id": f"{settings.MITOL_HUBSPOT_API_ID_PREFIX}-{B2B_ORDER_PREFIX}-{order.id}", + } + ) diff --git a/hubspot_xpro/task_helpers.py b/hubspot_xpro/task_helpers.py index a86b5b01c..c40418f3d 100644 --- a/hubspot_xpro/task_helpers.py +++ b/hubspot_xpro/task_helpers.py @@ -1,4 +1,4 @@ -""" Task helper functions for ecommerce """ +"""Task helper functions for ecommerce""" from django.conf import settings from ecommerce.models import Order diff --git a/hubspot_xpro/tasks.py b/hubspot_xpro/tasks.py index c58fac240..b0c1fd24b 100644 --- a/hubspot_xpro/tasks.py +++ b/hubspot_xpro/tasks.py @@ -4,13 +4,12 @@ import logging import time from math import ceil -from typing import List, Tuple import celery from django.conf import settings from django.contrib.contenttypes.models import ContentType from hubspot.crm.associations import BatchInputPublicAssociation, PublicAssociation -from hubspot.crm.objects import BatchInputSimplePublicObjectInput, ApiException +from hubspot.crm.objects import ApiException, BatchInputSimplePublicObjectInput from mitol.common.decorators import single_task from mitol.common.utils import chunks from mitol.hubspot_api.api import HubspotApi, HubspotAssociationType, HubspotObjectType @@ -25,13 +24,10 @@ from mitxpro.celery import app from users.models import User - log = logging.getLogger() -def task_obj_lock( - func_name: str, args: List[object], kwargs: dict # pylint:disable=unused-argument -) -> str: +def task_obj_lock(func_name: str, args: list[object], kwargs: dict) -> str: """ Determine a task lock name for a specific task function and object id @@ -49,7 +45,7 @@ def task_obj_lock( elif kwargs: # Assumes there is one key:value, for the object id # For tasks where this isn't true, a different function should be used - return f"{func_name}_{list(kwargs.values())[0]}" + return f"{func_name}_{list(kwargs.values())[0]}" # noqa: RUF015 else: return func_name @@ -68,8 +64,8 @@ def max_concurrent_chunk_size(obj_count: int) -> int: def batched_chunks( - hubspot_type: str, batch_ids: List[int or (int, str)] -) -> List[List[int or str]]: + hubspot_type: str, batch_ids: list[int or (int, str)] +) -> list[list[int or str]]: """ If list of ids exceed max allowed in a batch API call, chunk them up @@ -100,7 +96,7 @@ def sync_failed_contacts(chunk: list[int]) -> list[int]: try: api.sync_contact_with_hubspot(user_id) time.sleep(settings.HUBSPOT_TASK_DELAY / 1000) - except ApiException: + except ApiException: # noqa: PERF203 failed_ids.append(user_id) return failed_ids @@ -226,7 +222,7 @@ def sync_b2b_deal_with_hubspot(order_id: int) -> str: retry_jitter=True, ) @raise_429 -def batch_upsert_hubspot_deals_chunked(ids: List[int]): +def batch_upsert_hubspot_deals_chunked(ids: list[int]): """ Batch sync hubspot deals with matching Order ids @@ -251,7 +247,7 @@ def batch_upsert_hubspot_deals_chunked(ids: List[int]): retry_jitter=True, ) @raise_429 -def batch_upsert_hubspot_b2b_deals_chunked(ids: List[int]) -> List[str]: +def batch_upsert_hubspot_b2b_deals_chunked(ids: list[int]) -> list[str]: """ Batch sync hubspot deals with matching B2BOrder ids @@ -269,7 +265,7 @@ def batch_upsert_hubspot_b2b_deals_chunked(ids: List[int]) -> List[str]: @app.task(bind=True) -def batch_upsert_hubspot_deals(self, create: bool): +def batch_upsert_hubspot_deals(self, create: bool): # noqa: FBT001 """ Batch create/update deals in hubspot @@ -292,7 +288,7 @@ def batch_upsert_hubspot_deals(self, create: bool): @app.task(bind=True) -def batch_upsert_hubspot_b2b_deals(self, create: bool): +def batch_upsert_hubspot_b2b_deals(self, create: bool): # noqa: FBT001 """ Batch create/update b2b deals in hubspot @@ -325,8 +321,8 @@ def batch_upsert_hubspot_b2b_deals(self, create: bool): ) @raise_429 def batch_create_hubspot_objects_chunked( - hubspot_type: str, ct_model_name: str, object_ids: List[int] -) -> List[str]: + hubspot_type: str, ct_model_name: str, object_ids: list[int] +) -> list[str]: """ Batch create or update a list of hubspot objects, no associations @@ -393,8 +389,8 @@ def batch_create_hubspot_objects_chunked( ) @raise_429 def batch_update_hubspot_objects_chunked( - hubspot_type: str, ct_model_name: str, object_ids: List[Tuple[int, str]] -) -> List[str]: + hubspot_type: str, ct_model_name: str, object_ids: list[tuple[int, str]] +) -> list[str]: """ Batch create or update hubspot objects, no associations @@ -443,13 +439,13 @@ def batch_update_hubspot_objects_chunked( @app.task(bind=True) -def batch_upsert_hubspot_objects( # pylint:disable=too-many-arguments +def batch_upsert_hubspot_objects( # noqa: PLR0913 self, hubspot_type: str, model_name: str, app_label: str, - create: bool = True, - object_ids: List[int] = None, + create: bool = True, # noqa: FBT001, FBT002 + object_ids: list[int] = None, # noqa: RUF013 ): """ Batch create or update objects in hubspot, no associations (so ideal for contacts and products) @@ -467,7 +463,7 @@ def batch_upsert_hubspot_objects( # pylint:disable=too-many-arguments ).values_list("object_id", "hubspot_id") unsynced_object_ids = ( content_type.model_class() - .objects.exclude(id__in=[id[0] for id in synced_object_ids]) + .objects.exclude(id__in=[id[0] for id in synced_object_ids]) # noqa: A001 .values_list("id", flat=True) ) object_ids = sorted(unsynced_object_ids if create else synced_object_ids) @@ -497,7 +493,7 @@ def batch_upsert_hubspot_objects( # pylint:disable=too-many-arguments retry_jitter=True, ) @raise_429 -def batch_upsert_associations_chunked(order_ids: List[int]): +def batch_upsert_associations_chunked(order_ids: list[int]): """ Upsert batches of deal-contact and line-deal associations @@ -531,8 +527,8 @@ def batch_upsert_associations_chunked(order_ids: List[int]): ) ) if ( - len(contact_associations_batch) == 100 - or len(line_associations_batch) == 100 + len(contact_associations_batch) == 100 # noqa: PLR2004 + or len(line_associations_batch) == 100 # noqa: PLR2004 or idx == deal_count - 1 ): hubspot_client.crm.associations.batch_api.create( @@ -555,7 +551,7 @@ def batch_upsert_associations_chunked(order_ids: List[int]): @app.task(bind=True) -def batch_upsert_associations(self, order_ids: List[int] = None): +def batch_upsert_associations(self, order_ids: list[int] = None): # noqa: RUF013 """ Upsert chunked batches of deal-contact and line-deal associations diff --git a/hubspot_xpro/tasks_test.py b/hubspot_xpro/tasks_test.py index 7d45be2ba..c2e13345d 100644 --- a/hubspot_xpro/tasks_test.py +++ b/hubspot_xpro/tasks_test.py @@ -1,7 +1,6 @@ """ Tests for hubspot_xpro tasks """ -# pylint: disable=redefined-outer-name from decimal import Decimal from math import ceil @@ -9,11 +8,11 @@ from django.contrib.contenttypes.models import ContentType from faker import Faker from hubspot.crm.associations import BatchInputPublicAssociation, PublicAssociation -from hubspot.crm.objects import BatchInputSimplePublicObjectInput, ApiException +from hubspot.crm.objects import ApiException, BatchInputSimplePublicObjectInput from mitol.hubspot_api.api import HubspotAssociationType, HubspotObjectType +from mitol.hubspot_api.exceptions import TooManyRequestsException from mitol.hubspot_api.factories import HubspotObjectFactory, SimplePublicObjectFactory from mitol.hubspot_api.models import HubspotObject -from mitol.hubspot_api.exceptions import TooManyRequestsException from b2b_ecommerce.factories import B2BOrderFactory from b2b_ecommerce.models import B2BOrder @@ -29,7 +28,6 @@ from hubspot_xpro.tasks import task_obj_lock from users.factories import UserFactory - pytestmark = [pytest.mark.django_db] fake = Faker() @@ -56,7 +54,8 @@ def test_task_functions(mocker, task_func): @pytest.mark.parametrize("task_func", SYNC_FUNCTIONS) @pytest.mark.parametrize( - "status, expected_error", [[429, TooManyRequestsException], [500, ApiException]] + "status, expected_error", # noqa: PT006 + [[429, TooManyRequestsException], [500, ApiException]], # noqa: PT007 ) def test_task_functions_error(mocker, task_func, status, expected_error): """These task functions should return the expected exception class""" @@ -205,11 +204,9 @@ def test_batch_update_hubspot_objects_chunked(mocker, id_count): """batch_update_hubspot_objects_chunked should make expected api calls and args""" contacts = UserFactory.create_batch(id_count) mock_ids = sorted( - list( - zip( - [contact.id for contact in contacts], - [f"10001{i}" for i in range(id_count)], - ) + zip( + [contact.id for contact in contacts], + [f"10001{i}" for i in range(id_count)], ) ) mock_hubspot_api = mocker.patch("hubspot_xpro.tasks.HubspotApi") @@ -241,7 +238,8 @@ def test_batch_update_hubspot_objects_chunked(mocker, id_count): @pytest.mark.parametrize( - "status, expected_error", [[429, TooManyRequestsException], [500, ApiException]] + "status, expected_error", # noqa: PT006 + [[429, TooManyRequestsException], [500, ApiException]], # noqa: PT007 ) def test_batch_update_hubspot_objects_chunked_error(mocker, status, expected_error): """batch_update_hubspot_objects_chunked raise expected exception""" @@ -295,7 +293,8 @@ def test_batch_create_hubspot_objects_chunked(mocker, id_count): @pytest.mark.parametrize( - "status, expected_error", [[429, TooManyRequestsException], [500, ApiException]] + "status, expected_error", # noqa: PT006 + [[429, TooManyRequestsException], [500, ApiException]], # noqa: PT007 ) def test_batch_create_hubspot_objects_chunked_error(mocker, status, expected_error): """batch_create_hubspot_objects_chunked raise expected exception""" @@ -318,9 +317,7 @@ def test_batch_create_hubspot_objects_chunked_error(mocker, status, expected_err mock_sync_contact.assert_any_call(item) -def test_batch_upsert_associations( - settings, mocker, mocked_celery -): # pylint:disable=unused-argument +def test_batch_upsert_associations(settings, mocker, mocked_celery): """ batch_upsert_associations should call batch_upsert_associations_chunked w/correct lists of ids """ @@ -402,13 +399,13 @@ def test_batch_upsert_associations_chunked(mocker): @pytest.mark.parametrize( - "func_name,args,kwargs,result", + "func_name,args,kwargs,result", # noqa: PT006 [ - ["func1", [2345], None, "func1_2345"], - ["func2", None, {"order_id": 5678}, "func2_5678"], - ["func2a", [], {"user_id": 5678}, "func2a_5678"], - ["func3", None, None, "func3"], - ["func3a", None, {}, "func3a"], + ["func1", [2345], None, "func1_2345"], # noqa: PT007 + ["func2", None, {"order_id": 5678}, "func2_5678"], # noqa: PT007 + ["func2a", [], {"user_id": 5678}, "func2a_5678"], # noqa: PT007 + ["func3", None, None, "func3"], # noqa: PT007 + ["func3a", None, {}, "func3a"], # noqa: PT007 ], ) def test_task_obj_lock(func_name, args, kwargs, result): diff --git a/localdev/seed/api.py b/localdev/seed/api.py index 9b6148b38..68ca65ce1 100644 --- a/localdev/seed/api.py +++ b/localdev/seed/api.py @@ -1,108 +1,108 @@ """Functions/classes for adding and removing seed data""" import datetime -import os import json -from types import SimpleNamespace +import os from collections import defaultdict, namedtuple +from types import SimpleNamespace -from django.core.exceptions import ImproperlyConfigured from django.conf import settings from django.contrib.contenttypes.models import ContentType -from wagtail.models import Page +from django.core.exceptions import ImproperlyConfigured from rest_framework.exceptions import ValidationError +from wagtail.models import Page +from cms.api import configure_wagtail, get_home_page +from cms.models import ( + CourseIndexPage, + CoursePage, + ProgramIndexPage, + ProgramPage, + ProgramProductPage, + ResourcePage, +) from courses.constants import CONTENT_TYPE_MODEL_COURSERUN, DEFAULT_PLATFORM_NAME from courses.models import ( - Program, Course, CourseRun, CourseRunEnrollment, CourseRunEnrollmentAudit, CourseTopic, + Platform, + Program, ProgramEnrollment, ProgramEnrollmentAudit, - Platform, ) from courseware.api import create_oauth_application, delete_oauth_application +from ecommerce.api import create_coupons +from ecommerce.models import ( + Basket, + BasketItem, + Company, + CouponEligibility, + CouponPayment, + CouponPaymentVersion, + CouponRedemption, + CouponSelection, + CourseRunSelection, + Line, + Order, + OrderAudit, + Product, + ProductCouponAssignment, + ProductVersion, + Receipt, +) from localdev.seed.serializers import ( - ProgramSerializer, - CourseSerializer, - CourseRunSerializer, CompanySerializer, + CourseRunSerializer, + CourseSerializer, + ProgramSerializer, ) from mitxpro.utils import ( dict_without_keys, filter_dict_by_key_set, - get_field_names, first_or_none, + get_field_names, has_equal_properties, ) -from cms.models import ( - ProgramPage, - CoursePage, - ProgramProductPage, - ResourcePage, - CourseIndexPage, - ProgramIndexPage, -) -from cms.api import get_home_page, configure_wagtail -from ecommerce.api import create_coupons -from ecommerce.models import ( - Product, - ProductVersion, - CouponEligibility, - CouponSelection, - CouponRedemption, - Order, - OrderAudit, - Line, - Receipt, - Basket, - BasketItem, - CourseRunSelection, - ProductCouponAssignment, - Company, - CouponPaymentVersion, - CouponPayment, -) # ROUGH EXPECTED FORMAT FOR SEED DATA FILE: -# { +# { # noqa: ERA001, RUF100 # "programs": [ -# { +# { # noqa: ERA001, RUF100 # ...mixed Program and ProgramPage properties... # ...optional "_product" key pointing to dict of ProductVersion properties... -# } +# } # noqa: ERA001, RUF100 # ], # "courses": [ -# { +# { # noqa: ERA001, RUF100 # ...mixed Course and CoursePage properties... # ...optional "program" key pointing to a parent Program title... # ...optional "topics" key pointing to CourseTopics that should be set for the Course... # "course_runs": [ # ...CourseRun properties... # ...optional "_product" key pointing to dict of ProductVersion properties... -# ] -# } +# ] # noqa: ERA001, RUF100 +# } # noqa: ERA001, RUF100 # ], # "resource_pages": [ -# { +# { # noqa: ERA001, RUF100 # ...ResourcePage properties... -# } +# } # noqa: ERA001, RUF100 # ], -# "companies": [ ...Company properties... ], +# "companies": [ ...Company properties... ], # noqa: ERA001 # "coupons": [ -# { -# "name": ...CouponPayment name..., +# { # noqa: ERA001, RUF100 +# "name": ...CouponPayment name..., # noqa: ERA001, RUF100 # ...parameters for the "create_coupon" ecommerce method... # ...optional "_courseruns" key pointing to course runs to make product coupons for... # ...optional "_company" key pointing to a company name... -# } -# ] -# } +# } # noqa: ERA001, RUF100 +# ] # noqa: ERA001, RUF100 +# } # noqa: ERA001, RUF100 -SEED_DATA_FILE_PATH = os.path.join( +SEED_DATA_FILE_PATH = os.path.join( # noqa: PTH118 settings.BASE_DIR, "localdev/seed/resources/seed_data.json" ) REQUIRED_VOUCHER_SETTINGS = [ @@ -124,7 +124,7 @@ def get_raw_seed_data_from_file(): """Loads raw seed data from our seed data file""" - with open(SEED_DATA_FILE_PATH) as f: + with open(SEED_DATA_FILE_PATH) as f: # noqa: PTH123 return json.loads(f.read()) @@ -136,8 +136,7 @@ def get_courseware_page_parent(courseware_page_obj): index_page_cls = ProgramIndexPage else: return None - page_specific = Page.objects.get(id=index_page_cls.objects.first().id).specific - return page_specific + return Page.objects.get(id=index_page_cls.objects.first().id).specific def delete_wagtail_pages(page_cls, filter_dict): @@ -155,7 +154,7 @@ def delete_wagtail_pages(page_cls, filter_dict): base_pages_qset.delete() return ( num_pages, - {page_cls._meta.label: num_pages}, # pylint: disable=protected-access + {page_cls._meta.label: num_pages}, # noqa: SLF001 ) @@ -204,15 +203,15 @@ def check_settings(): # check in settings if not getattr(settings, variable): missing.append(variable) - except AttributeError: + except AttributeError: # noqa: PERF203 missing.append(variable) if missing: raise ImproperlyConfigured( - "Missing required voucher settings: {}".format(missing) + "Missing required voucher settings: {}".format(missing) # noqa: EM103, UP032 ) -SeedDataSpec = namedtuple("SeedDataSpec", ["model_cls", "data", "parent"]) +SeedDataSpec = namedtuple("SeedDataSpec", ["model_cls", "data", "parent"]) # noqa: PYI024 class SeedResult: @@ -253,7 +252,7 @@ def add_invalid(self, model_cls, field_value, exc): exc (Exception): The exception encountered while trying to save """ if isinstance(exc, ValidationError): - first_error_field = list(exc.detail.keys())[0] + first_error_field = list(exc.detail.keys())[0] # noqa: RUF015 exc_message = str(exc.detail[first_error_field][0]) else: exc_message = str(exc) @@ -313,12 +312,12 @@ def __init__(self): @classmethod def seed_prefixed(cls, value): """Returns the same value with a prefix that indicates seed data""" - return " ".join([cls.SEED_DATA_PREFIX, value]) + return " ".join([cls.SEED_DATA_PREFIX, value]) # noqa: FLY002 @classmethod def is_seed_value(cls, value): """Returns True of the given value matches the seeded value format""" - return value.startswith("{} ".format(cls.SEED_DATA_PREFIX)) + return value.startswith(f"{cls.SEED_DATA_PREFIX} ") def _seeded_field_and_value(self, model_cls, data): """ @@ -334,7 +333,7 @@ def _get_existing_seeded_qset(self, model_cls, data): """Returns a qset of seed data objects for some model class""" field_name, seeded_value = self._seeded_field_and_value(model_cls, data) if model_cls == CouponPaymentVersion: - field_name = "payment__{}".format(field_name) + field_name = f"payment__{field_name}" return model_cls.objects.filter(**{field_name: seeded_value}) def _deserialize_courseware_object(self, serializer_cls, data): @@ -350,7 +349,7 @@ def _deserialize_courseware_object(self, serializer_cls, data): "live": True, # Use every property in 'data' that corresponds to a model property **filter_for_model_fields(model_cls, data), - **{seeded_field_name: seeded_value}, + **{seeded_field_name: seeded_value}, # noqa: PIE800 } existing_qset = model_cls.objects.filter(**{seeded_field_name: seeded_value}) @@ -372,10 +371,9 @@ def _deserialize_courseware_object(self, serializer_cls, data): return courseware_obj def _get_topic_objects(self, topics): - topic_objs = [ + return [ CourseTopic.objects.get_or_create(name=topic["name"])[0] for topic in topics ] - return topic_objs def _deserialize_product(self, courseware_obj, product_data): """ @@ -438,14 +436,14 @@ def _deserialize_coupon(self, data): **{ **dict_without_keys(data, "_company", "_courseruns"), **dates, - **{ + **{ # noqa: PIE800 seeded_field_name: seeded_value, "product_ids": course_run_product_ids, "company_id": company_id, }, } ) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # noqa: BLE001 self.seed_result.add_invalid(model_cls, seeded_value, exc) return None else: @@ -598,7 +596,7 @@ def _delete_cms_resource_page(self, resource_page): __, deleted_type_counts = delete_wagtail_pages( ResourcePage, {"id": existing_obj.id} ) - self.seed_result.add_deleted(deleted_type_counts) + self.seed_result.add_deleted(deleted_type_counts) # noqa: RET503 def iter_seed_data(self, raw_data): """ @@ -672,7 +670,7 @@ def get_platform_id(platform_name): model_cls=CouponPayment, data=raw_coupon_data, parent=None ) - def create_seed_data(self, raw_data): + def create_seed_data(self, raw_data): # noqa: C901 """ Iterate over all objects described in the seed data spec, add/update them one-by-one, and return the results """ diff --git a/localdev/seed/api_test.py b/localdev/seed/api_test.py index 3418392db..fcc8509f8 100644 --- a/localdev/seed/api_test.py +++ b/localdev/seed/api_test.py @@ -1,10 +1,10 @@ """Seed data API tests""" -# pylint: disable=unused-argument, redefined-outer-name from types import SimpleNamespace + import pytest -from courses.models import Program, Course, CourseRun, CourseTopic, Platform -from cms.models import ProgramPage, CoursePage, ResourcePage +from cms.models import CoursePage, ProgramPage, ResourcePage +from courses.models import Course, CourseRun, CourseTopic, Platform, Program from ecommerce.models import Product, ProductVersion from ecommerce.test_utils import unprotect_version_tables from localdev.seed.api import SeedDataLoader, get_raw_seed_data_from_file @@ -29,7 +29,7 @@ def test_seed_prefix(seeded): """ # Test helper functions seeded_value = seeded.loader.seed_prefixed("Some Title") - assert seeded_value == "{} Some Title".format(SeedDataLoader.SEED_DATA_PREFIX) + assert seeded_value == f"{SeedDataLoader.SEED_DATA_PREFIX} Some Title" assert seeded.loader.is_seed_value(seeded_value) is True # Test saved object titles assert ( diff --git a/localdev/seed/management/commands/delete_seed_data.py b/localdev/seed/management/commands/delete_seed_data.py index 4cde7a992..75bc7c2cb 100644 --- a/localdev/seed/management/commands/delete_seed_data.py +++ b/localdev/seed/management/commands/delete_seed_data.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from courses.models import Program, Course, CourseRun +from courses.models import Course, CourseRun, Program from localdev.seed.api import SeedDataLoader, get_raw_seed_data_from_file User = get_user_model() @@ -29,7 +29,7 @@ class Command(BaseCommand): help = "Deletes seeded data based on raw seed data file or specific properties" - def add_arguments(self, parser): + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "--type", type=str, @@ -42,15 +42,15 @@ def add_arguments(self, parser): help="The title of the seeded object you want to delete", ) - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" seed_data_loader = SeedDataLoader() if options["type"]: if not options["title"]: - raise CommandError("'title' must be specified with 'type'") + raise CommandError("'title' must be specified with 'type'") # noqa: EM101 if not seed_data_loader.is_seed_value(options["title"]): raise CommandError( - "This command should only be run to delete seeded objects. Seeded objects are indicated " + "This command should only be run to delete seeded objects. Seeded objects are indicated " # noqa: EM103 "by a prefixed title (example: {})".format( seed_data_loader.seed_prefixed("Some Title") ) @@ -78,4 +78,4 @@ def handle(self, *args, **options): else: self.stdout.write(self.style.SUCCESS("RESULTS")) for k, v in results.report.items(): - self.stdout.write("{}: {}".format(k, v)) + self.stdout.write("{}: {}".format(k, v)) # noqa: UP032 diff --git a/localdev/seed/management/commands/seed_data.py b/localdev/seed/management/commands/seed_data.py index 89e7d9ff3..667296583 100644 --- a/localdev/seed/management/commands/seed_data.py +++ b/localdev/seed/management/commands/seed_data.py @@ -12,7 +12,7 @@ class Command(BaseCommand): help = "Creates or updates seed data based on a raw seed data file" - def handle(self, *args, **options): + def handle(self, *args, **options): # noqa: ARG002 """Handle command execution""" seed_data_loader = SeedDataLoader() raw_seed_data = get_raw_seed_data_from_file() @@ -23,4 +23,4 @@ def handle(self, *args, **options): else: self.stdout.write(self.style.SUCCESS("RESULTS")) for k, v in results.report.items(): - self.stdout.write("{}: {}".format(k, v)) + self.stdout.write("{}: {}".format(k, v)) # noqa: UP032 diff --git a/mail/api.py b/mail/api.py index d9d8184c7..f8c928afb 100644 --- a/mail/api.py +++ b/mail/api.py @@ -18,10 +18,10 @@ # send the emails send_messages(messages) """ -from email.utils import formataddr import logging import re from collections import namedtuple +from email.utils import formataddr from anymail.message import AnymailMessage from bs4 import BeautifulSoup @@ -36,7 +36,7 @@ log = logging.getLogger() -EmailMetadata = namedtuple("EmailMetadata", ["tags", "user_variables"]) +EmailMetadata = namedtuple("EmailMetadata", ["tags", "user_variables"]) # noqa: PYI024 class UserMessageProps: @@ -130,12 +130,10 @@ def render_email_templates(template_name, context): Returns: (str, str, str): tuple of the templates for subject, text_body, html_body """ - subject_text = render_to_string( - "{}/subject.txt".format(template_name), context - ).rstrip() + subject_text = render_to_string(f"{template_name}/subject.txt", context).rstrip() context.update({"subject": subject_text}) - html_text = render_to_string("{}/body.html".format(template_name), context) + html_text = render_to_string(f"{template_name}/body.html", context) # pynliner internally uses bs4, which we can now modify the inlined version into a plaintext version # this avoids parsing the body twice in bs4 @@ -191,7 +189,7 @@ def message_for_recipient(recipient, context, template_name): Returns: django.core.mail.EmailMultiAlternatives: email message with rendered content """ - return list(messages_for_recipients([(recipient, context)], template_name))[0] + return list(messages_for_recipients([(recipient, context)], template_name))[0] # noqa: RUF015 def build_messages(template_name, recipients, extra_context, metadata=None): @@ -270,9 +268,7 @@ def build_message(connection, template_name, recipient, context, metadata=None): if metadata.tags: esp_extra.update({"o:tag": metadata.tags}) if metadata.user_variables: - esp_extra.update( - {"v:{}".format(k): v for k, v in metadata.user_variables.items()} - ) + esp_extra.update({f"v:{k}": v for k, v in metadata.user_variables.items()}) if esp_extra: msg.esp_extra = esp_extra msg.attach_alternative(html_body, "text/html") @@ -289,7 +285,7 @@ def send_messages(messages): for msg in messages: try: msg.send() - except: # pylint: disable=bare-except + except: # noqa: E722, PERF203 log.exception("Error sending email '%s' to %s", msg.subject, msg.to) @@ -318,7 +314,7 @@ def validate_email_addresses(email_addresses): for email in email_addresses: try: validate_email(email) - except ValidationError: + except ValidationError: # noqa: PERF203 invalid_emails.add(email) if invalid_emails: raise MultiEmailValidationError(invalid_emails) diff --git a/mail/api_test.py b/mail/api_test.py index b67bf3189..8277f707a 100644 --- a/mail/api_test.py +++ b/mail/api_test.py @@ -1,18 +1,19 @@ """API tests""" from email.utils import formataddr + import pytest from mail.api import ( + EmailMetadata, + UserMessageProps, + build_message, + build_messages, + build_user_specific_messages, context_for_user, - safe_format_recipients, + messages_for_recipients, render_email_templates, + safe_format_recipients, send_messages, - messages_for_recipients, - build_messages, - build_user_specific_messages, - build_message, - UserMessageProps, - EmailMetadata, ) from mitxpro.test_utils import any_instance_of from users.factories import UserFactory @@ -22,7 +23,7 @@ @pytest.fixture -def email_settings(settings): +def email_settings(settings): # noqa: PT004 """Default settings for email tests""" settings.MAILGUN_RECIPIENT_OVERRIDE = None @@ -99,7 +100,7 @@ def test_messages_for_recipients(): for user, msg in zip(users, messages): assert user.email in str(msg.to[0]) - assert msg.subject == "Welcome {}".format(user.name) + assert msg.subject == f"Welcome {user.name}" def test_build_messages(mocker): diff --git a/mail/templatetags/calculate_tax.py b/mail/templatetags/calculate_tax.py index f1712ef77..21ed90fb2 100644 --- a/mail/templatetags/calculate_tax.py +++ b/mail/templatetags/calculate_tax.py @@ -3,7 +3,6 @@ from django import template - register = template.Library() diff --git a/mail/templatetags/calculate_tax_total.py b/mail/templatetags/calculate_tax_total.py index 51a425bd1..20b490c7e 100644 --- a/mail/templatetags/calculate_tax_total.py +++ b/mail/templatetags/calculate_tax_total.py @@ -5,7 +5,6 @@ from mail.templatetags.calculate_tax import calculate_tax - register = template.Library() diff --git a/mail/verification_api.py b/mail/verification_api.py index c12171f8c..3b5ef6b8f 100644 --- a/mail/verification_api.py +++ b/mail/verification_api.py @@ -6,12 +6,15 @@ from affiliate.api import get_affiliate_code_from_request from affiliate.constants import AFFILIATE_QS_PARAM from mail import api -from mail.constants import EMAIL_VERIFICATION, EMAIL_CHANGE_EMAIL +from mail.constants import EMAIL_CHANGE_EMAIL, EMAIL_VERIFICATION def send_verification_email( - strategy, backend, code, partial_token -): # pylint: disable=unused-argument + strategy, + backend, # noqa: ARG001 + code, + partial_token, +): """ Sends a verification email for python-social-auth diff --git a/mail/verification_api_test.py b/mail/verification_api_test.py index 861782cf4..43dbbf1c2 100644 --- a/mail/verification_api_test.py +++ b/mail/verification_api_test.py @@ -1,9 +1,9 @@ """Tests for verification_api""" from urllib.parse import quote_plus -import pytest -from django.core.mail import EmailMessage +import pytest from django.contrib.sessions.middleware import SessionMiddleware +from django.core.mail import EmailMessage from django.shortcuts import reverse from django.test.client import RequestFactory from social_core.backends.email import EmailAuth diff --git a/mail/views.py b/mail/views.py index 228365492..d6e72efde 100644 --- a/mail/views.py +++ b/mail/views.py @@ -8,14 +8,13 @@ from mail import api from mail.constants import ( - EMAIL_VERIFICATION, - EMAIL_PW_RESET, - EMAIL_BULK_ENROLL, EMAIL_B2B_RECEIPT, + EMAIL_BULK_ENROLL, + EMAIL_PW_RESET, + EMAIL_VERIFICATION, ) from mail.forms import EmailDebuggerForm - EMAIL_DEBUG_EXTRA_CONTEXT = { EMAIL_PW_RESET: {"uid": "abc-def", "token": "abc-def"}, EMAIL_VERIFICATION: {"confirmation_url": "http://www.example.com/confirm/url"}, diff --git a/manage.py b/manage.py index 1966442f3..9acf62b6e 100755 --- a/manage.py +++ b/manage.py @@ -12,7 +12,7 @@ from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( - "Couldn't import Django. Are you sure it's installed and " + "Couldn't import Django. Are you sure it's installed and " # noqa: EM101 "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc diff --git a/maxmind/api.py b/maxmind/api.py index 2db4875f8..653b04ca8 100644 --- a/maxmind/api.py +++ b/maxmind/api.py @@ -9,7 +9,6 @@ from maxmind import models - MAXMIND_CSV_COUNTRY_LOCATIONS_LITE = "geolite2-country-locations" MAXMIND_CSV_COUNTRY_BLOCKS_IPV4_LITE = "geolite2-country-ipv4" MAXMIND_CSV_COUNTRY_BLOCKS_IPV6_LITE = "geolite2-country-ipv6" @@ -34,11 +33,11 @@ def import_maxmind_database(import_type: str, import_filename: str) -> None: """ if import_type not in MAXMIND_CSV_TYPES: - raise Exception(f"Invalid database type {import_type}") + raise Exception(f"Invalid database type {import_type}") # noqa: EM102, TRY002 rows = [] - with open(import_filename) as import_raw: + with open(import_filename) as import_raw: # noqa: PTH123 dr = csv.DictReader(import_raw) for row in dr: @@ -131,15 +130,16 @@ def import_maxmind_database(import_type: str, import_filename: str) -> None: ) if len(rows) == 0: - raise Exception("No rows to process - file format invalid?") + raise Exception("No rows to process - file format invalid?") # noqa: EM101, TRY002 with transaction.atomic(): if import_type == MAXMIND_CSV_COUNTRY_LOCATIONS_LITE: models.Geoname.objects.all().delete() models.Geoname.objects.bulk_create(rows) - elif import_type == MAXMIND_CSV_COUNTRY_BLOCKS_IPV4_LITE: - models.NetBlock.objects.filter(is_ipv6=False).delete() - elif import_type == MAXMIND_CSV_COUNTRY_BLOCKS_IPV6_LITE: + elif import_type in ( + MAXMIND_CSV_COUNTRY_BLOCKS_IPV4_LITE, + MAXMIND_CSV_COUNTRY_BLOCKS_IPV6_LITE, + ): models.NetBlock.objects.filter(is_ipv6=False).delete() if import_type in [ diff --git a/maxmind/api_test.py b/maxmind/api_test.py index 5079fbc68..4e722f6ce 100644 --- a/maxmind/api_test.py +++ b/maxmind/api_test.py @@ -10,18 +10,17 @@ from maxmind.api import ip_to_country_code from maxmind.factories import NetBlockIPv4Factory, NetBlockIPv6Factory - fake = faker.Factory.create() @pytest.mark.django_db() @pytest.mark.parametrize( - "v4,in_block", + "v4,in_block", # noqa: PT006 [ - [True, True], - [True, False], - [False, True], - [False, False], + [True, True], # noqa: PT007 + [True, False], # noqa: PT007 + [False, True], # noqa: PT007 + [False, False], # noqa: PT007 ], ) def test_ipv4_lookup(v4, in_block): diff --git a/maxmind/factories.py b/maxmind/factories.py index 3be62f7c1..800f8ef41 100644 --- a/maxmind/factories.py +++ b/maxmind/factories.py @@ -9,7 +9,6 @@ from maxmind import models - fake = faker.Faker() diff --git a/maxmind/management/commands/import_maxmind_data.py b/maxmind/management/commands/import_maxmind_data.py index fbe3ebd8c..5b0373e33 100644 --- a/maxmind/management/commands/import_maxmind_data.py +++ b/maxmind/management/commands/import_maxmind_data.py @@ -3,9 +3,10 @@ API call that does.) """ -from django.core.management import BaseCommand, CommandError from os import path +from django.core.management import BaseCommand, CommandError + from maxmind import api @@ -16,7 +17,7 @@ class Command(BaseCommand): help = "Imports the MaxMind GeoLite2 databases." - def add_arguments(self, parser) -> None: + def add_arguments(self, parser) -> None: # noqa: D102 parser.add_argument( "file", type=str, @@ -30,9 +31,9 @@ def add_arguments(self, parser) -> None: help="The type of file being imported.", ) - def handle(self, *args, **kwargs): - if not path.exists(kwargs["file"]): - raise CommandError(f"Input file {kwargs['file']} does not exist.") + def handle(self, *args, **kwargs): # noqa: ARG002, D102 + if not path.exists(kwargs["file"]): # noqa: PTH110 + raise CommandError(f"Input file {kwargs['file']} does not exist.") # noqa: EM102 api.import_maxmind_database(kwargs["filetype"], kwargs["file"]) diff --git a/maxmind/models.py b/maxmind/models.py index abed826e4..47cb3866b 100644 --- a/maxmind/models.py +++ b/maxmind/models.py @@ -22,13 +22,13 @@ class Geoname(models.Model): continent_name = models.TextField() country_iso_code = models.CharField(max_length=2) country_name = models.TextField() - subdivision_1_iso_code = models.CharField(max_length=3, blank=True, null=True) - subdivision_1_name = models.TextField(blank=True, null=True) - subdivision_2_iso_code = models.CharField(max_length=3, blank=True, null=True) - subdivision_2_name = models.TextField(blank=True, null=True) - city_name = models.TextField(blank=True, null=True) + subdivision_1_iso_code = models.CharField(max_length=3, blank=True, null=True) # noqa: DJ001 + subdivision_1_name = models.TextField(blank=True, null=True) # noqa: DJ001 + subdivision_2_iso_code = models.CharField(max_length=3, blank=True, null=True) # noqa: DJ001 + subdivision_2_name = models.TextField(blank=True, null=True) # noqa: DJ001 + city_name = models.TextField(blank=True, null=True) # noqa: DJ001 metro_code = models.IntegerField(blank=True, null=True) - time_zone = models.TextField(blank=True, null=True) + time_zone = models.TextField(blank=True, null=True) # noqa: DJ001 is_in_european_union = models.BooleanField(blank=True, null=True, default=False) class Meta: @@ -79,7 +79,7 @@ class NetBlock(models.Model): represented_country_geoname_id = models.BigIntegerField(blank=True, null=True) is_anonymous_proxy = models.BooleanField(default=False, null=True, blank=True) is_satellite_provider = models.BooleanField(default=False, null=True, blank=True) - postal_code = models.CharField(max_length=10, null=True, blank=True) + postal_code = models.CharField(max_length=10, null=True, blank=True) # noqa: DJ001 latitude = models.DecimalField( max_digits=16, decimal_places=6, blank=True, null=True ) diff --git a/mitxpro/admin.py b/mitxpro/admin.py index b37a8c941..aa40f6137 100644 --- a/mitxpro/admin.py +++ b/mitxpro/admin.py @@ -5,14 +5,14 @@ class AuditableModelAdmin(admin.ModelAdmin): """A ModelAdmin which will save and log""" - def save_model(self, request, obj, form, change): + def save_model(self, request, obj, form, change): # noqa: ARG002, D102 obj.save_and_log(request.user) class SingletonModelAdmin(admin.ModelAdmin): """A ModelAdmin which enforces a singleton model""" - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002 """Overridden method - prevent adding an object if one already exists""" return self.model.objects.count() == 0 @@ -42,7 +42,7 @@ def _join_and_dedupe(existing_field_names, field_names_to_add): field for field in field_names_to_add if field not in existing_field_names ) - def get_list_display(self, request): + def get_list_display(self, request): # noqa: D102 list_display = tuple(super().get_list_display(request) or ()) added_fields = () if self.include_timestamps_in_list: @@ -51,12 +51,12 @@ def get_list_display(self, request): added_fields += ("created_on",) return self._join_and_dedupe(list_display, added_fields) - def get_readonly_fields(self, request, obj=None): + def get_readonly_fields(self, request, obj=None): # noqa: D102 readonly_fields = tuple(super().get_readonly_fields(request, obj=obj) or ()) if obj is None: return readonly_fields return self._join_and_dedupe(readonly_fields, ("created_on", "updated_on")) - def get_exclude(self, request, obj=None): + def get_exclude(self, request, obj=None): # noqa: D102 exclude = tuple(super().get_exclude(request, obj=obj) or ()) return self._join_and_dedupe(exclude, ("created_on", "updated_on")) diff --git a/mitxpro/apps.py b/mitxpro/apps.py index 30c812919..d86ca066d 100644 --- a/mitxpro/apps.py +++ b/mitxpro/apps.py @@ -9,7 +9,7 @@ class RootConfig(AppConfig): name = "mitxpro" - def ready(self): + def ready(self): # noqa: D102 from mitol.common import envs envs.validate() diff --git a/mitxpro/celery_utils.py b/mitxpro/celery_utils.py index f0c44224f..f309beeb5 100644 --- a/mitxpro/celery_utils.py +++ b/mitxpro/celery_utils.py @@ -18,7 +18,7 @@ def __init__(self, run_every=None, offset=None): self._apply_offset = offset is not None super().__init__(run_every=self._run_every + (offset or timedelta(seconds=0))) - def is_due(self, last_run_at): + def is_due(self, last_run_at): # noqa: D102 retval = super().is_due(last_run_at) if self._apply_offset is not None and retval.is_due: self._apply_offset = False diff --git a/mitxpro/context_processors.py b/mitxpro/context_processors.py index 3d173cc76..a0af1a388 100644 --- a/mitxpro/context_processors.py +++ b/mitxpro/context_processors.py @@ -3,10 +3,8 @@ """ from django.conf import settings -# pylint: disable=unused-argument - -def api_keys(request): +def api_keys(request): # noqa: ARG001 """ Pass a `APIKEYS` dictionary into the template context, which holds IDs and secret keys for the various APIs used in this project. @@ -19,7 +17,7 @@ def api_keys(request): } -def configuration_context(request): +def configuration_context(request): # noqa: ARG001 """ Configuration context for django templates. """ diff --git a/mitxpro/migrations/0001_create_default_robots_txt.py b/mitxpro/migrations/0001_create_default_robots_txt.py index 5fa89d7c7..ccd26c711 100644 --- a/mitxpro/migrations/0001_create_default_robots_txt.py +++ b/mitxpro/migrations/0001_create_default_robots_txt.py @@ -5,7 +5,7 @@ from django.db import migrations -def create_default_robots_txt(apps, schema_editor): +def create_default_robots_txt(apps, schema_editor): # noqa: D103 Site = apps.get_model("sites", "Site") Url = apps.get_model("robots", "Url") Rule = apps.get_model("robots", "Rule") @@ -15,7 +15,8 @@ def create_default_robots_txt(apps, schema_editor): # django.contrib.sites should be creating this, but it's in a delayed post-migration hook: # current_site, created = Site.objects.get_or_create( - pk=getattr(settings, "SITE_ID", 1), defaults=dict(domain=domain, name=domain) + pk=getattr(settings, "SITE_ID", 1), + defaults=dict(domain=domain, name=domain), # noqa: C408 ) url, _ = Url.objects.get_or_create(pattern="/") @@ -29,7 +30,6 @@ def create_default_robots_txt(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("sites", "0002_alter_domain_unique"), ("robots", "0001_initial")] operations = [ diff --git a/mitxpro/models.py b/mitxpro/models.py index 3347168e3..3ba0fbd22 100644 --- a/mitxpro/models.py +++ b/mitxpro/models.py @@ -2,21 +2,20 @@ Common model classes """ import copy -from typing import Iterable +from typing import Iterable # noqa: UP035 from django.conf import settings -from django.db import models from django.core.exceptions import ValidationError +from django.db import models, transaction from django.db.models import ( + PROTECT, DateTimeField, ForeignKey, Manager, Model, - PROTECT, prefetch_related_objects, ) from django.db.models.query import QuerySet -from django.db import transaction from mitxpro.utils import now_in_utc @@ -31,7 +30,7 @@ def update(self, **kwargs): Automatically update updated_on timestamp when .update(). This is because .update() does not go through .save(), thus will not auto_now, because it happens on the database level without loading objects into memory. - """ + """ # noqa: D402 if "updated_on" not in kwargs: kwargs["updated_on"] = now_in_utc() return super().update(**kwargs) @@ -61,7 +60,7 @@ class TimestampedModel(Model): """ objects = TimestampedModelManager() - created_on = DateTimeField(auto_now_add=True) # UTC + created_on = DateTimeField(auto_now_add=True) # UTC # noqa: DJ012 updated_on = DateTimeField(auto_now=True) # UTC class Meta: @@ -138,7 +137,7 @@ def save_and_log(self, acting_user, *args, **kwargs): if before_obj is not None: before_dict = before_obj.to_dict() - audit_kwargs = dict( + audit_kwargs = dict( # noqa: C408 acting_user=acting_user, data_before=before_dict, data_after=self.to_dict() ) audit_class = self.get_audit_class() @@ -149,12 +148,16 @@ def save_and_log(self, acting_user, *args, **kwargs): class SingletonModel(Model): """Model class for models representing tables that should only have a single record""" - def save( - self, force_insert=False, force_update=False, using=None, update_fields=None + def save( # noqa: D102 + self, + force_insert=False, # noqa: FBT002 + force_update=False, # noqa: FBT002 + using=None, + update_fields=None, ): if force_insert and self._meta.model.objects.count() > 0: raise ValidationError( - "Only one {} object should exist. Update the existing object instead " + "Only one {} object should exist. Update the existing object instead " # noqa: EM103 "of creating a new one.".format(self.__class__.__name__) ) return super().save( @@ -164,7 +167,7 @@ def save( update_fields=update_fields, ) - class Meta: + class Meta: # noqa: DJ012 abstract = True @@ -201,17 +204,15 @@ def prefetch_generic_related(self, content_type_field, model_lookups): qs = self._chain() for model_classes, lookups in model_lookups.items(): - # pylint: disable=isinstance-second-argument-not-valid-type - model_classes = ( + model_classes = ( # noqa: PLW2901 model_classes if isinstance(model_classes, Iterable) else [model_classes] ) for model_cls in model_classes: key = (content_type_field, model_cls) - # pylint: disable=protected-access - qs._prefetch_generic_related_lookups[key] = [ - *qs._prefetch_generic_related_lookups.get(key, []), + qs._prefetch_generic_related_lookups[key] = [ # noqa: SLF001 + *qs._prefetch_generic_related_lookups.get(key, []), # noqa: SLF001 *lookups, ] @@ -237,9 +238,8 @@ def _fetch_all(self): def _clone(self): """Clone the queryset""" - # pylint: disable=protected-access c = super()._clone() - c._prefetch_generic_related_lookups = copy.deepcopy( + c._prefetch_generic_related_lookups = copy.deepcopy( # noqa: SLF001 self._prefetch_generic_related_lookups ) return c diff --git a/mitxpro/models_test.py b/mitxpro/models_test.py index 8224a1457..d681e6f58 100644 --- a/mitxpro/models_test.py +++ b/mitxpro/models_test.py @@ -1,37 +1,34 @@ """Tests for mitxpro models""" -from random import sample, randint, choice +from random import choice, randint, sample from types import SimpleNamespace +import pytest from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import connection, models -import pytest from mitxpro.models import PrefetchGenericQuerySet pytestmark = pytest.mark.django_db -# pylint: disable=redefined-outer-name - @pytest.fixture(scope="module") -@pytest.mark.usefixtures("django_db_setup") def test_models(django_db_blocker): """Fixture that creates test-only models""" with django_db_blocker.unblock(): - class SecondLevel1(models.Model): + class SecondLevel1(models.Model): # noqa: DJ008 """Test-only model""" - class SecondLevel2(models.Model): + class SecondLevel2(models.Model): # noqa: DJ008 """Test-only model""" - class FirstLevel1(models.Model): + class FirstLevel1(models.Model): # noqa: DJ008 """Test-only model""" second_level = models.ForeignKey(SecondLevel1, on_delete=models.CASCADE) - class FirstLevel2(models.Model): + class FirstLevel2(models.Model): # noqa: DJ008 """Test-only model""" second_levels = models.ManyToManyField(SecondLevel2) @@ -39,12 +36,12 @@ class FirstLevel2(models.Model): class TestModelQuerySet(PrefetchGenericQuerySet): """Test-only QuerySet""" - class Root(models.Model): + class Root(models.Model): # noqa: DJ008 """Test-only model""" objects = TestModelQuerySet.as_manager() - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # noqa: DJ012 object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") @@ -56,7 +53,7 @@ class Root(models.Model): editor.create_model(FirstLevel2) editor.create_model(Root) - yield SimpleNamespace( + return SimpleNamespace( SecondLevel1=SecondLevel1, SecondLevel2=SecondLevel2, FirstLevel1=FirstLevel1, @@ -69,7 +66,7 @@ def test_prefetch_generic_related(django_assert_num_queries, test_models): """Test prefetch over a many-to-one relation""" second_levels1 = [test_models.SecondLevel1.objects.create() for _ in range(5)] first_levels1 = [ - test_models.FirstLevel1.objects.create(second_level=choice(second_levels1)) + test_models.FirstLevel1.objects.create(second_level=choice(second_levels1)) # noqa: S311 for _ in range(10) ] @@ -77,14 +74,14 @@ def test_prefetch_generic_related(django_assert_num_queries, test_models): first_levels2 = [] for _ in range(10): first_level = test_models.FirstLevel2.objects.create() - first_level.second_levels.set(sample(second_levels2, randint(1, 3))) + first_level.second_levels.set(sample(second_levels2, randint(1, 3))) # noqa: S311 first_levels2.append(first_level) roots = [ - test_models.Root.objects.create(content_object=choice(first_levels1)) + test_models.Root.objects.create(content_object=choice(first_levels1)) # noqa: S311 for _ in range(5) ] + [ - test_models.Root.objects.create(content_object=choice(first_levels2)) + test_models.Root.objects.create(content_object=choice(first_levels2)) # noqa: S311 for _ in range(5) ] diff --git a/mitxpro/permissions.py b/mitxpro/permissions.py index 66ea8ed3f..3693d0bf3 100644 --- a/mitxpro/permissions.py +++ b/mitxpro/permissions.py @@ -12,11 +12,6 @@ def has_object_permission(self, request, view, obj): """ owner_field = getattr(view, "owner_field", None) - if owner_field is None: - # if no owner_field is specified, the object itself is compared - owner = obj - else: - # otherwise we lookup the owner by the specified field - owner = getattr(obj, owner_field) - + # if no owner_field is specified, the object itself is compared + owner = obj if owner_field is None else getattr(obj, owner_field) return owner == request.user diff --git a/mitxpro/sentry.py b/mitxpro/sentry.py index 25c8f05c7..d5d265823 100644 --- a/mitxpro/sentry.py +++ b/mitxpro/sentry.py @@ -1,11 +1,10 @@ """Sentry setup and configuration""" -from celery.exceptions import WorkerLostError import sentry_sdk +from celery.exceptions import WorkerLostError from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import LoggingIntegration - # these errors occur when a shutdown is happening (usually caused by a SIGTERM) SHUTDOWN_ERRORS = (WorkerLostError, SystemExit) @@ -40,7 +39,7 @@ def init_sentry(*, dsn, environment, version, log_level, heroku_app_name): log_level (str): the sentry log level heroku_app_name (str or None): the name of the heroku review app """ - sentry_sdk.init( # pylint: disable=abstract-class-instantiated + sentry_sdk.init( dsn=dsn, environment=environment, release=version, diff --git a/mitxpro/serializers.py b/mitxpro/serializers.py index f13a094af..75b1ac40d 100644 --- a/mitxpro/serializers.py +++ b/mitxpro/serializers.py @@ -15,23 +15,23 @@ class AppContextSerializer(serializers.Serializer): release_version = serializers.SerializerMethodField() features = serializers.SerializerMethodField() - def get_features(self, request): + def get_features(self, request): # noqa: ARG002 """Returns a dictionary of features""" return {} - def get_release_version(self, request): + def get_release_version(self, request): # noqa: ARG002 """Returns a dictionary of features""" return settings.VERSION - def get_gtm_tracking_id(self, request): + def get_gtm_tracking_id(self, request): # noqa: ARG002 """Returns the GTM container ID""" return settings.GTM_TRACKING_ID - def get_ga_tracking_id(self, request): + def get_ga_tracking_id(self, request): # noqa: ARG002 """Returns a dictionary of features""" return settings.GA_TRACKING_ID - def get_environment(self, request): + def get_environment(self, request): # noqa: ARG002 """Returns a dictionary of features""" return settings.ENVIRONMENT @@ -49,5 +49,5 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.read_only = False - def to_internal_value(self, data): + def to_internal_value(self, data): # noqa: D102 return data diff --git a/mitxpro/settings.py b/mitxpro/settings.py index d7be999ba..4788aba17 100644 --- a/mitxpro/settings.py +++ b/mitxpro/settings.py @@ -1,15 +1,14 @@ -# pylint: disable=too-many-lines """ Django settings for mitxpro. """ import logging import os import platform -from datetime import timedelta, timezone +from datetime import timedelta from urllib.parse import urljoin, urlparse +from zoneinfo import ZoneInfo import dj_database_url -from zoneinfo import ZoneInfo from celery.schedules import crontab from django.core.exceptions import ImproperlyConfigured from mitol.common.envs import ( @@ -20,14 +19,13 @@ get_string, import_settings_modules, ) -from mitol.common.settings.webpack import * # pylint: disable=wildcard-import,unused-wildcard-import -from mitol.digitalcredentials.settings import * # pylint: disable=wildcard-import,unused-wildcard-import +from mitol.common.settings.webpack import * # noqa: F403 +from mitol.digitalcredentials.settings import * # noqa: F403 from redbeat import RedBeatScheduler from mitxpro.celery_utils import OffsettingSchedule from mitxpro.sentry import init_sentry - VERSION = "0.144.0" ENVIRONMENT = get_string( @@ -57,7 +55,7 @@ ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # noqa: PTH100, PTH120 SITE_BASE_URL = get_string( name="MITXPRO_BASE_URL", @@ -118,7 +116,7 @@ "DEFAULT": { "CACHE": not DEBUG, "BUNDLE_DIR_NAME": "bundles/", - "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"), + "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"), # noqa: PTH118 "POLL_INTERVAL": 0.1, "TIMEOUT": None, "IGNORE": [r".+\.hot-update\.+", r".+\.js\.map"], @@ -202,7 +200,7 @@ INSTALLED_APPS += ("localdev.seed",) -if not WEBPACK_DISABLE_LOADER_STATS: +if not WEBPACK_DISABLE_LOADER_STATS: # noqa: F405 INSTALLED_APPS += ("webpack_loader",) MIDDLEWARE = ( @@ -248,7 +246,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates")], + "DIRS": [os.path.join(BASE_DIR, "templates")], # noqa: PTH118 "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -273,7 +271,7 @@ DEFAULT_DATABASE_CONFIG = dj_database_url.parse( get_string( name="DATABASE_URL", - default="sqlite:///{0}".format(os.path.join(BASE_DIR, "db.sqlite3")), + default="sqlite:///{0}".format(os.path.join(BASE_DIR, "db.sqlite3")), # noqa: PTH118, UP030 description="The connection url to the Postgres database", required=True, write_app_json=False, @@ -427,7 +425,8 @@ ) if CLOUDFRONT_DIST: STATIC_URL = urljoin( - "https://{dist}.cloudfront.net".format(dist=CLOUDFRONT_DIST), STATIC_URL + "https://{dist}.cloudfront.net".format(dist=CLOUDFRONT_DIST), # noqa: UP032 + STATIC_URL, ) STATICFILES_FINDERS = [ @@ -436,7 +435,7 @@ ] STATIC_ROOT = "staticfiles" -STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) +STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) # noqa: PTH118 # Important to define this so DEBUG works properly @@ -538,10 +537,7 @@ description="E-mail to send 500 reports to.", required=True, ) -if ADMIN_EMAIL != "": - ADMINS = (("Admins", ADMIN_EMAIL),) -else: - ADMINS = () +ADMINS = (("Admins", ADMIN_EMAIL),) if ADMIN_EMAIL != "" else () # Logging configuration LOG_LEVEL = get_string( @@ -573,7 +569,7 @@ "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, "formatters": { "verbose": { - "format": ( + "format": ( # noqa: UP032 "[%(asctime)s] %(levelname)s %(process)d [%(name)s] " "%(filename)s:%(lineno)d - " "[{hostname}] - %(message)s" @@ -679,13 +675,13 @@ not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY or not AWS_STORAGE_BUCKET_NAME ): raise ImproperlyConfigured( - "You have enabled S3 support, but are missing one of " + "You have enabled S3 support, but are missing one of " # noqa: EM101 "AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or " "AWS_STORAGE_BUCKET_NAME" ) if MITXPRO_USE_S3: if CLOUDFRONT_DIST: - AWS_S3_CUSTOM_DOMAIN = "{dist}.cloudfront.net".format(dist=CLOUDFRONT_DIST) + AWS_S3_CUSTOM_DOMAIN = "{dist}.cloudfront.net".format(dist=CLOUDFRONT_DIST) # noqa: UP032 DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" FEATURES = get_features() @@ -944,9 +940,9 @@ # required for migrations -OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "oauth2_provider.AccessToken" +OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "oauth2_provider.AccessToken" # noqa: S105 OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application" -OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth2_provider.RefreshToken" +OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth2_provider.RefreshToken" # noqa: S105 OAUTH2_PROVIDER = { # this is the list of available scopes @@ -980,7 +976,7 @@ # Relative URL to be used by Djoser for the link in the password reset email # (see: http://djoser.readthedocs.io/en/stable/settings.html#password-reset-confirm-url) -PASSWORD_RESET_CONFIRM_URL = "password_reset/confirm/{uid}/{token}/" +PASSWORD_RESET_CONFIRM_URL = "password_reset/confirm/{uid}/{token}/" # noqa: S105 # mitol-django-common MITOL_COMMON_USER_FACTORY = "users.factories.UserFactory" @@ -1078,7 +1074,7 @@ if DEBUG: INSTALLED_APPS += ("debug_toolbar",) # it needs to be enabled before other middlewares - MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + MIDDLEWARE + MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware",) + MIDDLEWARE # noqa: RUF005 # Cybersource CYBERSOURCE_ACCESS_KEY = get_string( @@ -1435,8 +1431,7 @@ default=None, description="Verification method for digital credentials", ) -# pylint:disable=fixme -# FIXME: This setting is meant to be temporary and it should be removed once we decide to support digital credentials +# TODO: This setting is meant to be temporary and it should be removed once we decide to support digital credentials # noqa: FIX002, TD002, TD003 # for all courses/programs. DIGITAL_CREDENTIALS_SUPPORTED_RUNS = get_delimited_list( name="DIGITAL_CREDENTIALS_SUPPORTED_RUNS", diff --git a/mitxpro/settings_test.py b/mitxpro/settings_test.py index 538252522..90b9a6c4c 100644 --- a/mitxpro/settings_test.py +++ b/mitxpro/settings_test.py @@ -11,9 +11,6 @@ from mitol.common import envs -# pylint: disable=redefined-outer-name, unused-argument - - # NOTE: this is temporarily inlined here until I can stabilize the test upstream in the library def test_app_json_modified(): """ @@ -26,7 +23,7 @@ def test_app_json_modified(): import json import logging - with open("app.json") as app_json_file: + with open("app.json") as app_json_file: # noqa: PTH123 app_json = json.load(app_json_file) generated_app_json = envs.generate_app_json() @@ -56,7 +53,6 @@ def _get(): return vars(sys.modules["mitxpro.settings"]) def _patch(overrides): - for key, value in overrides.items(): monkeypatch.setenv(key, value) diff --git a/mitxpro/templatetags/js_interop.py b/mitxpro/templatetags/js_interop.py index 6253ccc92..ee7f7f806 100644 --- a/mitxpro/templatetags/js_interop.py +++ b/mitxpro/templatetags/js_interop.py @@ -15,7 +15,7 @@ def js_settings(context): request = context["request"] js_settings_json = json.dumps(get_js_settings(request)) - return mark_safe( + return mark_safe( # noqa: S308 f"""""" diff --git a/mitxpro/templatetags/latest_notification.py b/mitxpro/templatetags/latest_notification.py index 5ad0d5502..247c50313 100644 --- a/mitxpro/templatetags/latest_notification.py +++ b/mitxpro/templatetags/latest_notification.py @@ -1,6 +1,7 @@ """Templatetags for rendering site notification""" from django import template + from cms.models import SiteNotification register = template.Library() diff --git a/mitxpro/test_utils.py b/mitxpro/test_utils.py index 34e8b97ce..397e3c741 100644 --- a/mitxpro/test_utils.py +++ b/mitxpro/test_utils.py @@ -1,17 +1,17 @@ """Testing utils""" import abc +import csv import json -from contextlib import contextmanager +import tempfile import traceback +from contextlib import contextmanager from unittest.mock import Mock -import csv -import tempfile import pytest from django.contrib.sessions.middleware import SessionMiddleware from django.core.files.uploadedfile import SimpleUploadedFile -from rest_framework.renderers import JSONRenderer from requests.exceptions import HTTPError +from rest_framework.renderers import JSONRenderer def any_instance_of(*cls): @@ -25,7 +25,7 @@ def any_instance_of(*cls): AnyInstanceOf: dynamic class type with the desired equality """ - class AnyInstanceOf(metaclass=abc.ABCMeta): + class AnyInstanceOf(metaclass=abc.ABCMeta): # noqa: B024 """Dynamic class type for __eq__ in terms of isinstance""" def __eq__(self, other): @@ -43,7 +43,7 @@ def assert_not_raises(): yield except AssertionError: raise - except Exception: # pylint: disable=broad-except + except Exception: # noqa: BLE001 pytest.fail(f"An exception was not raised: {traceback.format_exc()}") @@ -90,7 +90,7 @@ class MockHttpError(HTTPError): def __init__(self, *args, **kwargs): response = MockResponse(content={"bad": "response"}, status_code=400) - super().__init__(*args, **{**kwargs, **{"response": response}}) + super().__init__(*args, **{**kwargs, **{"response": response}}) # noqa: PIE800 def drf_datetime(dt): @@ -130,11 +130,11 @@ def create_tempfile_csv(rows_iter): SimpleUploadedFile: A temporary CSV file with the given contents """ f = tempfile.NamedTemporaryFile(suffix=".csv", delete=False) - with open(f.name, "w", encoding="utf8", newline="") as f: + with open(f.name, "w", encoding="utf8", newline="") as f: # noqa: PTH123 writer = csv.writer(f, delimiter=",") for row in rows_iter: writer.writerow(row) - with open(f.name, "r") as user_csv: + with open(f.name, "r") as user_csv: # noqa: PTH123, UP015 return SimpleUploadedFile( f.name, user_csv.read().encode("utf8"), content_type="application/csv" ) @@ -196,8 +196,8 @@ def update_namespace(tuple_to_update, **updates): Union([types.namedtuple, typing.NamedTuple]): The updated namespace """ return tuple_to_update.__class__( - **{ # pylint: disable=protected-access - **tuple_to_update._asdict(), # pylint: disable=protected-access + **{ + **tuple_to_update._asdict(), **updates, } ) diff --git a/mitxpro/test_utils_test.py b/mitxpro/test_utils_test.py index 03ff1cc5f..8e4a4f53e 100644 --- a/mitxpro/test_utils_test.py +++ b/mitxpro/test_utils_test.py @@ -4,11 +4,11 @@ import pytest from mitxpro.test_utils import ( - any_instance_of, - assert_not_raises, MockResponse, PickleableMock, + any_instance_of, assert_drf_json_equal, + assert_not_raises, ) @@ -36,15 +36,15 @@ def test_assert_not_raises_exception(mocker): # Here there be dragons fail_mock = mocker.patch("pytest.fail", autospec=True) with assert_not_raises(): - raise TabError() + raise TabError assert fail_mock.called is True def test_assert_not_raises_failure(): """assert_not_raises should reraise an AssertionError""" - with pytest.raises(AssertionError): + with pytest.raises(AssertionError): # noqa: SIM117 with assert_not_raises(): - assert 1 == 2 + assert 1 == 2 # noqa: PLR0133 def test_assert_drf_json_equall(): @@ -55,16 +55,16 @@ def test_assert_drf_json_equall(): @pytest.mark.parametrize( - "content,expected_content,expected_json", + "content,expected_content,expected_json", # noqa: PT006 [ - ['{"test": "content"}', '{"test": "content"}', {"test": "content"}], - [{"test": "content"}, '{"test": "content"}', {"test": "content"}], - [["test", "content"], '["test", "content"]', ["test", "content"]], - [123, "123", 123], + ['{"test": "content"}', '{"test": "content"}', {"test": "content"}], # noqa: PT007 + [{"test": "content"}, '{"test": "content"}', {"test": "content"}], # noqa: PT007 + [["test", "content"], '["test", "content"]', ["test", "content"]], # noqa: PT007 + [123, "123", 123], # noqa: PT007 ], ) def test_mock_response(content, expected_content, expected_json): - """assert MockResponse returns correct values""" + """Assert MockResponse returns correct values""" response = MockResponse(content, 404) assert response.status_code == 404 assert response.content == expected_content @@ -73,4 +73,4 @@ def test_mock_response(content, expected_content, expected_json): def test_pickleable_mock(): """Tests that a mock can be pickled""" - pickle.dumps(PickleableMock(field_name=dict())) + pickle.dumps(PickleableMock(field_name=dict())) # noqa: C408 diff --git a/mitxpro/tests/app_tests.py b/mitxpro/tests/app_tests.py index 6e040fccf..fcd222023 100644 --- a/mitxpro/tests/app_tests.py +++ b/mitxpro/tests/app_tests.py @@ -4,10 +4,10 @@ def test_app_json_valid(): """Verify app.json is parsable and has some necessary keys""" - with open("app.json") as f: + with open("app.json") as f: # noqa: PTH123 config = json.load(f) - assert isinstance(config, dict) + assert isinstance(config, dict) # noqa: S101 for required_key in ["addons", "buildpacks", "env"]: - assert required_key in config + assert required_key in config # noqa: S101 diff --git a/mitxpro/tests/js_interop_test.py b/mitxpro/tests/js_interop_test.py index 8bf046c8f..456309934 100644 --- a/mitxpro/tests/js_interop_test.py +++ b/mitxpro/tests/js_interop_test.py @@ -11,7 +11,7 @@ def test_js_settings(mocker, rf): request = rf.get("/") context = Context({"request": request}) - template = Template(("{% load js_interop %}" "{% js_settings %}")) + template = Template("{% load js_interop %}" "{% js_settings %}") rendered_template = template.render(context) assert ( diff --git a/mitxpro/urls.py b/mitxpro/urls.py index 4913ec85d..450e57e0c 100644 --- a/mitxpro/urls.py +++ b/mitxpro/urls.py @@ -14,10 +14,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.conf import settings -from django.urls import include from django.conf.urls.static import static from django.contrib import admin -from django.urls import path, re_path +from django.urls import include, path, re_path from django.views.decorators.cache import cache_control from mitol.common.decorators import cache_control_max_age_jitter from oauth2_provider.urls import base_urlpatterns @@ -31,12 +30,15 @@ from mitxpro.views import ( AppContextView, cms_signin_redirect_to_site_signin, - handler404 as not_found_handler, - handler500 as server_error_handler, index, restricted, ) - +from mitxpro.views import ( + handler404 as not_found_handler, +) +from mitxpro.views import ( + handler500 as server_error_handler, +) WAGTAIL_IMG_CACHE_AGE = 31_536_000 # 1 year diff --git a/mitxpro/utils.py b/mitxpro/utils.py index f4cb6ad59..9b08aec87 100644 --- a/mitxpro/utils.py +++ b/mitxpro/utils.py @@ -1,12 +1,13 @@ """mitxpro utilities""" import csv import datetime -from enum import auto, Flag +import itertools import json import logging -import itertools -from urllib.parse import urlparse, urlunparse, ParseResult +from enum import Flag, auto +from urllib.parse import ParseResult, urlparse, urlunparse +import requests from django.conf import settings from django.core.serializers import serialize from django.db import models @@ -14,7 +15,6 @@ from django.http.response import HttpResponse from django.templatetags.static import static from rest_framework import status -import requests log = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class FeatureFlag(Flag): def ensure_trailing_slash(url): - """ensure a url has a trailing slash""" + """Ensure a url has a trailing slash""" return url if url.endswith("/") else url + "/" @@ -85,7 +85,7 @@ def now_in_utc(): return datetime.datetime.now(tz=datetime.timezone.utc) -def format_datetime_for_filename(datetime_object, include_time=False, include_ms=False): +def format_datetime_for_filename(datetime_object, include_time=False, include_ms=False): # noqa: FBT002 """ Formats a datetime object for use as part of a filename @@ -130,12 +130,12 @@ def dict_without_keys(d, *omitkeys): Returns: dict: A dict with omitted keys """ - return {key: d[key] for key in d.keys() if key not in omitkeys} + return {key: d[key] for key in d.keys() if key not in omitkeys} # noqa: SIM118 def filter_dict_by_key_set(dict_to_filter, key_set): """Takes a dictionary and returns a copy with only the keys that exist in the given set""" - return {key: dict_to_filter[key] for key in dict_to_filter.keys() if key in key_set} + return {key: dict_to_filter[key] for key in dict_to_filter.keys() if key in key_set} # noqa: SIM118 def serialize_model_object(obj): @@ -148,7 +148,7 @@ def serialize_model_object(obj): A representation of the model """ # serialize works on iterables so we need to wrap object in a list, then unwrap it - if obj: + if obj: # noqa: RET503 data = json.loads(serialize("json", [obj]))[0] serialized = data["fields"] serialized["id"] = data["pk"] @@ -167,8 +167,8 @@ def get_field_names(model): """ return [ field.name - for field in model._meta.get_fields() - if not field.auto_created # pylint: disable=protected-access + for field in model._meta.get_fields() # noqa: SLF001 + if not field.auto_created ] @@ -211,7 +211,7 @@ def find_object_with_matching_attr(iterable, attr_name, value): try: if getattr(item, attr_name) == value: return item - except AttributeError: + except AttributeError: # noqa: PERF203 pass return None @@ -225,7 +225,7 @@ def has_equal_properties(obj, property_dict): try: if getattr(obj, field) != value: return False - except AttributeError: + except AttributeError: # noqa: PERF203 return False return True @@ -453,9 +453,12 @@ class ValidateOnSaveMixin(models.Model): class Meta: abstract = True - def save( - self, force_insert=False, force_update=False, **kwargs - ): # pylint: disable=arguments-differ + def save( # noqa: D102 + self, + force_insert=False, # noqa: FBT002 + force_update=False, # noqa: FBT002 + **kwargs, + ): if not (force_insert or force_update): self.full_clean() super().save(force_insert=force_insert, force_update=force_update, **kwargs) @@ -554,7 +557,7 @@ def request_get_with_timeout_retry(url, retries): Raises: requests.exceptions.HTTPError: Raised if the response has a status code indicating an error """ - resp = requests.get(url) + resp = requests.get(url) # noqa: S113 # If there was a timeout (504), retry before giving up tries = 1 while resp.status_code == status.HTTP_504_GATEWAY_TIMEOUT and tries < retries: @@ -562,7 +565,7 @@ def request_get_with_timeout_retry(url, retries): log.warning( "GET request timed out (%s). Retrying for attempt %d...", url, tries ) - resp = requests.get(url) + resp = requests.get(url) # noqa: S113 resp.raise_for_status() return resp diff --git a/mitxpro/utils_test.py b/mitxpro/utils_test.py index c02eb1963..16d282e53 100644 --- a/mitxpro/utils_test.py +++ b/mitxpro/utils_test.py @@ -104,7 +104,7 @@ def test_format_datetime_for_filename(): """ Test that format_datetime_for_filename formats a datetime object to a string for use in a filename """ - dt = datetime.datetime( + dt = datetime.datetime( # noqa: DTZ001 year=2019, month=1, day=1, hour=20, minute=21, second=22, microsecond=100 ) assert format_datetime_for_filename(dt) == "20190101" @@ -158,10 +158,10 @@ def test_has_equal_properties(): """ obj = SimpleNamespace(a=1, b=2, c=3) assert has_equal_properties(obj, {}) is True - assert has_equal_properties(obj, dict(a=1, b=2)) is True - assert has_equal_properties(obj, dict(a=1, b=2, c=3)) is True - assert has_equal_properties(obj, dict(a=2)) is False - assert has_equal_properties(obj, dict(d=4)) is False + assert has_equal_properties(obj, dict(a=1, b=2)) is True # noqa: C408 + assert has_equal_properties(obj, dict(a=1, b=2, c=3)) is True # noqa: C408 + assert has_equal_properties(obj, dict(a=2)) is False # noqa: C408 + assert has_equal_properties(obj, dict(d=4)) is False # noqa: C408 def test_find_object_with_matching_attr(): @@ -211,11 +211,11 @@ def test_partition_to_lists(): @pytest.mark.parametrize( - "url, expected", + "url, expected", # noqa: PT006 [ - ["", ""], - ["http://url.com/url/here#other", "http://url.com/url/here#other"], - ["https://user:pass@sentry.io/12345", "https://user@sentry.io/12345"], + ["", ""], # noqa: PT007 + ["http://url.com/url/here#other", "http://url.com/url/here#other"], # noqa: PT007 + ["https://user:pass@sentry.io/12345", "https://user@sentry.io/12345"], # noqa: PT007 ], ) def test_remove_password_from_url(url, expected): @@ -329,7 +329,7 @@ def test_group_into_dict(): grouped by generated keys """ - class Car: # pylint: disable=missing-docstring + class Car: def __init__(self, make, model): self.make = make self.model = model @@ -355,8 +355,8 @@ def __init__(self, make, model): @pytest.mark.parametrize( - "price,expected", - [[Decimal("0"), "$0.00"], [Decimal("1234567.89"), "$1,234,567.89"]], + "price,expected", # noqa: PT006 + [[Decimal("0"), "$0.00"], [Decimal("1234567.89"), "$1,234,567.89"]], # noqa: PT007 ) def test_format_price(price, expected): """Format a decimal value into a price""" @@ -364,11 +364,11 @@ def test_format_price(price, expected): @pytest.mark.parametrize( - "content,content_type,exp_summary_content,exp_url_in_summary", + "content,content_type,exp_summary_content,exp_url_in_summary", # noqa: PT006 [ - ['{"bad": "response"}', "application/json", '{"bad": "response"}', False], - ["plain text", "text/plain", "plain text", False], - [ + ['{"bad": "response"}', "application/json", '{"bad": "response"}', False], # noqa: PT007 + ["plain text", "text/plain", "plain text", False], # noqa: PT007 + [ # noqa: PT007 "
HTML content
", "text/html; charset=utf-8", "(HTML body ignored)", @@ -395,11 +395,11 @@ def test_get_error_response_summary( @pytest.mark.parametrize( - "content,content_type,expected", + "content,content_type,expected", # noqa: PT006 [ - ['{"bad": "response"}', "application/json", True], - ["plain text", "text/plain", False], - ["
HTML content
", "text/html; charset=utf-8", False], + ['{"bad": "response"}', "application/json", True], # noqa: PT007 + ["plain text", "text/plain", False], # noqa: PT007 + ["
HTML content
", "text/html; charset=utf-8", False], # noqa: PT007 ], ) def test_is_json_response(content, content_type, expected): diff --git a/mitxpro/views.py b/mitxpro/views.py index 262ccd970..9604b0b39 100644 --- a/mitxpro/views.py +++ b/mitxpro/views.py @@ -17,7 +17,7 @@ from mitxpro.serializers import AppContextSerializer -def get_base_context(request): +def get_base_context(request): # noqa: ARG001 """ Returns the template context key/values needed for the base template and all templates that extend it """ @@ -30,13 +30,12 @@ def get_base_context(request): @csrf_exempt -def index(request, **kwargs): # pylint: disable=unused-argument +def index(request, **kwargs): # noqa: ARG001 """ The index view """ context = get_base_context(request) - # pylint: disable=too-many-boolean-expressions if request.method == "POST" and ( "auth_amount" in request.POST and "req_merchant_defined_data2" in request.POST @@ -62,7 +61,7 @@ def index(request, **kwargs): # pylint: disable=unused-argument return render(request, "index.html", context=context) -def handler404(request, exception): # pylint: disable=unused-argument +def handler404(request, exception): # noqa: ARG001 """404: NOT FOUND ERROR handler""" response = render_to_string( "404.html", request=request, context=get_base_context(request) @@ -78,7 +77,7 @@ def handler500(request): return HttpResponseServerError(response) -def cms_signin_redirect_to_site_signin(request): +def cms_signin_redirect_to_site_signin(request): # noqa: ARG001 """Redirect wagtail admin signin to site signin page""" return redirect_to_login(reverse("wagtailadmin_home"), login_url="/signin") @@ -97,6 +96,6 @@ class AppContextView(APIView): permission_classes = [] - def get(self, request, *args, **kwargs): # pylint: disable=unused-argument + def get(self, request, *args, **kwargs): # noqa: ARG002 """Read-only access""" return Response(AppContextSerializer(request).data) diff --git a/mitxpro/views_test.py b/mitxpro/views_test.py index 500b0d03d..4650979a9 100644 --- a/mitxpro/views_test.py +++ b/mitxpro/views_test.py @@ -1,9 +1,9 @@ """ Test end to end django views. """ +import pytest from django.test import Client from django.urls import reverse -import pytest from rest_framework import status pytestmark = [pytest.mark.django_db] diff --git a/mitxpro/wsgi.py b/mitxpro/wsgi.py index a88296084..044250586 100644 --- a/mitxpro/wsgi.py +++ b/mitxpro/wsgi.py @@ -12,4 +12,4 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mitxpro.settings") -application = get_wsgi_application() # pylint: disable=invalid-name +application = get_wsgi_application() diff --git a/poetry.lock b/poetry.lock index 611cbd8b0..68f5426cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -187,41 +187,6 @@ files = [ {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, ] -[[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "blessed" version = "1.20.0" @@ -1752,23 +1717,6 @@ files = [ [package.dependencies] six = "*" -[[package]] -name = "isort" -version = "4.3.21" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, - {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, -] - -[package.extras] -pipfile = ["pipreqs", "requirementslib"] -pyproject = ["toml"] -requirements = ["pip-api", "pipreqs"] -xdg-home = ["appdirs (>=1.4.0)"] - [[package]] name = "jedi" version = "0.19.1" @@ -2159,17 +2107,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = "*" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2302,17 +2239,6 @@ mitol-django-common = "*" pytz = ">=2020.4" requests = ">=2.20.0" -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "newrelic" version = "8.11.0" @@ -2411,17 +2337,6 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pdbpp" version = "0.10.3" @@ -2609,21 +2524,6 @@ docs = ["sphinx (>=4.4)", "sphinx-issues (>=3.0.1)", "sphinx-rtd-theme (>=1.0)"] tests = ["defusedxml", "numpy", "packaging", "pympler", "pytest"] tests-min = ["defusedxml", "packaging", "pytest"] -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - [[package]] name = "pluggy" version = "1.4.0" @@ -2883,59 +2783,6 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] -[[package]] -name = "pylint" -version = "2.7.4" -description = "python code static checker" -optional = false -python-versions = "~=3.6" -files = [ - {file = "pylint-2.7.4-py3-none-any.whl", hash = "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a"}, - {file = "pylint-2.7.4.tar.gz", hash = "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"}, -] - -[package.dependencies] -astroid = ">=2.5.2,<2.7" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.7" -toml = ">=0.7.1" - -[package.extras] -docs = ["python-docs-theme (==2020.12)", "sphinx (==3.5.1)"] - -[[package]] -name = "pylint-django" -version = "2.5.5" -description = "A Pylint plugin to help Pylint understand the Django web framework" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "pylint_django-2.5.5-py3-none-any.whl", hash = "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7"}, - {file = "pylint_django-2.5.5.tar.gz", hash = "sha256:2f339e4bf55776958283395c5139c37700c91bd5ef1d8251ef6ac88b5abbba9b"}, -] - -[package.dependencies] -pylint = ">=2.0,<4" -pylint-plugin-utils = ">=0.8" - -[package.extras] -with-django = ["Django (>=2.2)"] - -[[package]] -name = "pylint-plugin-utils" -version = "0.8.2" -description = "Utilities and helpers for writing Pylint plugins" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "pylint_plugin_utils-0.8.2-py3-none-any.whl", hash = "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507"}, - {file = "pylint_plugin_utils-0.8.2.tar.gz", hash = "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4"}, -] - -[package.dependencies] -pylint = ">=1.7" - [[package]] name = "pynacl" version = "1.3.0" @@ -3428,6 +3275,32 @@ files = [ {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, ] +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + [[package]] name = "s3transfer" version = "0.10.0" @@ -3698,17 +3571,6 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" diff --git a/pylintrc b/pylintrc deleted file mode 100644 index b98500737..000000000 --- a/pylintrc +++ /dev/null @@ -1,26 +0,0 @@ -[MASTER] -ignore=.git -ignore-paths=^.*/migrations/.*$, ^node_modules -load-plugins = pylint_django - -[BASIC] -# Allow django's urlpatterns, and our log preference -const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ -# Don't require docstrings for double-underscore methods, or for unittest support methods -no-docstring-rgx = __.*__$|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$|Params$ - -[TYPECHECK] -generated-members = - status_code -ignored-classes= - six, - six.moves, -ignored-modules= - lxml, - lxml.etree, - six, - six.moves, - pdftotext, - -[MESSAGES CONTROL] -disable = no-member, old-style-class, no-init, too-few-public-methods, abstract-method, invalid-name, too-many-ancestors, line-too-long, no-self-use, len-as-condition, no-else-return, cyclic-import, duplicate-code, inconsistent-return-statements, bad-continuation, import-outside-toplevel, raise-missing-from, superfluous-parens diff --git a/pyproject.toml b/pyproject.toml index c8a6ecb4f..39a14bff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,3 @@ -[tool.black] -py36 = true -include = '\.pyi?$' -exclude = ''' -/( - \.git - | \.mypy_cache - | \.tox - | \.venv - | build - | dist - | node_modules -)/ -''' - -[tool.isort] -profile = "black" -multi_line_output = 3 - [tool.poetry] name = "MITx Pro" version = "0.1.0" @@ -83,19 +64,15 @@ xmltodict = "^0.13.0" [tool.poetry.group.dev.dependencies] astroid = "2.6.6" -black = "22.12.0" bpython = "*" django-debug-toolbar = "*" factory-boy = "3.3.0" faker = "13.16.0" freezegun = "0.3.15" ipdb = "*" -isort = "4.3.21" nplusone = ">=0.8.1" pdbpp = "*" hypothesis = "4.23.4" -pylint = "2.7.4" -pylint-django = "2.5.5" pytest = "^7.4.4" pytest-cov = ">=2.6.1" pytest-django = "*" @@ -106,7 +83,118 @@ responses = "*" safety = "*" semantic-version = "*" wagtail-factories = "*" +ruff = "^0.1.11" [build-system] -requires = ["poetry-core^1.0.0"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +target-version = "py39" +line-length = 88 +select = [ + "A", # flake8-builtins + # "AIR", # Airflow + # "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + # "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + # "COM", # flake8-commas + # "CPY", # flake8-copyright + "D", # pydocstyle + "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # Pycodestyle Error + "EM", # flake8-errmsg + "ERA", # eradicate + "EXE", # flake8-executable + "F", # Pyflakes + "FA", # flake8-future-annotations + "FBT", # flake8-boolean-trap + "FIX", # flake8-fixme + "FLY", # flynt + # "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + # "NPY", # NumPy-specific rules + # "PD", # pandas-vet + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # Pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TCH", # flake8-type-checking + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # Pycodestyle Warning + "YTT", # flake8-2020 +] +ignore = [ + "B008", + "B905", + "D106", + "D104", + "D200", + "D202", + "D205", + "D301", + "D400", + "D401", + "E501", + "N803", + "N806", + "N999", + "PIE804", + "PT023", + "RET505", + "RET506", + "RET507", + "RET508", + "RUF012", + "UP007", + "A003", + "TRY003", + "INP001", + "D105", + "D107", + "PLR0912", + "ISC001", +] + +typing-modules = ["colour.hints"] + +[tool.ruff.pydocstyle] +convention = "pep257" + +[tool.ruff.flake8-quotes] +inline-quotes = "double" + +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false + +[tool.ruff.per-file-ignores] +"*_test.py" = ["ARG001", "E501", "S101", "PLR2004"] +"test_*.py" = ["ARG001", "E501", "S101", "PLR2004"] +"**/migrations/**" = ["ARG001", "D101", "D100"] diff --git a/repl.py b/repl.py index 9bf95f3bc..55b759d3c 100755 --- a/repl.py +++ b/repl.py @@ -4,15 +4,15 @@ import os if not os.environ.get("PYTHONSTARTUP"): - from subprocess import check_call import sys + from subprocess import check_call - base_dir = os.path.dirname(os.path.abspath(__file__)) + base_dir = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120 sys.exit( check_call( - [os.path.join(base_dir, "manage.py"), "shell", *sys.argv[1:]], - env={**os.environ, "PYTHONSTARTUP": os.path.join(base_dir, "repl.py")}, + [os.path.join(base_dir, "manage.py"), "shell", *sys.argv[1:]], # noqa: PTH118, S603 + env={**os.environ, "PYTHONSTARTUP": os.path.join(base_dir, "repl.py")}, # noqa: PTH118 ) ) @@ -20,9 +20,9 @@ from django.conf import settings for app in settings.INSTALLED_APPS: - try: - exec( # pylint: disable=exec-used - "from {app}.models import *".format(app=app) + try: # noqa: SIM105 + exec( # noqa: S102 + "from {app}.models import *".format(app=app) # noqa: UP032 ) - except ModuleNotFoundError: + except ModuleNotFoundError: # noqa: PERF203 pass diff --git a/sheets/__init__.py b/sheets/__init__.py index d835a5f92..e69de29bb 100644 --- a/sheets/__init__.py +++ b/sheets/__init__.py @@ -1 +0,0 @@ -# pylint: disable=missing-docstring,invalid-name diff --git a/sheets/admin.py b/sheets/admin.py index 566156c7f..cc3e3217f 100644 --- a/sheets/admin.py +++ b/sheets/admin.py @@ -8,11 +8,11 @@ from mitxpro.admin import SingletonModelAdmin from sheets.models import ( CouponGenerationRequest, - RefundRequest, DeferralRequest, + FileWatchRenewalAttempt, GoogleApiAuth, GoogleFileWatch, - FileWatchRenewalAttempt, + RefundRequest, ) @@ -63,12 +63,12 @@ class GoogleFileWatchAdmin(admin.ModelAdmin): ) ordering = ["-expiration_date"] - def save_form(self, request, form, change): + def save_form(self, request, form, change): # noqa: D102 if not change: file_id = form.cleaned_data["file_id"] if self.model.objects.filter(file_id=file_id).exists(): raise ValidationError( - "Only one GoogleFileWatch object should exist for each unique file_id (file_id provided: {}). " + "Only one GoogleFileWatch object should exist for each unique file_id (file_id provided: {}). " # noqa: EM103 "Update the existing object instead of creating a new one.".format( file_id ) diff --git a/sheets/api.py b/sheets/api.py index bb35fd83b..482172ba2 100644 --- a/sheets/api.py +++ b/sheets/api.py @@ -1,54 +1,53 @@ """API for the Sheets app""" -import os -import json import datetime -import pickle +import json import logging +import os +import pickle from collections import namedtuple from urllib.parse import urljoin +import pygsheets from django.conf import settings -from django.db import transaction from django.core.exceptions import ImproperlyConfigured -import pygsheets - -from google.oauth2.credentials import Credentials # pylint:disable=no-name-in-module -from google.oauth2.service_account import ( # pylint:disable=no-name-in-module +from django.db import transaction +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google.oauth2.service_account import ( Credentials as ServiceAccountCredentials, ) -from google.auth.transport.requests import Request # pylint:disable=no-name-in-module from googleapiclient.discovery import build from googleapiclient.errors import HttpError from mitxpro.utils import now_in_utc -from sheets.models import GoogleApiAuth, GoogleFileWatch, FileWatchRenewalAttempt from sheets.constants import ( - GOOGLE_TOKEN_URI, - REQUIRED_GOOGLE_API_SCOPES, - GOOGLE_SERVICE_ACCOUNT_EMAIL_DOMAIN, + DEFAULT_GOOGLE_EXPIRE_TIMEDELTA, GOOGLE_API_FILE_WATCH_KIND, GOOGLE_API_NOTIFICATION_TYPE, - DEFAULT_GOOGLE_EXPIRE_TIMEDELTA, + GOOGLE_SERVICE_ACCOUNT_EMAIL_DOMAIN, + GOOGLE_TOKEN_URI, + REQUIRED_GOOGLE_API_SCOPES, + SHEET_RENEWAL_RECORD_LIMIT, + SHEET_TYPE_COUPON_ASSIGN, SHEET_TYPE_COUPON_REQUEST, SHEET_TYPE_ENROLL_CHANGE, WORKSHEET_TYPE_REFUND, - SHEET_TYPE_COUPON_ASSIGN, - SHEET_RENEWAL_RECORD_LIMIT, ) +from sheets.exceptions import FailedBatchRequestException +from sheets.models import FileWatchRenewalAttempt, GoogleApiAuth, GoogleFileWatch from sheets.utils import ( - format_datetime_for_google_timestamp, - google_timestamp_to_datetime, - build_drive_file_email_share_request, + CouponAssignSheetMetadata, CouponRequestSheetMetadata, RefundRequestSheetMetadata, - CouponAssignSheetMetadata, + build_drive_file_email_share_request, + format_datetime_for_google_timestamp, + google_timestamp_to_datetime, ) -from sheets.exceptions import FailedBatchRequestException log = logging.getLogger(__name__) -DEV_TOKEN_PATH = "localdev/google.token" -FileWatchSpec = namedtuple( +DEV_TOKEN_PATH = "localdev/google.token" # noqa: S105 +FileWatchSpec = namedtuple( # noqa: PYI024 "FileWatchSpec", ["sheet_metadata", "sheet_file_id", "channel_id", "handler_url", "force"], ) @@ -59,16 +58,16 @@ def get_google_creds_from_pickled_token_file(token_file_path): Helper method to get valid credentials from a local token file (and refresh as necessary). For dev use only. """ - with open(token_file_path, "rb") as f: - creds = pickle.loads(f.read()) + with open(token_file_path, "rb") as f: # noqa: PTH123 + creds = pickle.loads(f.read()) # noqa: S301 if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) - with open(token_file_path, "wb") as token: + with open(token_file_path, "wb") as token: # noqa: PTH123 pickle.dump(creds, token) if not creds: - raise ImproperlyConfigured("Local token file credentials are empty") + raise ImproperlyConfigured("Local token file credentials are empty") # noqa: EM101 if not creds.valid: - raise ImproperlyConfigured("Local token file credentials are invalid") + raise ImproperlyConfigured("Local token file credentials are invalid") # noqa: EM101 return creds @@ -90,7 +89,7 @@ def get_credentials(): ) if not is_sharing_to_service_account: raise ImproperlyConfigured( - "If Service Account auth is being used, the SHEETS_ADMIN_EMAILS setting must " + "If Service Account auth is being used, the SHEETS_ADMIN_EMAILS setting must " # noqa: EM101 "include a Service Account email for spreadsheet updates/creation to work. " "Add the Service Account email to that setting, or remove the DRIVE_SERVICE_ACCOUNT_CREDS " "setting and use a different auth method." @@ -129,10 +128,10 @@ def get_credentials(): # A script with more helpful options than the one in that guide can be found here: # https://gist.github.com/gsidebo/b87abaafda3e79186c1e5f7f964074ab if settings.ENVIRONMENT == "dev": - token_file_path = os.path.join(settings.BASE_DIR, DEV_TOKEN_PATH) - if os.path.exists(token_file_path): + token_file_path = os.path.join(settings.BASE_DIR, DEV_TOKEN_PATH) # noqa: PTH118 + if os.path.exists(token_file_path): # noqa: PTH110 return get_google_creds_from_pickled_token_file(token_file_path) - raise ImproperlyConfigured("Authorization with Google has not been completed.") + raise ImproperlyConfigured("Authorization with Google has not been completed.") # noqa: EM101 def get_authorized_pygsheets_client(): @@ -176,7 +175,7 @@ def get_metadata_for_matching_files(self, query, file_fields="id, name"): extra_list_params = {} if self.supports_team_drives: extra_list_params.update( - dict( + dict( # noqa: C408 corpora="teamDrive", teamDriveId=settings.DRIVE_SHARED_ID, supportsTeamDrives=True, @@ -184,7 +183,9 @@ def get_metadata_for_matching_files(self, query, file_fields="id, name"): ) ) return self.pygsheets_client.drive.list( - **extra_list_params, fields="files({})".format(file_fields), q=query + **extra_list_params, + fields="files({})".format(file_fields), # noqa: UP032 + q=query, ) def update_spreadsheet_properties(self, file_id, property_dict): @@ -285,8 +286,10 @@ def build_drive_service(credentials=None): def batch_share_callback( - request_id, response, exception -): # pylint: disable=unused-argument + request_id, # noqa: ARG001 + response, # noqa: ARG001 + exception, +): """ A callback function given to the Google API client's new_batch_http_request(). Called for the result of each individual request executed in a batch API call. @@ -317,9 +320,6 @@ def share_drive_file_with_emails(file_id, emails_to_share, credentials=None): emails_to_share (list of str): Email addresses that will be added as shared users for the given file credentials (google.oauth2.credentials.Credentials or None): Credentials to be used by the Google Drive client - - Returns: - """ if not emails_to_share: return @@ -347,7 +347,7 @@ def share_drive_file_with_emails(file_id, emails_to_share, credentials=None): ) try: perm_request.execute() - except: # pylint: disable=bare-except + except: # noqa: E722 log.exception( "Failed to share the file with id '%s' with email '%s'", file_id, @@ -465,7 +465,7 @@ def _track_file_watch_renewal(sheet_type, sheet_file_id, exception=None): FileWatchRenewalAttempt.objects.filter(id__lte=id_to_delete).delete() -def create_or_renew_sheet_file_watch(sheet_metadata, force=False, sheet_file_id=None): +def create_or_renew_sheet_file_watch(sheet_metadata, force=False, sheet_file_id=None): # noqa: FBT002 """ Creates or renews a file watch on a spreadsheet depending on the existence of other file watches and their expiration. @@ -499,7 +499,7 @@ def create_or_renew_sheet_file_watch(sheet_metadata, force=False, sheet_file_id= with transaction.atomic(): file_watch, created = GoogleFileWatch.objects.select_for_update().get_or_create( file_id=sheet_file_id, - defaults=dict( + defaults=dict( # noqa: C408 version=1, channel_id=new_channel_id, activation_date=now, @@ -569,4 +569,4 @@ def get_sheet_metadata_from_type(sheet_type): return CouponAssignSheetMetadata() elif sheet_type in {SHEET_TYPE_ENROLL_CHANGE, WORKSHEET_TYPE_REFUND}: return RefundRequestSheetMetadata() - raise ValueError(f"No sheet metadata exists matching the type '{sheet_type}'") + raise ValueError(f"No sheet metadata exists matching the type '{sheet_type}'") # noqa: EM102 diff --git a/sheets/api_test.py b/sheets/api_test.py index 8727efa48..3452abb34 100644 --- a/sheets/api_test.py +++ b/sheets/api_test.py @@ -1,9 +1,7 @@ -# pylint: disable=redefined-outer-name,unused-argument """Sheets API tests""" import pytest - from django.core.exceptions import ImproperlyConfigured -from google.oauth2.credentials import Credentials # pylint: disable-all +from google.oauth2.credentials import Credentials from sheets.api import get_credentials from sheets.constants import ( @@ -27,7 +25,7 @@ def test_get_credentials_service_account(mocker, settings): get_credentials() settings.SHEETS_ADMIN_EMAILS.append( - "service-account@mitxpro.{}".format(GOOGLE_SERVICE_ACCOUNT_EMAIL_DOMAIN) + "service-account@mitxpro.{}".format(GOOGLE_SERVICE_ACCOUNT_EMAIL_DOMAIN) # noqa: UP032 ) creds = get_credentials() @@ -45,7 +43,7 @@ def test_get_credentials_personal_auth(settings): """ settings.DRIVE_SERVICE_ACCOUNT_CREDS = None settings.DRIVE_CLIENT_ID = "client-id" - settings.DRIVE_CLIENT_SECRET = "client-secret" + settings.DRIVE_CLIENT_SECRET = "client-secret" # noqa: S105 settings.ENVIRONMENT = "prod" with pytest.raises(ImproperlyConfigured): get_credentials() diff --git a/sheets/conftest.py b/sheets/conftest.py index ca5184ca0..aef3b3ce4 100644 --- a/sheets/conftest.py +++ b/sheets/conftest.py @@ -1,10 +1,9 @@ """Fixtures relevant to the sheets app test suite""" -# pylint: disable=redefined-outer-name import pytest @pytest.fixture(autouse=True) -def sheets_settings(settings): +def sheets_settings(settings): # noqa: PT004 """Default settings for sheets tests""" settings.FEATURES["COUPON_SHEETS"] = True settings.SHEETS_REQ_EMAIL_COL = 7 diff --git a/sheets/constants.py b/sheets/constants.py index df6903065..c9958a9b9 100644 --- a/sheets/constants.py +++ b/sheets/constants.py @@ -1,17 +1,17 @@ """Sheets app constants""" from mail.constants import ( - MAILGUN_FAILED, + MAILGUN_CLICKED, MAILGUN_DELIVERED, + MAILGUN_FAILED, MAILGUN_OPENED, - MAILGUN_CLICKED, ) REQUIRED_GOOGLE_API_SCOPES = [ "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive", ] -DEFAULT_GOOGLE_EXPIRE_TIMEDELTA = dict(minutes=60) +DEFAULT_GOOGLE_EXPIRE_TIMEDELTA = dict(minutes=60) # noqa: C408 SHEETS_VALUE_REQUEST_PAGE_SIZE = 50 SHEET_TYPE_COUPON_REQUEST = "enrollrequest" @@ -29,7 +29,7 @@ # The index of the first row of a spreadsheet according to Google GOOGLE_SHEET_FIRST_ROW = 1 GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/auth" -GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" # noqa: S105 GOOGLE_AUTH_PROVIDER_X509_CERT_URL = "https://www.googleapis.com/oauth2/v1/certs" GOOGLE_DATE_TIME_FORMAT = "DATE_TIME" GOOGLE_SERVICE_ACCOUNT_EMAIL_DOMAIN = "iam.gserviceaccount.com" diff --git a/sheets/coupon_assign_api.py b/sheets/coupon_assign_api.py index 2df589972..882ca2b53 100644 --- a/sheets/coupon_assign_api.py +++ b/sheets/coupon_assign_api.py @@ -1,4 +1,3 @@ -# pylint: disable=too-many-lines """Coupon assignment API""" import logging from collections import defaultdict @@ -11,48 +10,48 @@ import ecommerce.api from ecommerce.mail_api import send_bulk_enroll_emails from ecommerce.models import ( + BulkCouponAssignment, CouponEligibility, ProductCouponAssignment, - BulkCouponAssignment, ) from mail.api import validate_email_addresses from mail.constants import MAILGUN_DELIVERED from mail.exceptions import MultiEmailValidationError from mitxpro.utils import ( - now_in_utc, all_unique, - partition_to_lists, - partition, - item_at_index_or_none, case_insensitive_equal, + item_at_index_or_none, + now_in_utc, + partition, + partition_to_lists, ) -from sheets.api import get_authorized_pygsheets_client, ExpandedSheetsClient +from sheets.api import ExpandedSheetsClient, get_authorized_pygsheets_client from sheets.constants import ( - ASSIGNMENT_SHEET_PREFIX, + ASSIGNMENT_MESSAGES_COMPLETED_DATE_KEY, ASSIGNMENT_MESSAGES_COMPLETED_KEY, + ASSIGNMENT_SHEET_ASSIGNED_STATUS, + ASSIGNMENT_SHEET_EMAIL_RETRY_MINUTES, + ASSIGNMENT_SHEET_ENROLLED_STATUS, + ASSIGNMENT_SHEET_INVALID_STATUS, + ASSIGNMENT_SHEET_MAX_AGE_DAYS, + ASSIGNMENT_SHEET_PREFIX, GOOGLE_API_TRUE_VAL, - ASSIGNMENT_MESSAGES_COMPLETED_DATE_KEY, GOOGLE_DATE_TIME_FORMAT, - ASSIGNMENT_SHEET_INVALID_STATUS, - UNSENT_EMAIL_STATUSES, - ASSIGNMENT_SHEET_ENROLLED_STATUS, GOOGLE_SHEET_FIRST_ROW, RELEVANT_ASSIGNMENT_EMAIL_EVENTS, - ASSIGNMENT_SHEET_MAX_AGE_DAYS, - ASSIGNMENT_SHEET_ASSIGNED_STATUS, - ASSIGNMENT_SHEET_EMAIL_RETRY_MINUTES, + UNSENT_EMAIL_STATUSES, ) -from sheets.exceptions import SheetValidationException, SheetRowParsingException +from sheets.exceptions import SheetRowParsingException, SheetValidationException from sheets.mail_api import get_bulk_assignment_messages from sheets.utils import ( - format_datetime_for_google_api, + AssignmentRowUpdate, + assign_sheet_metadata, build_multi_cell_update_request_body, + format_datetime_for_google_api, format_datetime_for_sheet_formula, get_data_rows, mailgun_timestamp_to_datetime, parse_sheet_datetime_str, - assign_sheet_metadata, - AssignmentRowUpdate, ) log = logging.getLogger(__name__) @@ -61,9 +60,9 @@ class CouponAssignmentRow: """Represents a row of a coupon assignment sheet""" - def __init__( + def __init__( # noqa: PLR0913 self, row_index, code, assignee_email, status, status_date, enrolled_email - ): # pylint: disable=too-many-arguments + ): self.row_index = row_index self.code = code self.email = assignee_email @@ -105,7 +104,7 @@ def parse_raw_data(cls, row_index, raw_row_data): raw_row_data, assign_sheet_metadata.ENROLLED_EMAIL_COL ), ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 raise SheetRowParsingException(str(exc)) from exc @@ -144,9 +143,9 @@ def add_assignment_rows(self, bulk_assignment, assignment_rows): else: self._unassigned_code_map[bulk_assignment.id] += 1 - def add_potential_event_date( + def add_potential_event_date( # noqa: PLR0913 self, bulk_assignment_id, code, recipient_email, event_type, event_date - ): # pylint: disable=too-many-arguments + ): """ Fills in a status (e.g.: "delivered") and the datetime when that status was logged if the given coupon assignment exists in the map, and the status is different from the previous status. @@ -527,7 +526,7 @@ class CouponAssignmentHandler: """Manages the processing of coupon assignments from Sheet data""" ASSIGNMENT_SHEETS_QUERY = ( - '"{folder_id}" in parents and ' + '"{folder_id}" in parents and ' # noqa: UP032 'name contains "{name_prefix}" and ' "trashed != true".format( folder_id=settings.DRIVE_OUTPUT_FOLDER_ID, @@ -580,10 +579,10 @@ def parsed_rows(self): data_rows = list(get_data_rows(self.worksheet)) coupon_codes = [row[0] for row in data_rows] if not coupon_codes: - raise SheetValidationException("No data found in coupon assignment Sheet") + raise SheetValidationException("No data found in coupon assignment Sheet") # noqa: EM101 if not all_unique(coupon_codes): raise SheetValidationException( - "All coupon codes in the Sheet must be unique" + "All coupon codes in the Sheet must be unique" # noqa: EM101 ) return [ CouponAssignmentRow.parse_raw_data( @@ -684,7 +683,7 @@ def report_invalid_emails(self, assignment_rows, invalid_emails): row_updates=row_updates, zero_based_index=False ) - def update_sheet_with_new_statuses(self, row_updates, zero_based_index=False): + def update_sheet_with_new_statuses(self, row_updates, zero_based_index=False): # noqa: FBT002 """ Updates the relevant cells of a coupon assignment Sheet with message statuses and dates. @@ -737,7 +736,7 @@ def update_sheet_with_new_statuses(self, row_updates, zero_based_index=False): ) return responses - def update_sheet_with_alternate_emails(self, row_updates, zero_based_index=False): + def update_sheet_with_alternate_emails(self, row_updates, zero_based_index=False): # noqa: FBT002 """ Updates the relevant cells of a coupon assignment Sheet with emails that users enrolled with (if different from the email that was originally entered for the assignment). @@ -822,11 +821,11 @@ def get_desired_coupon_assignments(cls, assignment_rows): ).values_list("coupon__coupon_code", "id") if len(product_coupon_tuples) != len(valid_rows): raise SheetValidationException( - "Mismatch between the number of matching product coupons and the number of coupon " + "Mismatch between the number of matching product coupons and the number of coupon " # noqa: EM101 "codes listed in the Sheet. There may be an invalid coupon code in the Sheet." ) product_coupon_dict = dict(product_coupon_tuples) - return set((row.email, product_coupon_dict[row.code]) for row in valid_rows) + return set((row.email, product_coupon_dict[row.code]) for row in valid_rows) # noqa: C401 @staticmethod def get_assignments_to_create_and_remove( @@ -875,7 +874,7 @@ def get_assignments_to_create_and_remove( ) # If any of the assignments we want to create have the same product coupon as one # of these already-redeemed assignments, filter them out and log an info message. - product_coupon_ids = set( + product_coupon_ids = set( # noqa: C401 assignment.product_coupon_id for assignment in already_redeemed_assignments ) @@ -932,7 +931,7 @@ def update_coupon_delivery_statuses(self, assignment_status_map): # Update spreadsheet metadata to reflect the status try: self.set_spreadsheet_completed(now) - except Exception: # pylint: disable=broad-except + except Exception: log.exception( "The BulkCouponAssignment has been updated to indicate that message delivery is complete, " "but the request to update spreadsheet properties to indicate this status failed " @@ -947,7 +946,7 @@ def update_coupon_delivery_statuses(self, assignment_status_map): return updated_assignments - def process_assignment_spreadsheet(self): # pylint: disable=too-many-locals + def process_assignment_spreadsheet(self): """ Ensures that there are product coupon assignments for every filled-in row in a coupon assignment Spreadsheet, and sets some metadata to reflect the state of the bulk assignment. @@ -991,7 +990,7 @@ def process_assignment_spreadsheet(self): # pylint: disable=too-many-locals # Validate emails before assignment so we can filter out and report on any bad emails try: validate_email_addresses( - (assignment_tuple[0] for assignment_tuple in assignments_to_create) + (assignment_tuple[0] for assignment_tuple in assignments_to_create) # noqa: UP034 ) except MultiEmailValidationError as exc: invalid_emails = exc.invalid_emails diff --git a/sheets/coupon_request_api.py b/sheets/coupon_request_api.py index fb80d9307..d2c59d707 100644 --- a/sheets/coupon_request_api.py +++ b/sheets/coupon_request_api.py @@ -1,8 +1,8 @@ """Coupon request API""" import itertools import json -from decimal import Decimal import logging +from decimal import Decimal from django.conf import settings from django.db import transaction @@ -10,33 +10,33 @@ import ecommerce.api from ecommerce.constants import DISCOUNT_TYPE_PERCENT_OFF -from ecommerce.models import Company, Coupon, CouponPaymentVersion, BulkCouponAssignment +from ecommerce.models import BulkCouponAssignment, Company, Coupon, CouponPaymentVersion from ecommerce.utils import make_checkout_url -from mitxpro.utils import now_in_utc, item_at_index_or_none, item_at_index_or_blank +from mitxpro.utils import item_at_index_or_blank, item_at_index_or_none, now_in_utc from sheets.api import ( + create_or_renew_sheet_file_watch, get_authorized_pygsheets_client, share_drive_file_with_emails, - create_or_renew_sheet_file_watch, ) from sheets.constants import GOOGLE_API_TRUE_VAL from sheets.exceptions import SheetRowParsingException from sheets.models import CouponGenerationRequest from sheets.sheet_handler_api import SheetHandler from sheets.utils import ( + ResultType, RowResult, - format_datetime_for_sheet_formula, - build_protected_range_request_body, + assign_sheet_metadata, assignment_sheet_file_name, + build_protected_range_request_body, + format_datetime_for_sheet_formula, + get_column_letter, parse_sheet_datetime_str, request_sheet_metadata, - assign_sheet_metadata, - ResultType, - get_column_letter, ) log = logging.getLogger(__name__) -BULK_PURCHASE_DEFAULTS = dict(amount=Decimal("1.0"), automatic=False) +BULK_PURCHASE_DEFAULTS = dict(amount=Decimal("1.0"), automatic=False) # noqa: C408 def create_coupons_for_request_row(row, company_id): @@ -71,10 +71,10 @@ def create_coupons_for_request_row(row, company_id): ) -class CouponRequestRow: # pylint: disable=too-many-instance-attributes +class CouponRequestRow: """Represents a row of a coupon request sheet""" - def __init__( + def __init__( # noqa: PLR0913 self, row_index, purchase_order_id, @@ -88,7 +88,7 @@ def __init__( errors, skip_row, requester, - ): # pylint: disable=too-many-arguments,too-many-locals + ): self.row_index = row_index self.purchase_order_id = purchase_order_id self.coupon_name = coupon_name @@ -157,7 +157,7 @@ def parse_raw_data(cls, row_index, raw_row_data): ) == GOOGLE_API_TRUE_VAL, ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 raise SheetRowParsingException(str(exc)) from exc @@ -190,7 +190,7 @@ def __init__(self): self.sheet_metadata = request_sheet_metadata @cached_property - def worksheet(self): + def worksheet(self): # noqa: D102 return self.spreadsheet.sheet1 def protect_coupon_assignment_ranges( @@ -259,11 +259,11 @@ def create_assignment_sheet(self, coupon_req_row): "Cannot create bulk coupon sheet - No coupon codes found matching the name '%s'", coupon_req_row.coupon_name, ) - return + return # noqa: RET502 # Create sheet spreadsheet_title = assignment_sheet_file_name(coupon_req_row) create_kwargs = ( - dict(folder=settings.DRIVE_OUTPUT_FOLDER_ID) + dict(folder=settings.DRIVE_OUTPUT_FOLDER_ID) # noqa: C408 if settings.DRIVE_OUTPUT_FOLDER_ID else {} ) @@ -273,7 +273,7 @@ def create_assignment_sheet(self, coupon_req_row): worksheet = bulk_coupon_sheet.sheet1 # Add headers worksheet.update_values( - crange="A1:{}1".format(assign_sheet_metadata.LAST_COL_LETTER), + crange="A1:{}1".format(assign_sheet_metadata.LAST_COL_LETTER), # noqa: UP032 values=[assign_sheet_metadata.column_headers], ) # Write enrollment codes to the appropriate column of the worksheet @@ -311,11 +311,11 @@ def create_assignment_sheet(self, coupon_req_row): # Format header cells with bold text header_range = worksheet.get_values( start="A1", - end="{}1".format(assign_sheet_metadata.LAST_COL_LETTER), + end="{}1".format(assign_sheet_metadata.LAST_COL_LETTER), # noqa: UP032 returnas="range", ) first_cell = header_range.cells[0][0] - first_cell.set_text_format("bold", True) + first_cell.set_text_format("bold", True) # noqa: FBT003 header_range.apply_format(first_cell) # Protect ranges of cells that should not be edited (everything besides the email column) self.protect_coupon_assignment_ranges( @@ -339,7 +339,7 @@ def create_assignment_sheet(self, coupon_req_row): ) return bulk_coupon_sheet - def update_completed_rows(self, success_row_results): + def update_completed_rows(self, success_row_results): # noqa: D102 for row_result in success_row_results: self.worksheet.update_values( crange="{date_processed_col}{row_index}:{error_col}{row_index}".format( @@ -359,13 +359,13 @@ def update_completed_rows(self, success_row_results): ], ) - def post_process_results(self, grouped_row_results): + def post_process_results(self, grouped_row_results): # noqa: D102 # Create assignment sheets for all newly-processed rows processed_row_results = grouped_row_results.get(ResultType.PROCESSED, []) for row_result in processed_row_results: self.create_assignment_sheet(row_result.row_object) - def get_or_create_request(self, row_data): + def get_or_create_request(self, row_data): # noqa: D102 coupon_name = row_data[self.sheet_metadata.COUPON_NAME_COL_INDEX].strip() purchase_order_id = row_data[ self.sheet_metadata.PURCHASE_ORDER_COL_INDEX @@ -379,7 +379,7 @@ def get_or_create_request(self, row_data): created, ) = CouponGenerationRequest.objects.select_for_update().get_or_create( coupon_name=coupon_name, - defaults=dict( + defaults=dict( # noqa: C408 purchase_order_id=purchase_order_id, raw_data=user_input_json ), ) @@ -390,7 +390,7 @@ def get_or_create_request(self, row_data): return coupon_gen_request, created, raw_data_changed @staticmethod - def validate_sheet(enumerated_rows): + def validate_sheet(enumerated_rows): # noqa: D102 enumerated_data_rows_1, enumerated_data_rows_2 = itertools.tee(enumerated_rows) invalid_rows = [] observed_coupon_names = set() @@ -420,13 +420,13 @@ def validate_sheet(enumerated_rows): ) valid_data_rows = filter( - lambda data_row_tuple: data_row_tuple[0] + lambda data_row_tuple: data_row_tuple[0] # noqa: SIM118 not in invalid_coupon_name_row_dict.keys(), enumerated_data_rows_2, ) return valid_data_rows, invalid_rows - def filter_ignored_rows(self, enumerated_rows): + def filter_ignored_rows(self, enumerated_rows): # noqa: D102 return filter( # If the "ignore" column is set to TRUE for this row, or it has already been processed, # it should be skipped @@ -434,9 +434,9 @@ def filter_ignored_rows(self, enumerated_rows): enumerated_rows, ) - def process_row( + def process_row( # noqa: D102 self, row_index, row_data - ): # pylint: disable=too-many-return-statements + ): ( coupon_gen_request, request_created, @@ -450,7 +450,7 @@ def process_row( row_db_record=coupon_gen_request, row_object=None, result_type=ResultType.FAILED, - message="Parsing failure: {}".format(str(exc)), + message="Parsing failure: {}".format(str(exc)), # noqa: UP032 ) is_unchanged_error_row = ( coupon_req_row.errors and not request_created and not request_updated @@ -476,7 +476,7 @@ def process_row( company, created = Company.objects.get_or_create( name__iexact=coupon_req_row.company_name, - defaults=dict(name=coupon_req_row.company_name), + defaults=dict(name=coupon_req_row.company_name), # noqa: C408 ) if created: log.info("Created new Company '%s'...", coupon_req_row.company_name) diff --git a/sheets/coupon_request_api_test.py b/sheets/coupon_request_api_test.py index 1c870da87..a4f95563e 100644 --- a/sheets/coupon_request_api_test.py +++ b/sheets/coupon_request_api_test.py @@ -1,11 +1,10 @@ -# pylint: disable=redefined-outer-name,unused-argument """Coupon request API tests""" import os from types import SimpleNamespace import pytest -from pygsheets import Worksheet, Spreadsheet +from pygsheets import Spreadsheet, Worksheet from pygsheets.client import Client as PygsheetsClient from pygsheets.drive import DriveAPIWrapper from pygsheets.sheet import SheetAPIWrapper @@ -20,7 +19,7 @@ @pytest.fixture -def courseware_objects(): +def courseware_objects(): # noqa: PT004 """Database objects that CSV data depends on""" run = CourseRunFactory.create(courseware_id="course-v1:edX+DemoX+Demo_Course") ProductVersionFactory.create(product__content_object=run) @@ -29,10 +28,10 @@ def courseware_objects(): @pytest.fixture def request_csv_rows(settings, courseware_objects): """Fake coupon request spreadsheet data rows (loaded from CSV)""" - fake_request_csv_filepath = os.path.join( + fake_request_csv_filepath = os.path.join( # noqa: PTH118 settings.BASE_DIR, "sheets/resources/coupon_requests.csv" ) - with open(fake_request_csv_filepath) as f: + with open(fake_request_csv_filepath) as f: # noqa: PTH123 # Return all rows except for the header return [line.split(",") for i, line in enumerate(f.readlines()) if i > 0] @@ -97,16 +96,14 @@ def test_full_sheet_process( expected_processed_rows = {6, 8} expected_failed_rows = {5, 7} assert ResultType.PROCESSED.value in result - assert ( - set(result[ResultType.PROCESSED.value]) == expected_processed_rows - ), "Rows %s as defined in coupon_requests.csv should be processed" % str( - expected_processed_rows + assert set(result[ResultType.PROCESSED.value]) == expected_processed_rows, ( + "Rows %s as defined in coupon_requests.csv should be processed" + % str(expected_processed_rows) ) assert ResultType.FAILED.value in result - assert ( - set(result[ResultType.FAILED.value]) == expected_failed_rows - ), "Rows %s as defined in coupon_requests.csv should fail" % str( - expected_failed_rows + assert set(result[ResultType.FAILED.value]) == expected_failed_rows, ( + "Rows %s as defined in coupon_requests.csv should fail" + % str(expected_failed_rows) ) # A CouponGenerationRequest should be created for each row that wasn't ignored and did not fail full sheet # validation (CSV has 1 row that should fail validation, hence the 1) @@ -119,7 +116,7 @@ def test_full_sheet_process( for i, row_data in enumerate(request_csv_rows, start=2) if i in expected_processed_rows ] - expected_coupons = sum((row.num_codes for row in processed_rows)) + expected_coupons = sum(row.num_codes for row in processed_rows) assert Coupon.objects.all().count() == expected_coupons # Sheets API should have been used to create an assignment sheet and share it assert patched_sheets_api.create_file_watch.call_count == len( diff --git a/sheets/deferral_request_api.py b/sheets/deferral_request_api.py index 352b57f23..998cbf7ee 100644 --- a/sheets/deferral_request_api.py +++ b/sheets/deferral_request_api.py @@ -6,29 +6,29 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from courses.api import defer_enrollment -from courses.models import CourseRunEnrollment, CourseRun +from courses.models import CourseRun, CourseRunEnrollment from courseware.exceptions import EdxEnrollmentCreateError from mitxpro.utils import now_in_utc from sheets.constants import GOOGLE_API_TRUE_VAL -from sheets.sheet_handler_api import EnrollmentChangeRequestHandler from sheets.exceptions import SheetRowParsingException from sheets.models import DeferralRequest +from sheets.sheet_handler_api import EnrollmentChangeRequestHandler from sheets.utils import ( ResultType, RowResult, clean_sheet_value, - parse_sheet_date_only_str, deferral_sheet_metadata, + parse_sheet_date_only_str, ) log = logging.getLogger(__name__) User = get_user_model() -class DeferralRequestRow: # pylint: disable=too-many-instance-attributes +class DeferralRequestRow: """Represents a row of the deferral request sheet""" - def __init__( + def __init__( # noqa: PLR0913 self, row_index, response_id, @@ -42,7 +42,7 @@ def __init__( deferral_complete_date, errors, skip_row, - ): # pylint: disable=too-many-arguments,too-many-locals + ): self.row_index = row_index self.response_id = response_id self.request_date = request_date @@ -91,7 +91,7 @@ def parse_raw_data(cls, row_index, raw_row_data): == GOOGLE_API_TRUE_VAL ), ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 raise SheetRowParsingException(str(exc)) from exc @@ -106,9 +106,9 @@ def __init__(self): request_model_cls=DeferralRequest, ) - def process_row( + def process_row( # noqa: C901, PLR0911 self, row_index, row_data - ): # pylint: disable=too-many-return-statements + ): """ Ensures that the given spreadsheet row is correctly represented in the database, attempts to parse it, defers the given enrollment if appropriate, and returns the @@ -133,7 +133,7 @@ def process_row( row_db_record=deferral_request, row_object=None, result_type=ResultType.FAILED, - message="Parsing failure: {}".format(str(exc)), + message="Parsing failure: {}".format(str(exc)), # noqa: UP032 ) is_unchanged_error_row = ( deferral_req_row.errors and not request_created and not request_updated @@ -176,7 +176,7 @@ def process_row( ) # When #1838 is completed, this logic can be removed if not from_enrollment and not to_enrollment: - raise Exception("edX enrollment change failed") + raise Exception("edX enrollment change failed") # noqa: EM101, TRY002 except ObjectDoesNotExist as exc: if isinstance(exc, CourseRunEnrollment.DoesNotExist): message = "'from' course run enrollment does not exist ({})".format( @@ -187,7 +187,7 @@ def process_row( deferral_req_row.to_courseware_id ) elif isinstance(exc, User.DoesNotExist): - message = "User '{}' does not exist".format( + message = "User '{}' does not exist".format( # noqa: UP032 deferral_req_row.learner_email ) else: @@ -205,7 +205,7 @@ def process_row( row_db_record=deferral_request, row_object=None, result_type=ResultType.FAILED, - message="Invalid deferral: {}".format(exc), + message="Invalid deferral: {}".format(exc), # noqa: UP032 ) except EdxEnrollmentCreateError as exc: return RowResult( @@ -213,7 +213,7 @@ def process_row( row_db_record=deferral_request, row_object=None, result_type=ResultType.FAILED, - message="Unable to defer enrollment: {}".format(exc), + message="Unable to defer enrollment: {}".format(exc), # noqa: UP032 ) deferral_request.date_completed = now_in_utc() diff --git a/sheets/exceptions.py b/sheets/exceptions.py index dce04b4cf..c3b35ef76 100644 --- a/sheets/exceptions.py +++ b/sheets/exceptions.py @@ -1,25 +1,25 @@ """Sheets app exceptions""" -class SheetValidationException(Exception): +class SheetValidationException(Exception): # noqa: N818 """ General exception for failures during the validation of Sheet data """ -class SheetUpdateException(Exception): +class SheetUpdateException(Exception): # noqa: N818 """ General exception for failures while attempting to update a Sheet via API """ -class SheetRowParsingException(Exception): +class SheetRowParsingException(Exception): # noqa: N818 """ General exception for failures while attempting to parse the data in a Sheet row """ -class SheetOutOfSyncException(Exception): +class SheetOutOfSyncException(Exception): # noqa: N818 """ General exception for situations where the data in a spreadsheet does not reflect the state of the database """ @@ -30,13 +30,13 @@ def __init__(self, coupon_gen_request, coupon_req_row, msg=None): super().__init__(msg) -class InvalidSheetProductException(Exception): +class InvalidSheetProductException(Exception): # noqa: N818 """ Exception for an invalid product entered into the coupon request spreadsheet """ -class FailedBatchRequestException(Exception): +class FailedBatchRequestException(Exception): # noqa: N818 """ General exception for a failure during a Google batch API request """ diff --git a/sheets/factories.py b/sheets/factories.py index e6d15ee55..6e8351b4d 100644 --- a/sheets/factories.py +++ b/sheets/factories.py @@ -1,5 +1,6 @@ """Factories for sheets app""" import datetime + from factory import Faker, SubFactory, fuzzy from factory.django import DjangoModelFactory @@ -7,9 +8,9 @@ from users.factories import UserFactory -class CouponGenerationRequestFactory( +class CouponGenerationRequestFactory( # noqa: D101 DjangoModelFactory -): # pylint: disable=missing-docstring +): purchase_order_id = Faker("pystr", max_chars=15) coupon_name = fuzzy.FuzzyText() @@ -17,7 +18,7 @@ class Meta: model = models.CouponGenerationRequest -class GoogleApiAuthFactory(DjangoModelFactory): # pylint: disable=missing-docstring +class GoogleApiAuthFactory(DjangoModelFactory): # noqa: D101 requesting_user = SubFactory(UserFactory) access_token = Faker("pystr", max_chars=30) refresh_token = Faker("pystr", max_chars=30) @@ -26,7 +27,7 @@ class Meta: model = models.GoogleApiAuth -class GoogleFileWatchFactory(DjangoModelFactory): # pylint: disable=missing-docstring +class GoogleFileWatchFactory(DjangoModelFactory): # noqa: D101 file_id = Faker("pystr", max_chars=15) channel_id = fuzzy.FuzzyText(prefix="Channel ") activation_date = Faker( diff --git a/sheets/mail_api.py b/sheets/mail_api.py index 1feb03655..f72c3de1e 100644 --- a/sheets/mail_api.py +++ b/sheets/mail_api.py @@ -1,19 +1,19 @@ """Mail API for sheets app""" import logging -from urllib.parse import urlencode from collections import namedtuple +from urllib.parse import urlencode from django.conf import settings from ecommerce.constants import BULK_ENROLLMENT_EMAIL_TAG from mail.constants import MAILGUN_API_DOMAIN +from mitxpro.utils import has_all_keys, request_get_with_timeout_retry from sheets.constants import MAILGUN_API_TIMEOUT_RETRIES from sheets.utils import format_datetime_for_mailgun -from mitxpro.utils import has_all_keys, request_get_with_timeout_retry log = logging.getLogger(__name__) -BulkAssignmentMessage = namedtuple( +BulkAssignmentMessage = namedtuple( # noqa: PYI024 "BulkAssignmentMessage", ["bulk_assignment_id", "coupon_code", "email", "event", "timestamp"], ) @@ -73,8 +73,8 @@ def get_bulk_assignment_messages(event=None, begin=None, end=None): raw_next_url = resp_data["paging"]["next"] # The "next" url in the paging section does not contain necessary auth. Fill it in here. url = raw_next_url.replace( - "/{}/".format(MAILGUN_API_DOMAIN), - "/api:{}@{}/".format(settings.MAILGUN_KEY, MAILGUN_API_DOMAIN), + "/{}/".format(MAILGUN_API_DOMAIN), # noqa: UP032 + "/api:{}@{}/".format(settings.MAILGUN_KEY, MAILGUN_API_DOMAIN), # noqa: UP032 ) resp = request_get_with_timeout_retry( url, retries=MAILGUN_API_TIMEOUT_RETRIES diff --git a/sheets/management/commands/create_coupon_assignment_sheet.py b/sheets/management/commands/create_coupon_assignment_sheet.py index 1ce373c75..c1396ba77 100644 --- a/sheets/management/commands/create_coupon_assignment_sheet.py +++ b/sheets/management/commands/create_coupon_assignment_sheet.py @@ -6,7 +6,7 @@ from sheets.coupon_request_api import CouponRequestHandler, CouponRequestRow from sheets.models import CouponGenerationRequest -from sheets.utils import spreadsheet_repr, assignment_sheet_file_name +from sheets.utils import assignment_sheet_file_name, spreadsheet_repr class Command(BaseCommand): @@ -16,14 +16,14 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-r", "--row", type=int, help="Row number in the request Sheet" ) - def handle(self, *args, **options): # pylint:disable=missing-docstring + def handle(self, *args, **options): # noqa: ARG002, D102 if not options["row"]: - raise CommandError("Need to specify -r/--row") + raise CommandError("Need to specify -r/--row") # noqa: EM101 row_index = options["row"] coupon_request_handler = CouponRequestHandler() @@ -35,7 +35,7 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring ).first() if coupon_gen_request is None: raise CommandError( - "No coupon generation request found for coupon name '{}'. " + "No coupon generation request found for coupon name '{}'. " # noqa: EM103 "This coupon request has probably not been processed yet.".format( coupon_req_row.coupon_name ) @@ -50,7 +50,7 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring already_exists = True if already_exists: raise CommandError( - "A spreadsheet already exists with the file name that would be created for this request ({})".format( + "A spreadsheet already exists with the file name that would be created for this request ({})".format( # noqa: EM103, UP032 spreadsheet_file_name ) ) @@ -58,7 +58,7 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring spreadsheet = coupon_request_handler.create_assignment_sheet(coupon_req_row) self.stdout.write( self.style.SUCCESS( - "Coupon assignment Sheet created ({})".format( + "Coupon assignment Sheet created ({})".format( # noqa: UP032 spreadsheet_repr(spreadsheet) ) ) diff --git a/sheets/management/commands/process_coupon_assignment_sheet.py b/sheets/management/commands/process_coupon_assignment_sheet.py index 13957fbcb..be2776dce 100644 --- a/sheets/management/commands/process_coupon_assignment_sheet.py +++ b/sheets/management/commands/process_coupon_assignment_sheet.py @@ -5,10 +5,10 @@ from django.core.management import BaseCommand, CommandError from ecommerce.models import BulkCouponAssignment -from sheets.api import get_authorized_pygsheets_client, ExpandedSheetsClient +from sheets.api import ExpandedSheetsClient, get_authorized_pygsheets_client from sheets.coupon_assign_api import CouponAssignmentHandler -from sheets.utils import spreadsheet_repr, google_date_string_to_datetime from sheets.management.utils import get_assignment_spreadsheet_by_title +from sheets.utils import google_date_string_to_datetime, spreadsheet_repr class Command(BaseCommand): @@ -19,7 +19,7 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 group = parser.add_mutually_exclusive_group() group.add_argument( "-i", @@ -43,9 +43,9 @@ def add_arguments(self, parser): # pylint:disable=missing-docstring ) super().add_arguments(parser) - def handle(self, *args, **options): # pylint:disable=missing-docstring + def handle(self, *args, **options): # noqa: ARG002, D102 if not options["id"] and not options["title"]: - raise CommandError("Need to provide --id or --title") + raise CommandError("Need to provide --id or --title") # noqa: EM101 pygsheets_client = get_authorized_pygsheets_client() # Fetch the correct spreadsheet @@ -57,7 +57,7 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring ) # Process the sheet self.stdout.write( - "Found spreadsheet ({}). Processing...".format( + "Found spreadsheet ({}). Processing...".format( # noqa: UP032 spreadsheet_repr(spreadsheet) ) ) @@ -77,7 +77,7 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring and not options["force"] ): raise CommandError( - "Spreadsheet is unchanged since it was last processed (%s, last modified: %s). " + "Spreadsheet is unchanged since it was last processed (%s, last modified: %s). " # noqa: UP031 "Add the '-f/--force' flag to process it anyway." % (spreadsheet_repr(spreadsheet), sheet_last_modified.isoformat()) ) diff --git a/sheets/management/commands/process_coupon_requests.py b/sheets/management/commands/process_coupon_requests.py index 49cc1aa6d..e6a51204d 100644 --- a/sheets/management/commands/process_coupon_requests.py +++ b/sheets/management/commands/process_coupon_requests.py @@ -15,17 +15,17 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-r", "--row", type=int, help="Row number in the request Sheet" ) - def handle(self, *args, **options): # pylint:disable=missing-docstring + def handle(self, *args, **options): # noqa: ARG002, D102 coupon_request_handler = CouponRequestHandler() self.stdout.write("Creating coupons and creating/updating Sheets...") results = coupon_request_handler.process_sheet( limit_row_index=options.get("row", None) ) self.stdout.write( - self.style.SUCCESS("Coupon generation succeeded.\n{}".format(results)) + self.style.SUCCESS("Coupon generation succeeded.\n{}".format(results)) # noqa: UP032 ) diff --git a/sheets/management/commands/process_deferral_requests.py b/sheets/management/commands/process_deferral_requests.py index f88f68c60..eb8340cc3 100644 --- a/sheets/management/commands/process_deferral_requests.py +++ b/sheets/management/commands/process_deferral_requests.py @@ -15,12 +15,12 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-r", "--row", type=int, help="Row number in the deferral request Sheet" ) - def handle(self, *args, **options): # pylint:disable=missing-docstring + def handle(self, *args, **options): # noqa: ARG002, D102 defer_request_handler = DeferralRequestHandler() self.stdout.write("Handling deferrals and updating spreadsheet...") results = defer_request_handler.process_sheet( @@ -28,6 +28,6 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring ) self.stdout.write( self.style.SUCCESS( - "Deferral sheet successfully processed.\n{}".format(results) + "Deferral sheet successfully processed.\n{}".format(results) # noqa: UP032 ) ) diff --git a/sheets/management/commands/process_refund_requests.py b/sheets/management/commands/process_refund_requests.py index 7dec0df61..12590d5ab 100644 --- a/sheets/management/commands/process_refund_requests.py +++ b/sheets/management/commands/process_refund_requests.py @@ -15,12 +15,12 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-r", "--row", type=int, help="Row number in the refund request Sheet" ) - def handle(self, *args, **options): # pylint:disable=missing-docstring + def handle(self, *args, **options): # noqa: ARG002, D102 refund_request_handler = RefundRequestHandler() self.stdout.write("Handling refunds and updating spreadsheet...") results = refund_request_handler.process_sheet( @@ -28,6 +28,6 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring ) self.stdout.write( self.style.SUCCESS( - "Refund sheet successfully processed.\n{}".format(results) + "Refund sheet successfully processed.\n{}".format(results) # noqa: UP032 ) ) diff --git a/sheets/management/commands/setup_file_watch.py b/sheets/management/commands/setup_file_watch.py index 0aabef7be..dce7e94a9 100644 --- a/sheets/management/commands/setup_file_watch.py +++ b/sheets/management/commands/setup_file_watch.py @@ -1,23 +1,23 @@ """ Makes a request to receive push notifications when xPro spreadsheets are updated """ -from collections import namedtuple import sys +from collections import namedtuple from django.core.management import BaseCommand from googleapiclient.errors import HttpError from sheets.api import ( create_or_renew_sheet_file_watch, - request_file_watch, get_sheet_metadata_from_type, + request_file_watch, ) +from sheets.constants import SHEET_TYPE_COUPON_ASSIGN, VALID_SHEET_TYPES from sheets.coupon_assign_api import fetch_webhook_eligible_assign_sheet_ids -from sheets.constants import VALID_SHEET_TYPES, SHEET_TYPE_COUPON_ASSIGN from sheets.models import FileWatchRenewalAttempt -SheetMap = namedtuple("SheetMap", ["metadata", "file_ids"]) -FileWatchResult = namedtuple( +SheetMap = namedtuple("SheetMap", ["metadata", "file_ids"]) # noqa: PYI024 +FileWatchResult = namedtuple( # noqa: PYI024 "FileWatchResult", ["file_watch", "metadata", "created", "updated"] ) @@ -29,7 +29,7 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 parser.add_argument( "-s", "--sheet-type", @@ -68,10 +68,11 @@ def add_arguments(self, parser): # pylint:disable=missing-docstring ), ) - def handle( - self, *args, **options - ): # pylint:disable=missing-docstring,too-many-branches,too-many-locals - + def handle( # noqa: D102, C901 + self, + *args, # noqa: ARG002 + **options, + ): sheet_dict = {} # Build a map of sheets that should be renewed (and specific file IDs if applicable) @@ -106,7 +107,7 @@ def handle( # Make requests to renew the file watches for the given sheets and record the results file_watch_results = [] - for sheet_type, sheet_map in sheet_dict.items(): + for sheet_type, sheet_map in sheet_dict.items(): # noqa: B007 for file_id in sheet_map.file_ids: file_watch, created, updated = create_or_renew_sheet_file_watch( sheet_map.metadata, force=options["force"], sheet_file_id=file_id @@ -135,7 +136,7 @@ def handle( ) ) self.style.ERROR( - "Failed to create/update file watch.{}".format(error_msg) + "Failed to create/update file watch.{}".format(error_msg) # noqa: UP032 ) continue if file_watch_result.created: @@ -146,7 +147,7 @@ def handle( desc = "found (unexpired)" file_id_desc = "" if file_watch_result.metadata.sheet_type == SHEET_TYPE_COUPON_ASSIGN: - file_id_desc = " (file id: {})".format(file_watch.file_id) + file_id_desc = " (file id: {})".format(file_watch.file_id) # noqa: UP032 self.stdout.write( self.style.SUCCESS( @@ -197,7 +198,7 @@ def handle( ) else: self.stdout.write( - self.style.ERROR("Request failed: {}".format(exc)) + self.style.ERROR("Request failed: {}".format(exc)) # noqa: UP032 ) sys.exit(1) else: diff --git a/sheets/management/commands/sync_assignment_sheet.py b/sheets/management/commands/sync_assignment_sheet.py index 668596e9b..1e75f496d 100644 --- a/sheets/management/commands/sync_assignment_sheet.py +++ b/sheets/management/commands/sync_assignment_sheet.py @@ -19,7 +19,7 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 group = parser.add_mutually_exclusive_group() group.add_argument("--id", type=int, help="The BulkCouponAssignment ID") group.add_argument( @@ -38,22 +38,24 @@ def add_arguments(self, parser): # pylint:disable=missing-docstring ) super().add_arguments(parser) - def handle( - self, *args, **options - ): # pylint:disable=missing-docstring,too-many-locals + def handle( # noqa: D102 + self, + *args, # noqa: ARG002 + **options, + ): if not any([options["id"], options["sheet_id"], options["title"]]): - raise CommandError("Need to provide --id, --sheet-id, or --title") + raise CommandError("Need to provide --id, --sheet-id, or --title") # noqa: EM101 if options["id"]: - qset_kwargs = dict(id=options["id"]) + qset_kwargs = dict(id=options["id"]) # noqa: C408 elif options["sheet_id"]: - qset_kwargs = dict(assignment_sheet_id=options["sheet_id"]) + qset_kwargs = dict(assignment_sheet_id=options["sheet_id"]) # noqa: C408 else: pygsheets_client = get_authorized_pygsheets_client() spreadsheet = get_assignment_spreadsheet_by_title( pygsheets_client, options["title"] ) - qset_kwargs = dict(assignment_sheet_id=spreadsheet.id) + qset_kwargs = dict(assignment_sheet_id=spreadsheet.id) # noqa: C408 bulk_assignment = BulkCouponAssignment.objects.get(**qset_kwargs) coupon_assignment_handler = CouponAssignmentHandler( @@ -80,7 +82,7 @@ def handle( ) row_update_summary = "\n".join( [ - "Row: {}, Status: {}".format( + "Row: {}, Status: {}".format( # noqa: UP032 row_update.row_index, row_update.status ) for row_update in row_updates diff --git a/sheets/management/commands/update_assignment_message_statuses.py b/sheets/management/commands/update_assignment_message_statuses.py index 559e9459a..4af1690f0 100644 --- a/sheets/management/commands/update_assignment_message_statuses.py +++ b/sheets/management/commands/update_assignment_message_statuses.py @@ -18,7 +18,7 @@ class Command(BaseCommand): help = __doc__ - def add_arguments(self, parser): # pylint:disable=missing-docstring + def add_arguments(self, parser): # noqa: D102 group = parser.add_mutually_exclusive_group() group.add_argument("--id", type=int, help="The BulkCouponAssignment ID") group.add_argument( @@ -38,20 +38,20 @@ def add_arguments(self, parser): # pylint:disable=missing-docstring ) super().add_arguments(parser) - def handle(self, *args, **options): # pylint:disable=missing-docstring + def handle(self, *args, **options): # noqa: ARG002, D102 if not any([options["id"], options["sheet_id"], options["title"]]): - raise CommandError("Need to provide --id, --sheet-id, or --title") + raise CommandError("Need to provide --id, --sheet-id, or --title") # noqa: EM101 if options["id"]: - qset_kwargs = dict(id=options["id"]) + qset_kwargs = dict(id=options["id"]) # noqa: C408 elif options["sheet_id"]: - qset_kwargs = dict(assignment_sheet_id=options["sheet_id"]) + qset_kwargs = dict(assignment_sheet_id=options["sheet_id"]) # noqa: C408 else: pygsheets_client = get_authorized_pygsheets_client() spreadsheet = get_assignment_spreadsheet_by_title( pygsheets_client, options["title"] ) - qset_kwargs = dict(assignment_sheet_id=spreadsheet.id) + qset_kwargs = dict(assignment_sheet_id=spreadsheet.id) # noqa: C408 bulk_assignment = BulkCouponAssignment.objects.get(**qset_kwargs) self.stdout.write( @@ -67,7 +67,7 @@ def handle(self, *args, **options): # pylint:disable=missing-docstring if update_count: self.stdout.write( self.style.SUCCESS( - "Successfully updated message status for bulk coupon assignment " + "Successfully updated message status for bulk coupon assignment " # noqa: UP032 "({} individual status(es) added/updated).".format(update_count) ) ) diff --git a/sheets/management/utils.py b/sheets/management/utils.py index 23ab43907..c03c3182f 100644 --- a/sheets/management/utils.py +++ b/sheets/management/utils.py @@ -18,13 +18,13 @@ def get_assignment_spreadsheet_by_title(pygsheets_client, title): pygsheets.spreadsheet.Spreadsheet: A pygsheets spreadsheet object """ matching_spreadsheets = pygsheets_client.open_all( - "{base_query} and name contains '{title}'".format( + "{base_query} and name contains '{title}'".format( # noqa: UP032 base_query=CouponAssignmentHandler.ASSIGNMENT_SHEETS_QUERY, title=title ) ) if len(matching_spreadsheets) != 1: raise CommandError( - "There should be 1 coupon assignment sheet that matches the given title ('{}'). " + "There should be 1 coupon assignment sheet that matches the given title ('{}'). " # noqa: EM103 "{} were found.".format(title, len(matching_spreadsheets)) ) return matching_spreadsheets[0] diff --git a/sheets/management/utils_test.py b/sheets/management/utils_test.py index 53989b541..eabdea17c 100644 --- a/sheets/management/utils_test.py +++ b/sheets/management/utils_test.py @@ -3,6 +3,7 @@ import pytest from django.core.management import CommandError + from sheets.management import utils diff --git a/sheets/migrations/0001_initial.py b/sheets/migrations/0001_initial.py index b8276bbaf..890e80c4c 100644 --- a/sheets/migrations/0001_initial.py +++ b/sheets/migrations/0001_initial.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.4 on 2019-10-22 20:28 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] diff --git a/sheets/migrations/0002_nullable_google_api_auth_user.py b/sheets/migrations/0002_nullable_google_api_auth_user.py index 256321ce7..4f186954f 100644 --- a/sheets/migrations/0002_nullable_google_api_auth_user.py +++ b/sheets/migrations/0002_nullable_google_api_auth_user.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.4 on 2019-11-01 16:07 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("sheets", "0001_initial")] operations = [ diff --git a/sheets/migrations/0003_remove_coupongenerationrequest_spreadsheet_updated.py b/sheets/migrations/0003_remove_coupongenerationrequest_spreadsheet_updated.py index b4d41a237..2e9e3edd4 100644 --- a/sheets/migrations/0003_remove_coupongenerationrequest_spreadsheet_updated.py +++ b/sheets/migrations/0003_remove_coupongenerationrequest_spreadsheet_updated.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0002_nullable_google_api_auth_user")] operations = [ diff --git a/sheets/migrations/0004_rename_transaction_id.py b/sheets/migrations/0004_rename_transaction_id.py index f3ec12b80..115161adc 100644 --- a/sheets/migrations/0004_rename_transaction_id.py +++ b/sheets/migrations/0004_rename_transaction_id.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("sheets", "0003_remove_coupongenerationrequest_spreadsheet_updated") ] diff --git a/sheets/migrations/0005_googlefilewatch.py b/sheets/migrations/0005_googlefilewatch.py index c4b56ae66..f90384dd6 100644 --- a/sheets/migrations/0005_googlefilewatch.py +++ b/sheets/migrations/0005_googlefilewatch.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0004_rename_transaction_id")] operations = [ diff --git a/sheets/migrations/0006_coupon_gen_request_err_handling_fields.py b/sheets/migrations/0006_coupon_gen_request_err_handling_fields.py index 5eacee32f..b6322ec00 100644 --- a/sheets/migrations/0006_coupon_gen_request_err_handling_fields.py +++ b/sheets/migrations/0006_coupon_gen_request_err_handling_fields.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0005_googlefilewatch")] operations = [ diff --git a/sheets/migrations/0007_fill_in_gen_request_date_completed.py b/sheets/migrations/0007_fill_in_gen_request_date_completed.py index 23b4b7644..928303f72 100644 --- a/sheets/migrations/0007_fill_in_gen_request_date_completed.py +++ b/sheets/migrations/0007_fill_in_gen_request_date_completed.py @@ -36,7 +36,6 @@ def fill_in_date_completed(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("sheets", "0006_coupon_gen_request_err_handling_fields")] operations = [ diff --git a/sheets/migrations/0008_remove_coupongenerationrequest_completed.py b/sheets/migrations/0008_remove_coupongenerationrequest_completed.py index fe9fd489b..dada8b057 100644 --- a/sheets/migrations/0008_remove_coupongenerationrequest_completed.py +++ b/sheets/migrations/0008_remove_coupongenerationrequest_completed.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0007_fill_in_gen_request_date_completed")] operations = [ diff --git a/sheets/migrations/0009_add_gen_request_coupon_name.py b/sheets/migrations/0009_add_gen_request_coupon_name.py index 48c9f2ce8..7825e15a0 100644 --- a/sheets/migrations/0009_add_gen_request_coupon_name.py +++ b/sheets/migrations/0009_add_gen_request_coupon_name.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0008_remove_coupongenerationrequest_completed")] operations = [ diff --git a/sheets/migrations/0010_fill_in_gen_request_coupon_name.py b/sheets/migrations/0010_fill_in_gen_request_coupon_name.py index ad2a1afb0..5a157d743 100644 --- a/sheets/migrations/0010_fill_in_gen_request_coupon_name.py +++ b/sheets/migrations/0010_fill_in_gen_request_coupon_name.py @@ -24,12 +24,12 @@ def fill_in_coupon_name(apps, schema_editor): for coupon_gen_request in coupon_gen_requests: try: raw_data = json.loads(coupon_gen_request.raw_data) - if not isinstance(raw_data, list) or len(raw_data) < 2 or not raw_data[1]: - raise ValueError( - "raw_data is either not a list, or does not include a valid coupon name" + if not isinstance(raw_data, list) or len(raw_data) < 2 or not raw_data[1]: # noqa: PLR2004 + raise ValueError( # noqa: TRY301 + "raw_data is either not a list, or does not include a valid coupon name" # noqa: EM101 ) - except Exception: - coupon_name = "COUPON NAME NEEDED ({})".format(coupon_gen_request.id) + except Exception: # noqa: BLE001 + coupon_name = "COUPON NAME NEEDED ({})".format(coupon_gen_request.id) # noqa: UP032 else: coupon_name = raw_data[1] coupon_gen_request.coupon_name = coupon_name @@ -37,7 +37,6 @@ def fill_in_coupon_name(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("sheets", "0009_add_gen_request_coupon_name")] operations = [migrations.RunPython(fill_in_coupon_name, set_coupon_name_to_none)] diff --git a/sheets/migrations/0011_gen_request_coupon_name_index.py b/sheets/migrations/0011_gen_request_coupon_name_index.py index be7cc41f7..080874d36 100644 --- a/sheets/migrations/0011_gen_request_coupon_name_index.py +++ b/sheets/migrations/0011_gen_request_coupon_name_index.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0010_fill_in_gen_request_coupon_name")] operations = [ diff --git a/sheets/migrations/0012_refundrequest.py b/sheets/migrations/0012_refundrequest.py index e56900a07..55f870d30 100644 --- a/sheets/migrations/0012_refundrequest.py +++ b/sheets/migrations/0012_refundrequest.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0011_gen_request_coupon_name_index")] operations = [ diff --git a/sheets/migrations/0013_deferralrequest.py b/sheets/migrations/0013_deferralrequest.py index 7f8200724..67b14a12f 100644 --- a/sheets/migrations/0013_deferralrequest.py +++ b/sheets/migrations/0013_deferralrequest.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0012_refundrequest")] operations = [ diff --git a/sheets/migrations/0014_file_watch_for_assignment_sheets.py b/sheets/migrations/0014_file_watch_for_assignment_sheets.py index ea0a87db7..f84fb6fb1 100644 --- a/sheets/migrations/0014_file_watch_for_assignment_sheets.py +++ b/sheets/migrations/0014_file_watch_for_assignment_sheets.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0013_deferralrequest")] operations = [ diff --git a/sheets/migrations/0015_googlefilewatch_last_request_received.py b/sheets/migrations/0015_googlefilewatch_last_request_received.py index 109b6b9fe..8154d0e32 100644 --- a/sheets/migrations/0015_googlefilewatch_last_request_received.py +++ b/sheets/migrations/0015_googlefilewatch_last_request_received.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0014_file_watch_for_assignment_sheets")] operations = [ diff --git a/sheets/migrations/0016_remove_file_watch_expiration_idx.py b/sheets/migrations/0016_remove_file_watch_expiration_idx.py index 81607d149..de1f48ae3 100644 --- a/sheets/migrations/0016_remove_file_watch_expiration_idx.py +++ b/sheets/migrations/0016_remove_file_watch_expiration_idx.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0015_googlefilewatch_last_request_received")] operations = [ diff --git a/sheets/migrations/0017_filewatchrenewalattempt.py b/sheets/migrations/0017_filewatchrenewalattempt.py index 19f6eafb9..9792bbf71 100644 --- a/sheets/migrations/0017_filewatchrenewalattempt.py +++ b/sheets/migrations/0017_filewatchrenewalattempt.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("sheets", "0016_remove_file_watch_expiration_idx")] operations = [ diff --git a/sheets/models.py b/sheets/models.py index e8a80c175..83b077917 100644 --- a/sheets/models.py +++ b/sheets/models.py @@ -1,10 +1,10 @@ """Sheets app models""" from django.conf import settings -from django.db import models from django.core.exceptions import ValidationError +from django.db import models from django.db.models import DateTimeField, Model, PositiveSmallIntegerField -from mitxpro.models import TimestampedModel, SingletonModel +from mitxpro.models import SingletonModel, TimestampedModel from sheets.constants import VALID_SHEET_TYPES @@ -15,7 +15,7 @@ class GoogleApiAuth(TimestampedModel, SingletonModel): settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True ) access_token = models.CharField(max_length=2048) - refresh_token = models.CharField(null=True, max_length=512) + refresh_token = models.CharField(null=True, max_length=512) # noqa: DJ001 class CouponGenerationRequest(TimestampedModel): @@ -24,7 +24,7 @@ class CouponGenerationRequest(TimestampedModel): purchase_order_id = models.CharField(max_length=100, null=False) coupon_name = models.CharField(max_length=256, db_index=True, null=False) date_completed = models.DateTimeField(null=True, blank=True) - raw_data = models.CharField(max_length=300, null=True, blank=True) + raw_data = models.CharField(max_length=300, null=True, blank=True) # noqa: DJ001 def __str__(self): return "CouponGenerationRequest: id={}, coupon_name={}, purchase_order_id={}, completed={}".format( @@ -40,7 +40,7 @@ class EnrollmentChangeRequestModel(TimestampedModel): form_response_id = models.IntegerField(db_index=True, unique=True, null=False) date_completed = models.DateTimeField(null=True, blank=True) - raw_data = models.CharField(max_length=300, null=True, blank=True) + raw_data = models.CharField(max_length=300, null=True, blank=True) # noqa: DJ001 class Meta: abstract = True @@ -80,15 +80,19 @@ class GoogleFileWatch(TimestampedModel): class Meta: unique_together = ("file_id", "version") - def save( - self, force_insert=False, force_update=False, using=None, update_fields=None + def save( # noqa: D102 + self, + force_insert=False, # noqa: FBT002 + force_update=False, # noqa: FBT002 + using=None, + update_fields=None, ): if ( force_insert and self._meta.model.objects.filter(file_id=self.file_id).count() > 0 ): raise ValidationError( - "Only one {} object should exist for each unique file_id (file_id provided: {}). " + "Only one {} object should exist for each unique file_id (file_id provided: {}). " # noqa: EM103 "Update the existing object instead of creating a new one.".format( self.__class__.__name__, self.file_id ) @@ -106,7 +110,7 @@ def __str__(self): ) -class FileWatchRenewalAttempt(Model): +class FileWatchRenewalAttempt(Model): # noqa: DJ008 """ Tracks attempts to renew a Google file watch. Used for debugging flaky endpoint. """ @@ -119,5 +123,5 @@ class FileWatchRenewalAttempt(Model): ) sheet_file_id = models.CharField(max_length=100, db_index=True, null=False) date_attempted = DateTimeField(auto_now_add=True) - result = models.CharField(max_length=300, null=True, blank=True) + result = models.CharField(max_length=300, null=True, blank=True) # noqa: DJ001 result_status_code = PositiveSmallIntegerField(null=True, blank=True) diff --git a/sheets/models_test.py b/sheets/models_test.py index 0d0258ce5..a7fa1b97f 100644 --- a/sheets/models_test.py +++ b/sheets/models_test.py @@ -1,6 +1,5 @@ """Tests for sheets models""" import pytest - from django.core.exceptions import ValidationError from sheets.factories import GoogleApiAuthFactory diff --git a/sheets/refund_request_api.py b/sheets/refund_request_api.py index f3fabf6d6..09238fa78 100644 --- a/sheets/refund_request_api.py +++ b/sheets/refund_request_api.py @@ -12,13 +12,13 @@ from ecommerce.models import Order from mitxpro.utils import now_in_utc from sheets.constants import ( - REFUND_SHEET_ORDER_TYPE_PAID, - REFUND_SHEET_ORDER_TYPE_FULL_COUPON, GOOGLE_API_TRUE_VAL, + REFUND_SHEET_ORDER_TYPE_FULL_COUPON, + REFUND_SHEET_ORDER_TYPE_PAID, ) -from sheets.sheet_handler_api import EnrollmentChangeRequestHandler from sheets.exceptions import SheetRowParsingException from sheets.models import RefundRequest +from sheets.sheet_handler_api import EnrollmentChangeRequestHandler from sheets.utils import ( ResultType, RowResult, @@ -31,10 +31,10 @@ User = get_user_model() -class RefundRequestRow: # pylint: disable=too-many-instance-attributes +class RefundRequestRow: """Represents a row of the refund request sheet""" - def __init__( + def __init__( # noqa: PLR0913 self, row_index, response_id, @@ -52,7 +52,7 @@ def __init__( refund_complete_date, errors, skip_row, - ): # pylint: disable=too-many-arguments,too-many-locals + ): self.row_index = row_index self.response_id = response_id self.request_date = request_date @@ -109,7 +109,7 @@ def parse_raw_data(cls, row_index, raw_row_data): == GOOGLE_API_TRUE_VAL ), ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 raise SheetRowParsingException(str(exc)) from exc @@ -168,7 +168,7 @@ def reverse_order_and_enrollments(order, enrollment): ) # When #1838 is completed, this logic can be removed if deactivated_enrollment is None: - raise Exception("Enrollment change failed in edX") + raise Exception("Enrollment change failed in edX") # noqa: EM101, TRY002 order.status = Order.REFUNDED order.save_and_log(acting_user=None) @@ -200,9 +200,7 @@ def is_ready_for_reversal(refund_req_row): ) ) - def process_row( - self, row_index, row_data - ): # pylint: disable=too-many-return-statements + def process_row(self, row_index, row_data): """ Ensures that the given spreadsheet row is correctly represented in the database, attempts to parse it, reverses/refunds the given enrollment if appropriate, and returns the @@ -227,7 +225,7 @@ def process_row( row_db_record=refund_request, row_object=None, result_type=ResultType.FAILED, - message="Parsing failure: {}".format(str(exc)), + message="Parsing failure: {}".format(str(exc)), # noqa: UP032 ) is_unchanged_error_row = ( refund_req_row.errors and not request_created and not request_updated @@ -253,13 +251,13 @@ def process_row( ) if not self.is_ready_for_reversal(refund_req_row): - return + return # noqa: RET502 try: order, enrollment = self.get_order_objects(refund_req_row) except ObjectDoesNotExist as exc: if isinstance(exc, User.DoesNotExist): - message = "User with email '{}' not found".format( + message = "User with email '{}' not found".format( # noqa: UP032 refund_req_row.learner_email ) elif isinstance(exc, Order.DoesNotExist): diff --git a/sheets/sheet_handler_api.py b/sheets/sheet_handler_api.py index 7ad37b623..acfaedab8 100644 --- a/sheets/sheet_handler_api.py +++ b/sheets/sheet_handler_api.py @@ -1,7 +1,7 @@ """API with general functionality for all enrollment change spreadsheets""" import json -import operator as op import logging +import operator as op from django.conf import settings from django.db import transaction @@ -15,11 +15,11 @@ GOOGLE_SHEET_FIRST_ROW, ) from sheets.utils import ( - get_data_rows, - get_data_rows_after_start, - format_datetime_for_sheet_formula, ResultType, RowResult, + format_datetime_for_sheet_formula, + get_data_rows, + get_data_rows_after_start, ) log = logging.getLogger(__name__) @@ -77,7 +77,7 @@ def update_row_errors(self, failed_row_results): """ for row_result in failed_row_results: self.worksheet.update_value( - "{}{}".format( + "{}{}".format( # noqa: UP032 self.sheet_metadata.ERROR_COL_LETTER, row_result.row_index ), row_result.message, @@ -207,13 +207,13 @@ def process_sheet(self, limit_row_index=None): row_result = None try: row_result = self.process_row(row_index, row_data) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # noqa: BLE001 row_result = RowResult( row_index=row_index, row_db_record=None, row_object=None, result_type=ResultType.FAILED, - message="Error: {}".format(str(exc)), + message="Error: {}".format(str(exc)), # noqa: UP032 ) finally: if row_result: @@ -255,10 +255,10 @@ def __init__(self, worksheet_id, start_row, sheet_metadata, request_model_cls): self.request_model_cls = request_model_cls @cached_property - def worksheet(self): + def worksheet(self): # noqa: D102 return self.spreadsheet.worksheet("id", value=self.worksheet_id) - def get_enumerated_rows(self): + def get_enumerated_rows(self): # noqa: D102 # Only yield rows in the spreadsheet that come after the legacy rows # (i.e.: the rows of data that were manually entered before we started automating this process) return enumerate( @@ -271,7 +271,7 @@ def get_enumerated_rows(self): start=self.start_row, ) - def update_completed_rows(self, success_row_results): + def update_completed_rows(self, success_row_results): # noqa: D102 for row_result in success_row_results: self.worksheet.update_values( crange="{processor_col}{row_index}:{error_col}{row_index}".format( @@ -292,7 +292,7 @@ def update_completed_rows(self, success_row_results): ], ) - def get_or_create_request(self, row_data): + def get_or_create_request(self, row_data): # noqa: D102 form_response_id = int( row_data[self.sheet_metadata.FORM_RESPONSE_ID_COL].strip() ) @@ -305,7 +305,7 @@ def get_or_create_request(self, row_data): created, ) = self.request_model_cls.objects.select_for_update().get_or_create( form_response_id=form_response_id, - defaults=dict(raw_data=user_input_json), + defaults=dict(raw_data=user_input_json), # noqa: C408 ) raw_data_changed = enroll_change_request.raw_data != user_input_json if raw_data_changed: @@ -313,7 +313,7 @@ def get_or_create_request(self, row_data): enroll_change_request.save() return enroll_change_request, created, raw_data_changed - def filter_ignored_rows(self, enumerated_rows): + def filter_ignored_rows(self, enumerated_rows): # noqa: D102 completed_form_response_ids = set( self.request_model_cls.objects.exclude(date_completed=None).values_list( "form_response_id", flat=True @@ -336,5 +336,5 @@ def filter_ignored_rows(self, enumerated_rows): continue yield row_index, row_data - def process_row(self, row_index, row_data): + def process_row(self, row_index, row_data): # noqa: D102 raise NotImplementedError diff --git a/sheets/tasks.py b/sheets/tasks.py index 7355dba7a..9e83f4b1f 100644 --- a/sheets/tasks.py +++ b/sheets/tasks.py @@ -3,25 +3,27 @@ from datetime import datetime, timedelta from itertools import chain, repeat -from googleapiclient.errors import HttpError -from django.conf import settings import celery +from django.conf import settings +from googleapiclient.errors import HttpError from ecommerce.models import BulkCouponAssignment from mitxpro.celery import app -from mitxpro.utils import now_in_utc, case_insensitive_equal +from mitxpro.utils import case_insensitive_equal, now_in_utc from sheets import ( api as sheets_api, +) +from sheets import ( coupon_assign_api, coupon_request_api, - refund_request_api, deferral_request_api, + refund_request_api, ) from sheets.constants import ( ASSIGNMENT_SHEET_ENROLLED_STATUS, + SHEET_TYPE_COUPON_ASSIGN, SHEET_TYPE_COUPON_REQUEST, SHEET_TYPE_ENROLL_CHANGE, - SHEET_TYPE_COUPON_ASSIGN, ) from sheets.utils import AssignmentRowUpdate @@ -36,8 +38,7 @@ def handle_unprocessed_coupon_requests(): the necessary coupon assignment sheets. """ coupon_request_handler = coupon_request_api.CouponRequestHandler() - results = coupon_request_handler.process_sheet() - return results + return coupon_request_handler.process_sheet() @app.task @@ -48,8 +49,7 @@ def handle_unprocessed_refund_requests(): made, and returns a summary of those changes. """ refund_request_handler = refund_request_api.RefundRequestHandler() - results = refund_request_handler.process_sheet() - return results + return refund_request_handler.process_sheet() @app.task @@ -60,8 +60,7 @@ def handle_unprocessed_deferral_requests(): made, and returns a summary of those changes. """ deferral_request_handler = deferral_request_api.DeferralRequestHandler() - results = deferral_request_handler.process_sheet() - return results + return deferral_request_handler.process_sheet() @app.task @@ -76,7 +75,8 @@ def process_coupon_assignment_sheet(*, file_id, change_date=None): """ change_dt = datetime.fromisoformat(change_date) if change_date else now_in_utc() bulk_assignment, _ = BulkCouponAssignment.objects.update_or_create( - assignment_sheet_id=file_id, defaults=dict(sheet_last_modified_date=change_dt) + assignment_sheet_id=file_id, + defaults=dict(sheet_last_modified_date=change_dt), # noqa: C408 ) coupon_assignment_handler = coupon_assign_api.CouponAssignmentHandler( spreadsheet_id=file_id, bulk_assignment=bulk_assignment @@ -151,7 +151,7 @@ def schedule_coupon_assignment_sheet_handling(file_id): @app.task def update_incomplete_assignment_delivery_statuses(): """ - Fetches all BulkCouponAssignments that have assignments but have not yet finished delivery, then updates the + Fetch all BulkCouponAssignments that have assignments but have not yet finished delivery, then updates the delivery status for each depending on what has been sent. """ bulk_assignments = coupon_assign_api.fetch_update_eligible_bulk_assignments() @@ -235,8 +235,8 @@ def renew_file_watch(*, sheet_type, file_id): ) return { "type": sheet_metadata.sheet_type, - "file_watch_channel_id": getattr(file_watch, "channel_id"), - "file_watch_file_id": getattr(file_watch, "file_id"), + "file_watch_channel_id": getattr(file_watch, "channel_id"), # noqa: B009 + "file_watch_file_id": getattr(file_watch, "file_id"), # noqa: B009 "created": created, } diff --git a/sheets/tasks_test.py b/sheets/tasks_test.py index 285efea4e..4794b3094 100644 --- a/sheets/tasks_test.py +++ b/sheets/tasks_test.py @@ -2,8 +2,8 @@ import pytest from sheets.tasks import ( - handle_unprocessed_coupon_requests, _get_scheduled_assignment_task_ids, + handle_unprocessed_coupon_requests, ) diff --git a/sheets/utils.py b/sheets/utils.py index 4872735f3..073f95c66 100644 --- a/sheets/utils.py +++ b/sheets/utils.py @@ -2,26 +2,26 @@ import datetime import email.utils from collections import namedtuple -from urllib.parse import urljoin, quote_plus from enum import Enum +from urllib.parse import quote_plus, urljoin from django.conf import settings from django.urls import reverse from mitxpro.utils import matching_item_index from sheets.constants import ( - GOOGLE_AUTH_URI, - GOOGLE_TOKEN_URI, - GOOGLE_AUTH_PROVIDER_X509_CERT_URL, ASSIGNMENT_SHEET_PREFIX, - GOOGLE_SHEET_FIRST_ROW, + GOOGLE_AUTH_PROVIDER_X509_CERT_URL, + GOOGLE_AUTH_URI, GOOGLE_SERVICE_ACCOUNT_EMAIL_DOMAIN, - SHEETS_VALUE_REQUEST_PAGE_SIZE, - SHEET_TYPE_COUPON_REQUEST, - WORKSHEET_TYPE_REFUND, + GOOGLE_SHEET_FIRST_ROW, + GOOGLE_TOKEN_URI, SHEET_TYPE_COUPON_ASSIGN, - WORKSHEET_TYPE_DEFERRAL, + SHEET_TYPE_COUPON_REQUEST, SHEET_TYPE_ENROLL_CHANGE, + SHEETS_VALUE_REQUEST_PAGE_SIZE, + WORKSHEET_TYPE_DEFERRAL, + WORKSHEET_TYPE_REFUND, ) @@ -44,7 +44,7 @@ def generate_google_client_config(): def get_column_letter(column_index): """ - Returns the spreadsheet column letter that corresponds to a given index (e.g.: 0 -> 'A', 3 -> 'D') + Return the spreadsheet column letter that corresponds to a given index (e.g.: 0 -> 'A', 3 -> 'D') Args: column_index (int): @@ -52,8 +52,8 @@ def get_column_letter(column_index): Returns: str: The column index expressed as a letter """ - if column_index > 25: - raise ValueError("Cannot generate a column letter past 'Z'") + if column_index > 25: # noqa: PLR2004 + raise ValueError("Cannot generate a column letter past 'Z'") # noqa: EM101 uppercase_a_ord = ord("A") return chr(column_index + uppercase_a_ord) @@ -81,7 +81,7 @@ def form_input_column_indices(self): def handler_url_stub(self, file_id=None): """ - Returns the URL that Google should send requests to when a change is made to a watched + Return the URL that Google should send requests to when a change is made to a watched spreadsheet. Args: @@ -91,7 +91,7 @@ def handler_url_stub(self, file_id=None): Returns: str: The URL that Google will send file watch requests to """ - params = dict(sheet=quote_plus(self.sheet_type)) + params = dict(sheet=quote_plus(self.sheet_type)) # noqa: C408 if file_id: params["fileId"] = file_id param_str = "&".join([f"{k}={v}" for k, v in params.items()]) @@ -99,7 +99,7 @@ def handler_url_stub(self, file_id=None): def get_form_input_columns(self, row_data): """ - Returns a list of column values for columns that contain data entered by a user in a form + Return a list of column values for columns that contain data entered by a user in a form (i.e.: no auto-generated values, or values entered by this app) Args: @@ -121,9 +121,7 @@ class SingletonSheetMetadata(SheetMetadata): sheet_file_id = None -class CouponRequestSheetMetadata( - SingletonSheetMetadata -): # pylint: disable=too-many-instance-attributes +class CouponRequestSheetMetadata(SingletonSheetMetadata): """Metadata for the coupon request spreadsheet""" PURCHASE_ORDER_COL_INDEX = 0 @@ -132,7 +130,7 @@ class CouponRequestSheetMetadata( ERROR_COL = settings.SHEETS_REQ_ERROR_COL SKIP_ROW_COL = ERROR_COL + 1 - def __init__(self): # pylint: disable=too-many-instance-attributes + def __init__(self): self.sheet_type = SHEET_TYPE_COUPON_REQUEST self.sheet_name = "Coupon Request sheet" self.first_data_row = GOOGLE_SHEET_FIRST_ROW + 1 @@ -144,9 +142,7 @@ def __init__(self): # pylint: disable=too-many-instance-attributes self.ERROR_COL_LETTER = get_column_letter(self.ERROR_COL) -class RefundRequestSheetMetadata( - SingletonSheetMetadata -): # pylint: disable=too-many-instance-attributes +class RefundRequestSheetMetadata(SingletonSheetMetadata): """Metadata for the refund request spreadsheet""" FORM_RESPONSE_ID_COL = 0 @@ -165,7 +161,7 @@ def __init__(self): self.num_columns = self.SKIP_ROW_COL + 1 self.non_input_column_indices = set( # Response ID column - [self.FORM_RESPONSE_ID_COL] + [self.FORM_RESPONSE_ID_COL] # noqa: RUF005 + # Every column from the finance columns to the end of the row list(range(8, self.num_columns)) @@ -176,9 +172,7 @@ def __init__(self): self.ERROR_COL_LETTER = get_column_letter(self.ERROR_COL) -class DeferralRequestSheetMetadata( - SingletonSheetMetadata -): # pylint: disable=too-many-instance-attributes +class DeferralRequestSheetMetadata(SingletonSheetMetadata): """Metadata for the deferral request spreadsheet""" FORM_RESPONSE_ID_COL = 0 @@ -196,7 +190,7 @@ def __init__(self): self.num_columns = self.SKIP_ROW_COL + 1 self.non_input_column_indices = set( # Response ID column - [self.FORM_RESPONSE_ID_COL] + [self.FORM_RESPONSE_ID_COL] # noqa: RUF005 + # Every column from the finance columns to the end of the row list(range(self.PROCESSOR_COL, self.num_columns)) @@ -207,9 +201,7 @@ def __init__(self): self.ERROR_COL_LETTER = get_column_letter(self.ERROR_COL) -class CouponAssignSheetMetadata( - SheetMetadata -): # pylint: disable=too-many-instance-attributes +class CouponAssignSheetMetadata(SheetMetadata): """Metadata for a coupon assignment spreadsheet""" def __init__(self): @@ -255,27 +247,27 @@ class ResultType(Enum): PROCESSED = "processed" def __lt__(self, other): - return self.value < other.value # pylint: disable=comparison-with-callable + return self.value < other.value -RowResult = namedtuple( +RowResult = namedtuple( # noqa: PYI024 "RowResult", ["row_index", "row_db_record", "row_object", "message", "result_type"] ) -ProcessedRequest = namedtuple( +ProcessedRequest = namedtuple( # noqa: PYI024 "ProcessedRequest", ["row_index", "coupon_req_row", "request_id", "date_processed"] ) -FailedRequest = namedtuple( +FailedRequest = namedtuple( # noqa: PYI024 "FailedRequest", ["row_index", "exception", "sheet_error_text"] ) -IgnoredRequest = namedtuple("IgnoredRequest", ["row_index", "coupon_req_row", "reason"]) -AssignmentRowUpdate = namedtuple( +IgnoredRequest = namedtuple("IgnoredRequest", ["row_index", "coupon_req_row", "reason"]) # noqa: PYI024 +AssignmentRowUpdate = namedtuple( # noqa: PYI024 "AssignmentRowUpdate", ["row_index", "status", "status_date", "alternate_email"] ) def assignment_sheet_file_name(coupon_req_row): """ - Generates the filename for a coupon assignment Sheet + Generate the filename for a coupon assignment Sheet Args: coupon_req_row (sheets.coupon_request_api.CouponRequestRow): @@ -283,7 +275,7 @@ def assignment_sheet_file_name(coupon_req_row): Returns: str: File name for a coupon assignment Sheet """ - return " - ".join( + return " - ".join( # noqa: FLY002 [ ASSIGNMENT_SHEET_PREFIX, coupon_req_row.company_name, @@ -293,9 +285,9 @@ def assignment_sheet_file_name(coupon_req_row): ) -def get_data_rows(worksheet, include_trailing_empty=False): +def get_data_rows(worksheet, include_trailing_empty=False): # noqa: FBT002 """ - Yields the data rows of a spreadsheet that has a header row + Yield the data rows of a spreadsheet that has a header row Args: worksheet (pygsheets.worksheet.Worksheet): Worksheet object @@ -328,7 +320,7 @@ def get_data_rows_after_start( **kwargs, ): """ - Yields the data rows of a spreadsheet starting with a given row and spanning a given column range + Yield the data rows of a spreadsheet starting with a given row and spanning a given column range until empty rows are encountered. Args: @@ -361,7 +353,7 @@ def get_data_rows_after_start( def spreadsheet_repr(spreadsheet=None, spreadsheet_metadata=None): """ - Returns a simple string representation of a Spreadsheet object + Return a simple string representation of a Spreadsheet object Args: spreadsheet (pygsheets.spreadsheet.Spreadsheet or None): @@ -377,13 +369,13 @@ def spreadsheet_repr(spreadsheet=None, spreadsheet_metadata=None): else: sheet_id, title = None, None if not sheet_id or not title: - raise ValueError("Invalid spreadsheet/metadata provided") - return "'{}', id: {}".format(title, sheet_id) + raise ValueError("Invalid spreadsheet/metadata provided") # noqa: EM101 + return f"'{title}', id: {sheet_id}" def clean_sheet_value(value): """ - Takes a spreadsheet cell value and returns a cleaned version + Take a spreadsheet cell value and returns a cleaned version Args: value (str): A raw spreadsheet cell value @@ -410,8 +402,8 @@ def format_datetime_for_google_api(dt): def format_datetime_for_google_timestamp(dt): """ - Formats a datetime for use in a Google API request that expects a timestamp - (e.g.: file watch expiration – https://developers.google.com/drive/api/v3/reference/files/watch#request-body) + Format a datetime for use in a Google API request that expects a timestamp + (e.g.: file watch expiration - https://developers.google.com/drive/api/v3/reference/files/watch#request-body) Args: dt (datetime.datetime): @@ -451,7 +443,7 @@ def format_datetime_for_sheet_formula(dt): def _parse_sheet_date_str(date_str, date_format): """ - Parses a string that represents a date/datetime and returns the UTC datetime (or None) + Parse a string that represents a date/datetime and returns the UTC datetime (or None) Args: date_str (str): The date/datetime string @@ -467,14 +459,14 @@ def _parse_sheet_date_str(date_str, date_format): ) return ( dt - if settings.SHEETS_DATE_TIMEZONE == datetime.timezone.utc + if settings.SHEETS_DATE_TIMEZONE == datetime.timezone.utc # noqa: SIM300 else dt.astimezone(datetime.timezone.utc) ) def parse_sheet_datetime_str(datetime_str): """ - Parses a string that represents a datetime and returns the UTC datetime (or None) + Parse a string that represents a datetime and returns the UTC datetime (or None) Args: datetime_str (str): The datetime string @@ -487,7 +479,7 @@ def parse_sheet_datetime_str(datetime_str): def parse_sheet_date_only_str(date_str): """ - Parses a string that represents a date and returns the UTC datetime (or None) + Parse a string that represents a date and returns the UTC datetime (or None) Args: date_str (str): The datetime string @@ -500,7 +492,7 @@ def parse_sheet_date_only_str(date_str): def google_timestamp_to_datetime(google_timestamp): """ - Parses a timestamp value from a Google API response as a normal datetime (UTC) + Parse a timestamp value from a Google API response as a normal datetime (UTC) Args: google_timestamp (str or int): A timestamp value from a Google API response @@ -515,7 +507,7 @@ def google_timestamp_to_datetime(google_timestamp): def google_date_string_to_datetime(google_date_str): """ - Parses a datetime string value from a Google API response as a normal datetime (UTC) + Parse a datetime string value from a Google API response as a normal datetime (UTC) Args: google_date_str (str): A datetime string value from a Google API response @@ -530,7 +522,7 @@ def google_date_string_to_datetime(google_date_str): def mailgun_timestamp_to_datetime(timestamp): """ - Parses a timestamp value from a Mailgun API response as a datetime + Parse a timestamp value from a Mailgun API response as a datetime Args: timestamp (float): A timestamp value from a Mailgun API response @@ -545,7 +537,7 @@ def build_multi_cell_update_request_body( row_index, column_index, values, worksheet_id=0 ): """ - Builds a dict for use in the body of a Google Sheets API batch update request + Build a dict for use in the body of a Google Sheets API batch update request Args: row_index (int): The index of the cell row that should be updated (starting with 0) @@ -571,17 +563,17 @@ def build_multi_cell_update_request_body( } -def build_protected_range_request_body( +def build_protected_range_request_body( # noqa: PLR0913 start_row_index, num_rows, start_col_index, num_cols, worksheet_id=0, - warning_only=False, + warning_only=False, # noqa: FBT002 description=None, -): # pylint: disable=too-many-arguments +): """ - Builds a request body that will be sent to the Google Sheets API to create a protected range on a spreadsheet. + Build a request body that will be sent to the Google Sheets API to create a protected range on a spreadsheet. Args: start_row_index (int): The zero-based index of the row of the range that will be protected @@ -616,7 +608,7 @@ def build_protected_range_request_body( def build_drive_file_email_share_request(file_id, email_to_share): """ - Builds the body of a Drive file share request + Build the body of a Drive file share request Args: file_id (str): The file id of the Drive file being shared diff --git a/sheets/utils_test.py b/sheets/utils_test.py index 8df4edc19..e45e013f2 100644 --- a/sheets/utils_test.py +++ b/sheets/utils_test.py @@ -2,18 +2,18 @@ from pygsheets.worksheet import Worksheet +from sheets import utils from sheets.constants import ( + GOOGLE_AUTH_PROVIDER_X509_CERT_URL, GOOGLE_AUTH_URI, GOOGLE_TOKEN_URI, - GOOGLE_AUTH_PROVIDER_X509_CERT_URL, ) -from sheets import utils def test_generate_google_client_config(settings): """generate_google_client_config should return a dict with expected values""" settings.DRIVE_CLIENT_ID = "some-id" - settings.DRIVE_CLIENT_SECRET = "some-secret" + settings.DRIVE_CLIENT_SECRET = "some-secret" # noqa: S105 settings.DRIVE_API_PROJECT_ID = "some-project-id" settings.SITE_BASE_URL = "http://example.com" assert utils.generate_google_client_config() == { @@ -35,7 +35,7 @@ def test_get_data_rows(mocker): ["row 1 - column 1", "row 1 - column 2"], ["row 2 - column 1", "row 2 - column 2"], ] - sheet_rows = [["HEADER 1", "HEADER 2"]] + non_header_rows + sheet_rows = [["HEADER 1", "HEADER 2"]] + non_header_rows # noqa: RUF005 mocked_worksheet = mocker.MagicMock( spec=Worksheet, get_all_values=mocker.Mock(return_value=sheet_rows) ) diff --git a/sheets/views.py b/sheets/views.py index d247eaa23..b31cc8354 100644 --- a/sheets/views.py +++ b/sheets/views.py @@ -4,33 +4,26 @@ from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required -from django.views.decorators.http import require_http_methods from django.db import transaction -from django.views.decorators.csrf import csrf_exempt -from django.http import HttpResponse, Http404 -from django.shortcuts import render, redirect +from django.http import Http404, HttpResponse +from django.shortcuts import redirect, render from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt +from google.auth.exceptions import GoogleAuthError +from google_auth_oauthlib.flow import Flow from rest_framework import status -# NOTE: Due to an unresolved bug (https://github.com/PyCQA/pylint/issues/2108), the -# `google` package (and other packages without an __init__.py file) will break pylint. -# The `disable-all` rules are here until that bug is fixed. -from google_auth_oauthlib.flow import Flow # pylint: disable-all -from google.auth.exceptions import GoogleAuthError # pylint: disable-all - from mitxpro.utils import now_in_utc +from sheets import tasks from sheets.api import get_sheet_metadata_from_type -from sheets.models import GoogleApiAuth, GoogleFileWatch from sheets.constants import ( REQUIRED_GOOGLE_API_SCOPES, + SHEET_TYPE_COUPON_ASSIGN, SHEET_TYPE_COUPON_REQUEST, SHEET_TYPE_ENROLL_CHANGE, - SHEET_TYPE_COUPON_ASSIGN, ) +from sheets.models import GoogleApiAuth, GoogleFileWatch from sheets.utils import generate_google_client_config -from sheets import tasks -from sheets.coupon_assign_api import CouponAssignmentHandler -from sheets.coupon_request_api import CouponRequestHandler log = logging.getLogger(__name__) @@ -77,7 +70,7 @@ def complete_google_auth(request): state = request.session.get("state") if not state: raise GoogleAuthError( - "Could not complete Google auth - 'state' was not found in the session" + "Could not complete Google auth - 'state' was not found in the session" # noqa: EM101 ) flow = Flow.from_client_config( generate_google_client_config(), scopes=REQUIRED_GOOGLE_API_SCOPES, state=state @@ -117,8 +110,8 @@ def handle_watched_sheet_update(request): sheet_type = request.GET.get("sheet", SHEET_TYPE_COUPON_REQUEST) try: sheet_metadata = get_sheet_metadata_from_type(sheet_type) - except: - log.error( + except: # noqa: E722 + log.error( # noqa: TRY400 "Unknown sheet type '%s' (passed via 'sheet' query parameter)", sheet_type ) return HttpResponse(status=status.HTTP_400_BAD_REQUEST) @@ -139,7 +132,7 @@ def handle_watched_sheet_update(request): req_sheet_file_watch.last_request_received = now_in_utc() req_sheet_file_watch.save() except GoogleFileWatch.DoesNotExist: - log.error( + log.error( # noqa: TRY400 "Google file watch request for %s received (%s), but no local file watch record exists " "in the database.", sheet_metadata.sheet_name, diff --git a/sheets/views_test.py b/sheets/views_test.py index 08e655509..b1784b8bc 100644 --- a/sheets/views_test.py +++ b/sheets/views_test.py @@ -1,18 +1,18 @@ """Tests for sheets app views""" import pytest -from django.urls import reverse from django.test.client import Client, RequestFactory +from django.urls import reverse from rest_framework import status -from sheets.views import complete_google_auth -from sheets.models import GoogleApiAuth -from sheets.factories import GoogleApiAuthFactory, GoogleFileWatchFactory from mitxpro.test_utils import set_request_session +from sheets.factories import GoogleApiAuthFactory, GoogleFileWatchFactory +from sheets.models import GoogleApiAuth +from sheets.views import complete_google_auth lazy = pytest.lazy_fixture -@pytest.fixture() +@pytest.fixture def google_api_auth(user): """Fixture that creates a google auth object""" return GoogleApiAuthFactory.create(requesting_user=user) @@ -55,16 +55,14 @@ def test_request_auth(mocker, settings, staff_client): @pytest.mark.parametrize("existing_auth", [lazy("google_api_auth"), None]) @pytest.mark.django_db -def test_complete_auth( - mocker, settings, user, existing_auth -): # pylint: disable=unused-argument +def test_complete_auth(mocker, settings, user, existing_auth): """ View that handles Google auth completion should fetch a token and save/update a GoogleApiAuth object """ settings.SITE_BASE_URL = "http://example.com" - access_token = "access-token-123" - refresh_token = "refresh-token-123" + access_token = "access-token-123" # noqa: S105 + refresh_token = "refresh-token-123" # noqa: S105 code = "auth-code" flow_mock = mocker.Mock( credentials=mocker.Mock(token=access_token, refresh_token=refresh_token) diff --git a/users/admin.py b/users/admin.py index 0e30d53a3..32b552d1f 100644 --- a/users/admin.py +++ b/users/admin.py @@ -5,7 +5,7 @@ from hijack.contrib.admin import HijackUserAdminMixin from mitxpro.admin import TimestampedModelAdmin -from users.models import LegalAddress, User, Profile, BlockList +from users.models import BlockList, LegalAddress, Profile, User class UserLegalAddressInline(admin.StackedInline): @@ -33,7 +33,7 @@ class UserLegalAddressInline(admin.StackedInline): ), ) - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return False @@ -43,7 +43,7 @@ class UserProfileInline(admin.StackedInline): model = Profile classes = ["collapse"] - def has_delete_permission(self, request, obj=None): + def has_delete_permission(self, request, obj=None): # noqa: ARG002, D102 return True @@ -101,5 +101,5 @@ class BlockListAdmin(admin.ModelAdmin): model = BlockList list_display = ("hashed_email",) - def has_add_permission(self, request): + def has_add_permission(self, request): # noqa: ARG002, D102 return False diff --git a/users/api.py b/users/api.py index c8242b6cc..c154a16ee 100644 --- a/users/api.py +++ b/users/api.py @@ -1,14 +1,14 @@ """Users api""" +import operator import re from functools import reduce -import operator -from django.db.models import Q +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.core.validators import validate_email -from django.contrib.auth import get_user_model +from django.db.models import Q -from mitxpro.utils import first_or_none, unique, unique_ignore_case, max_or_none +from mitxpro.utils import first_or_none, max_or_none, unique, unique_ignore_case from users.constants import USERNAME_MAX_LEN User = get_user_model() @@ -18,7 +18,7 @@ def get_user_by_id(user_id): """ - Gets a User by id + Get a User by id Args: user_id (int): the user id to fetch @@ -31,7 +31,7 @@ def get_user_by_id(user_id): def _is_case_insensitive_searchable(field_name): """ - Indicates whether or not a given field in the User model is a string field + Indicate whether or not a given field in the User model is a string field Args: field_name (str): The name of the User field @@ -56,14 +56,14 @@ def _determine_filter_field(user_property): else: try: validate_email(user_property) - return "email" + return "email" # noqa: TRY300 except ValidationError: return "username" -def fetch_user(filter_value, ignore_case=True): +def fetch_user(filter_value, ignore_case=True): # noqa: FBT002 """ - Attempts to fetch a user based on several properties + Attempt to fetch a user based on several properties Args: filter_value (Union[str, int]): The id, email, or username of some User @@ -74,14 +74,14 @@ def fetch_user(filter_value, ignore_case=True): filter_field = _determine_filter_field(filter_value) if _is_case_insensitive_searchable(filter_field) and ignore_case: - query = {"{}__iexact".format(filter_field): filter_value} + query = {"{}__iexact".format(filter_field): filter_value} # noqa: UP032 else: query = {filter_field: filter_value} try: return User.objects.get(**query) except User.DoesNotExist as e: raise User.DoesNotExist( - "Could not find User with {}={} ({})".format( + "Could not find User with {}={} ({})".format( # noqa: EM103 filter_field, filter_value, "case-insensitive" if ignore_case else "case-sensitive", @@ -89,9 +89,9 @@ def fetch_user(filter_value, ignore_case=True): ) from e -def fetch_users(filter_values, ignore_case=True): +def fetch_users(filter_values, ignore_case=True): # noqa: FBT002 """ - Attempts to fetch a set of users based on several properties. The property being searched + Attempt to fetch a set of users based on several properties. The property being searched (i.e.: id, email, or username) is assumed to be the same for all of the given values, so the property type is determined for the first element, then used for all of the values provided. @@ -115,7 +115,7 @@ def fetch_users(filter_values, ignore_case=True): ) if len(filter_values) > len(unique_filter_values): raise ValidationError( - "Duplicate values provided ({})".format( + "Duplicate values provided ({})".format( # noqa: EM103 set(filter_values).intersection(unique_filter_values) ) ) @@ -124,23 +124,23 @@ def fetch_users(filter_values, ignore_case=True): query = reduce( operator.or_, ( - Q(**{"{}__iexact".format(filter_field): filter_value}) + Q(**{"{}__iexact".format(filter_field): filter_value}) # noqa: UP032 for filter_value in filter_values ), ) user_qset = User.objects.filter(query) else: user_qset = User.objects.filter( - **{"{}__in".format(filter_field): filter_values} + **{"{}__in".format(filter_field): filter_values} # noqa: UP032 ) - if not user_qset.count() == len(filter_values): + if user_qset.count() != len(filter_values): valid_values = user_qset.values_list(filter_field, flat=True) invalid_values = set(filter_values) - set(valid_values) raise User.DoesNotExist( - "Could not find Users with these '{}' values ({}): {}".format( + "Could not find Users with these '{}' values ({}): {}".format( # noqa: EM103 filter_field, "case-insensitive" if ignore_case else "case-sensitive", - sorted(list(invalid_values)), + sorted(invalid_values), ) ) return user_qset @@ -148,7 +148,7 @@ def fetch_users(filter_values, ignore_case=True): def find_available_username(initial_username_base): """ - Returns a username with the lowest possible suffix given some base username. If the applied suffix + Return a username with the lowest possible suffix given some base username. If the applied suffix makes the username longer than the username max length, characters are removed from the right of the username to make room. @@ -177,13 +177,13 @@ def find_available_username(initial_username_base): # Any query for suffixed usernames could come up empty. The minimum suffix will be added to # the username in that case. current_min_suffix = 1 - while letters_to_truncate < len(initial_username_base): + while letters_to_truncate < len(initial_username_base): # noqa: RET503 username_base = initial_username_base[ 0 : len(initial_username_base) - letters_to_truncate ] # Find usernames that match the username base and have a numerical suffix, then find the max suffix existing_usernames = User.objects.filter( - username__regex=r"{username_base}[0-9]+".format(username_base=username_base) + username__regex=r"{username_base}[0-9]+".format(username_base=username_base) # noqa: UP032 ).values_list("username", flat=True) max_suffix = max_or_none( int(re.search(r"\d+$", username).group()) for username in existing_usernames diff --git a/users/api_test.py b/users/api_test.py index 67c78dd78..8174c4b96 100644 --- a/users/api_test.py +++ b/users/api_test.py @@ -1,12 +1,11 @@ """Tests for user api""" -import pytest import factory - +import pytest from django.contrib.auth import get_user_model -from users.api import get_user_by_id, fetch_user, fetch_users, find_available_username -from users.utils import usernameify +from users.api import fetch_user, fetch_users, find_available_username, get_user_by_id from users.factories import UserFactory +from users.utils import usernameify User = get_user_model() @@ -18,12 +17,12 @@ def test_get_user_by_id(user): @pytest.mark.django_db @pytest.mark.parametrize( - "prop,value,db_value", + "prop,value,db_value", # noqa: PT006 [ - ["username", "abcdefgh", None], - ["id", 100, None], - ["id", "100", 100], - ["email", "abc@example.com", None], + ["username", "abcdefgh", None], # noqa: PT007 + ["id", 100, None], # noqa: PT007 + ["id", "100", 100], # noqa: PT007 + ["email", "abc@example.com", None], # noqa: PT007 ], ) def test_fetch_user(prop, value, db_value): @@ -56,12 +55,12 @@ def test_fetch_user_fail(): @pytest.mark.django_db @pytest.mark.parametrize( - "prop,values,db_values", + "prop,values,db_values", # noqa: PT006 [ - ["username", ["abcdefgh", "ijklmnop", "qrstuvwxyz"], None], - ["id", [100, 101, 102], None], - ["id", ["100", "101", "102"], [100, 101, 102]], - ["email", ["abc@example.com", "def@example.com", "ghi@example.com"], None], + ["username", ["abcdefgh", "ijklmnop", "qrstuvwxyz"], None], # noqa: PT007 + ["id", [100, 101, 102], None], # noqa: PT007 + ["id", ["100", "101", "102"], [100, 101, 102]], # noqa: PT007 + ["email", ["abc@example.com", "def@example.com", "ghi@example.com"], None], # noqa: PT007 ], ) def test_fetch_users(prop, values, db_values): @@ -89,11 +88,11 @@ def test_fetch_users_case_sens(): @pytest.mark.django_db @pytest.mark.parametrize( - "prop,existing_values,missing_values", + "prop,existing_values,missing_values", # noqa: PT006 [ - ["username", ["abcdefgh"], ["ijklmnop", "qrstuvwxyz"]], - ["id", [100], [101, 102]], - ["email", ["abc@example.com"], ["def@example.com", "ghi@example.com"]], + ["username", ["abcdefgh"], ["ijklmnop", "qrstuvwxyz"]], # noqa: PT007 + ["id", [100], [101, 102]], # noqa: PT007 + ["email", ["abc@example.com"], ["def@example.com", "ghi@example.com"]], # noqa: PT007 ], ) def test_fetch_users_fail(prop, existing_values, missing_values): @@ -105,19 +104,19 @@ def test_fetch_users_fail(prop, existing_values, missing_values): UserFactory.create_batch( len(existing_values), **{prop: factory.Iterator(existing_values)} ) - expected_missing_value_output = str(sorted(list(missing_values))) + expected_missing_value_output = str(sorted(missing_values)) with pytest.raises(User.DoesNotExist, match=expected_missing_value_output): fetch_users(fetch_users_values) @pytest.mark.django_db @pytest.mark.parametrize( - "username_base,suffixed_to_create,expected_available_username", + "username_base,suffixed_to_create,expected_available_username", # noqa: PT006 [ - ["someuser", 0, "someuser1"], - ["someuser", 5, "someuser6"], - ["abcdefghij", 10, "abcdefgh11"], - ["abcdefghi", 99, "abcdefg100"], + ["someuser", 0, "someuser1"], # noqa: PT007 + ["someuser", 5, "someuser6"], # noqa: PT007 + ["abcdefghij", 10, "abcdefgh11"], # noqa: PT007 + ["abcdefghi", 99, "abcdefg100"], # noqa: PT007 ], ) def test_find_available_username( @@ -132,7 +131,7 @@ def suffixed_username_generator(): """Generator for usernames with suffixes that will not exceed the username character limit""" for suffix_int in range(1, suffixed_to_create + 1): suffix = str(suffix_int) - username = "{}{}".format(username_base, suffix) + username = f"{username_base}{suffix}" if len(username) <= temp_username_max_len: yield username else: @@ -160,7 +159,7 @@ def test_full_username_creation(): generated_username = usernameify(user_full_name) assert len(generated_username) == expected_username_max UserFactory.create(username=generated_username, name=user_full_name) - new_user_full_name = "{} Jr.".format(user_full_name) + new_user_full_name = f"{user_full_name} Jr." new_generated_username = usernameify(new_user_full_name) assert new_generated_username == generated_username available_username = find_available_username(new_generated_username) diff --git a/users/factories.py b/users/factories.py index 572e1669f..d3118e8c4 100644 --- a/users/factories.py +++ b/users/factories.py @@ -2,9 +2,9 @@ import pycountry from factory import ( Faker, - Trait, - SubFactory, RelatedFactory, + SubFactory, + Trait, fuzzy, lazy_attribute, random, @@ -13,7 +13,7 @@ from factory.fuzzy import FuzzyText from social_django.models import UserSocialAuth -from users.models import LegalAddress, Profile, User, GENDER_CHOICES +from users.models import GENDER_CHOICES, LegalAddress, Profile, User class UserFactory(DjangoModelFactory): @@ -72,7 +72,7 @@ def state_or_territory(self): return "" subdivisions = pycountry.subdivisions.get(country_code=self.country) subdivision = random.randgen.sample(subdivisions, 1)[0] - # Example: "US-MA" + # Example: "US-MA" # noqa: ERA001 return subdivision.code class Meta: diff --git a/users/management/commands/block_users.py b/users/management/commands/block_users.py index 91d6823a7..12e8d6d74 100644 --- a/users/management/commands/block_users.py +++ b/users/management/commands/block_users.py @@ -2,9 +2,11 @@ block user(s) from MIT xPRO """ -from argparse import RawTextHelpFormatter import sys +from argparse import RawTextHelpFormatter + from django.core.management import BaseCommand + from authentication.utils import block_user_email from mail.api import validate_email_addresses from mail.exceptions import MultiEmailValidationError @@ -25,7 +27,7 @@ class Command(BaseCommand): `./manage.py block_users --user=foo@email.com --user=bar@email.com --user=abc@email.com` or do \n """ - def create_parser(self, prog_name, subcommand): # pylint: disable=arguments-differ + def create_parser(self, prog_name, subcommand): """ create parser to add new line in help text. """ @@ -34,9 +36,7 @@ def create_parser(self, prog_name, subcommand): # pylint: disable=arguments-dif return parser def add_arguments(self, parser): - """parse arguments""" - - # pylint: disable=expression-not-assigned + """Parse arguments""" parser.add_argument( "-u", "--user", @@ -46,7 +46,7 @@ def add_arguments(self, parser): help="Single or multiple username(s) or email(s)", ) - def handle(self, *args, **kwargs): + def handle(self, *args, **kwargs): # noqa: ARG002, D102 users = kwargs.get("users", []) if not users: self.stderr.write( diff --git a/users/management/commands/retire_users.py b/users/management/commands/retire_users.py index edd106f3c..b6bced67a 100644 --- a/users/management/commands/retire_users.py +++ b/users/management/commands/retire_users.py @@ -1,21 +1,18 @@ """ Retire user(s) from MIT xPRO """ -import hashlib +import sys from argparse import RawTextHelpFormatter from urllib.parse import urlparse -import sys from django.contrib.auth import get_user_model from django.core.management import BaseCommand -from authentication.utils import block_user_email from social_django.models import UserSocialAuth - from user_util import user_util -from users.api import fetch_users -from users.models import BlockList +from authentication.utils import block_user_email from mitxpro import settings +from users.api import fetch_users User = get_user_model() @@ -48,7 +45,7 @@ class Command(BaseCommand): `./manage.py retire_users -u foo@email.com -b` \n or do \n """ - def create_parser(self, prog_name, subcommand): # pylint: disable=arguments-differ + def create_parser(self, prog_name, subcommand): """ create parser to add new line in help text. """ @@ -57,9 +54,7 @@ def create_parser(self, prog_name, subcommand): # pylint: disable=arguments-dif return parser def add_arguments(self, parser): - """parse arguments""" - - # pylint: disable=expression-not-assigned + """Parse arguments""" parser.add_argument( "-u", "--user", @@ -81,7 +76,7 @@ def get_retired_email(self, email): """Convert user email to retired email format.""" return user_util.get_retired_email(email, RETIRED_USER_SALTS, RETIRED_EMAIL_FMT) - def handle(self, *args, **kwargs): + def handle(self, *args, **kwargs): # noqa: ARG002, D102 users = kwargs.get("users", []) block_users = kwargs.get("block_users") @@ -96,11 +91,11 @@ def handle(self, *args, **kwargs): users = fetch_users(kwargs["users"]) for user in users: - self.stdout.write("Retiring user: {user}".format(user=user)) + self.stdout.write("Retiring user: {user}".format(user=user)) # noqa: UP032 if not user.is_active: self.stdout.write( self.style.ERROR( - "User: '{user}' is already deactivated in MIT xPRO".format( + "User: '{user}' is already deactivated in MIT xPRO".format( # noqa: UP032 user=user ) ) @@ -122,7 +117,7 @@ def handle(self, *args, **kwargs): user.save() self.stdout.write( - "Email changed from {email} to {retired_email} and password is not useable now".format( + "Email changed from {email} to {retired_email} and password is not useable now".format( # noqa: UP032 email=email, retired_email=user.email ) ) @@ -132,11 +127,11 @@ def handle(self, *args, **kwargs): if auth_deleted_count: self.stdout.write( - "For user: '{user}' SocialAuth rows deleted".format(user=user) + "For user: '{user}' SocialAuth rows deleted".format(user=user) # noqa: UP032 ) self.stdout.write( self.style.SUCCESS( - "User: '{user}' is retired from MIT xPRO".format(user=user) + "User: '{user}' is retired from MIT xPRO".format(user=user) # noqa: UP032 ) ) diff --git a/users/management/commands/unblock_users.py b/users/management/commands/unblock_users.py index 69dbca69d..5b7a6b6df 100644 --- a/users/management/commands/unblock_users.py +++ b/users/management/commands/unblock_users.py @@ -1,8 +1,8 @@ """ Unblock user(s) from MIT xPRO """ -from argparse import RawTextHelpFormatter import sys +from argparse import RawTextHelpFormatter from django.contrib.auth import get_user_model from django.core.management import BaseCommand @@ -12,7 +12,6 @@ from mail.exceptions import MultiEmailValidationError from users.models import BlockList - User = get_user_model() @@ -32,7 +31,7 @@ class Command(BaseCommand): `./manage.py unblock_users --user=foo@email.com --user=bar@email.com --user=abc@email.com` or do \n """ - def create_parser(self, prog_name, subcommand): # pylint: disable=arguments-differ + def create_parser(self, prog_name, subcommand): """ create parser to add new line in help text. """ @@ -41,9 +40,7 @@ def create_parser(self, prog_name, subcommand): # pylint: disable=arguments-dif return parser def add_arguments(self, parser): - """parse arguments""" - - # pylint: disable=expression-not-assigned + """Parse arguments""" parser.add_argument( "-u", "--user", @@ -53,7 +50,7 @@ def add_arguments(self, parser): help="Single or multiple email(s)", ) - def handle(self, *args, **kwargs): + def handle(self, *args, **kwargs): # noqa: ARG002, D102 users = kwargs.get("users", []) if not users: self.stderr.write( @@ -82,7 +79,7 @@ def handle(self, *args, **kwargs): blocked_user.delete() self.stdout.write( self.style.SUCCESS( - "Email {email} has been removed from the blocklist of MIT xPRO.".format( + "Email {email} has been removed from the blocklist of MIT xPRO.".format( # noqa: UP032 email=user_email ) ) @@ -90,7 +87,7 @@ def handle(self, *args, **kwargs): else: self.stdout.write( self.style.WARNING( - "Email {email} was not found in the blocklist.".format( + "Email {email} was not found in the blocklist.".format( # noqa: UP032 email=user_email ) ) diff --git a/users/management/tests/block_users_test.py b/users/management/tests/block_users_test.py index 696286cf3..f62dc9141 100644 --- a/users/management/tests/block_users_test.py +++ b/users/management/tests/block_users_test.py @@ -1,5 +1,6 @@ """block user test""" import hashlib + import pytest from django.contrib.auth import get_user_model from django.test import TestCase @@ -18,17 +19,17 @@ class TestblockUsers(TestCase): Tests block users management command. """ - def setUp(self): + def setUp(self): # noqa: D102 super().setUp() @pytest.mark.django_db def test_block_user_blocking_with_email(self): - """test block_users command success with user email""" + """Test block_users command success with user email""" test_email = "test@email.com" user = UserFactory.create(email=test_email, is_active=True) email = user.email - hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() + hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() # noqa: S324 assert BlockList.objects.all().count() == 0 COMMAND.handle("block_users", users=[test_email], block_users=True) @@ -39,11 +40,11 @@ def test_block_user_blocking_with_email(self): @pytest.mark.django_db def test_multiple_success_blocking_user(self): - """test block_users command blocking emails success with more than one user""" + """Test block_users command blocking emails success with more than one user""" test_usernames = ["foo@test.com", "bar@test.com", "baz@test.com"] for username in test_usernames: - user = UserFactory.create(username=username, is_active=True) + UserFactory.create(username=username, is_active=True) assert BlockList.objects.all().count() == 0 COMMAND.handle("block_users", users=test_usernames, block_users=True) @@ -51,14 +52,14 @@ def test_multiple_success_blocking_user(self): @pytest.mark.django_db def test_user_blocking_if_not_requested(self): - """test block_users command exit if not user provided""" + """Test block_users command exit if not user provided""" assert BlockList.objects.all().count() == 0 - with self.assertRaises(SystemExit): + with self.assertRaises(SystemExit): # noqa: PT027 COMMAND.handle("block_users", users=[]) @pytest.mark.django_db def test_user_blocking_with_invalid_email(self): - """test block_users command system exit if not provided a valid email address""" + """Test block_users command system exit if not provided a valid email address""" test_email = "test.com" - with self.assertRaises(SystemExit): + with self.assertRaises(SystemExit): # noqa: PT027 COMMAND.handle("block_users", users=[test_email]) diff --git a/users/management/tests/retire_users_test.py b/users/management/tests/retire_users_test.py index 308515a70..4957830b3 100644 --- a/users/management/tests/retire_users_test.py +++ b/users/management/tests/retire_users_test.py @@ -1,5 +1,6 @@ """retire user test""" import hashlib + import pytest from django.contrib.auth import get_user_model from social_django.models import UserSocialAuth @@ -15,7 +16,7 @@ @pytest.mark.django_db def test_single_success(): - """test retire_users command success with one user""" + """Test retire_users command success with one user""" test_username = "test_user" user = UserFactory.create(username=test_username, is_active=True) @@ -35,7 +36,7 @@ def test_single_success(): @pytest.mark.django_db def test_multiple_success(): - """test retire_users command success with more than one user""" + """Test retire_users command success with more than one user""" test_usernames = ["foo", "bar", "baz"] for username in test_usernames: @@ -57,7 +58,7 @@ def test_multiple_success(): @pytest.mark.django_db def test_retire_user_with_email(): - """test retire_users command success with user email""" + """Test retire_users command success with user email""" test_email = "test@email.com" user = UserFactory.create(email=test_email, is_active=True) @@ -77,13 +78,13 @@ def test_retire_user_with_email(): @pytest.mark.django_db def test_retire_user_blocking_with_email(): - """test retire_users command success with user email""" + """Test retire_users command success with user email""" test_email = "test@email.com" user = UserFactory.create(email=test_email, is_active=True) UserSocialAuthFactory.create(user=user, provider="edX") email = user.email - hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() + hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() # noqa: S324 assert user.is_active is True assert "retired_email" not in user.email assert UserSocialAuth.objects.filter(user=user).count() == 1 @@ -101,7 +102,7 @@ def test_retire_user_blocking_with_email(): @pytest.mark.django_db def test_multiple_success_blocking_user(): - """test retire_users command blocking emails success with more than one user""" + """Test retire_users command blocking emails success with more than one user""" test_usernames = ["foo", "bar", "baz"] for username in test_usernames: @@ -126,13 +127,13 @@ def test_multiple_success_blocking_user(): @pytest.mark.django_db def test_user_blocking_if_not_requested(): - """test retire_users command success but it should not block user(s) if not requested""" + """Test retire_users command success but it should not block user(s) if not requested""" test_email = "test@email.com" user = UserFactory.create(email=test_email, is_active=True) UserSocialAuthFactory.create(user=user, provider="edX") email = user.email - hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() + hashlib.md5(email.lower().encode("utf-8")).hexdigest() # noqa: S324 assert user.is_active is True assert "retired_email" not in user.email assert UserSocialAuth.objects.filter(user=user).count() == 1 diff --git a/users/management/tests/unblock_users_test.py b/users/management/tests/unblock_users_test.py index 63185783d..19579c91f 100644 --- a/users/management/tests/unblock_users_test.py +++ b/users/management/tests/unblock_users_test.py @@ -1,14 +1,14 @@ """retire user test""" import hashlib + import pytest from django.contrib.auth import get_user_model +from django.test import TestCase from social_django.models import UserSocialAuth from users.factories import UserFactory, UserSocialAuthFactory from users.management.commands import retire_users, unblock_users from users.models import BlockList -from django.test import TestCase - User = get_user_model() @@ -18,20 +18,20 @@ class TestUnblockUsers(TestCase): Tests unblock users management command. """ - def setUp(self): + def setUp(self): # noqa: D102 super().setUp() self.RETIRE_USER_COMMAND = retire_users.Command() self.UNBLOCK_USER_COMMAND = unblock_users.Command() @pytest.mark.django_db def test_user_unblocking_with_email(self): - """test unblock_users command success with user email""" + """Test unblock_users command success with user email""" test_email = "test@email.com" user = UserFactory.create(email=test_email, is_active=True) UserSocialAuthFactory.create(user=user, provider="edX") email = user.email - hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() + hashed_email = hashlib.md5(email.lower().encode("utf-8")).hexdigest() # noqa: S324 assert user.is_active is True assert "retired_email" not in user.email assert UserSocialAuth.objects.filter(user=user).count() == 1 @@ -55,7 +55,7 @@ def test_user_unblocking_with_email(self): @pytest.mark.django_db def test_multiple_success_unblocking_user(self): - """test unblock_users command unblocking emails success with more than one user""" + """Test unblock_users command unblocking emails success with more than one user""" test_users = ["foo@email.com", "bar@email.com", "baz@email.com"] for email in test_users: @@ -78,13 +78,13 @@ def test_multiple_success_unblocking_user(self): @pytest.mark.django_db def test_user_unblocking_with_invalid_email(self): - """test unblock_users command system exit if not provided a valid email address""" + """Test unblock_users command system exit if not provided a valid email address""" test_email = "test.com" - with self.assertRaises(SystemExit): + with self.assertRaises(SystemExit): # noqa: PT027 self.UNBLOCK_USER_COMMAND.handle("unblock_users", users=[test_email]) @pytest.mark.django_db def test_user_unblocking_with_no_users(self): - """test unblock_users command system exit if not any users provided""" - with self.assertRaises(SystemExit): + """Test unblock_users command system exit if not any users provided""" + with self.assertRaises(SystemExit): # noqa: PT027 self.UNBLOCK_USER_COMMAND.handle("unblock_users", users=[]) diff --git a/users/migrations/0001_add_user.py b/users/migrations/0001_add_user.py index d8a559cb7..de105629c 100644 --- a/users/migrations/0001_add_user.py +++ b/users/migrations/0001_add_user.py @@ -2,16 +2,16 @@ import ulid from django.db import migrations, models + import users.models def generate_username(): - """Generates a new username""" + """Generate a new username""" return ulid.new().str class Migration(migrations.Migration): - initial = True dependencies = [("auth", "0009_alter_user_last_name_max_length")] diff --git a/users/migrations/0002_add_legal_address.py b/users/migrations/0002_add_legal_address.py index e6bee72b7..96f382ee9 100644 --- a/users/migrations/0002_add_legal_address.py +++ b/users/migrations/0002_add_legal_address.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-04-22 15:24 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("users", "0001_add_user")] operations = [ diff --git a/users/migrations/0003_populate_legal_addresses.py b/users/migrations/0003_populate_legal_addresses.py index 3ee2260ae..0572201b4 100644 --- a/users/migrations/0003_populate_legal_addresses.py +++ b/users/migrations/0003_populate_legal_addresses.py @@ -3,7 +3,7 @@ from django.db import migrations -def add_legal_addresses(apps, schema_editor): +def add_legal_addresses(apps, schema_editor): # noqa: D103 User = apps.get_model("users", "User") LegalAddress = apps.get_model("users", "LegalAddress") @@ -11,13 +11,12 @@ def add_legal_addresses(apps, schema_editor): LegalAddress.objects.create(user=user) -def remove_legal_addresses(apps, schema_editor): +def remove_legal_addresses(apps, schema_editor): # noqa: D103 LegalAddress = apps.get_model("users", "LegalAddress") LegalAddress.objects.all().delete() class Migration(migrations.Migration): - dependencies = [("users", "0002_add_legal_address")] operations = [migrations.RunPython(add_legal_addresses, remove_legal_addresses)] diff --git a/users/migrations/0004_user_legaladdress.py b/users/migrations/0004_user_legaladdress.py index 7e958a3b7..4ea1fcf9e 100644 --- a/users/migrations/0004_user_legaladdress.py +++ b/users/migrations/0004_user_legaladdress.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-04-30 16:43 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("users", "0003_populate_legal_addresses")] operations = [ diff --git a/users/migrations/0005_profile.py b/users/migrations/0005_profile.py index 8cd34455d..7e26082df 100644 --- a/users/migrations/0005_profile.py +++ b/users/migrations/0005_profile.py @@ -1,15 +1,14 @@ # Generated by Django 2.1.7 on 2019-05-08 18:40 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("users", "0004_user_legaladdress")] - def add_profiles(apps, schema_editor): + def add_profiles(apps, schema_editor): # noqa: ARG002, N805 """Create profiles for all existing test users, with some defaults for required fields""" User = apps.get_model("users", "User") Profile = apps.get_model("users", "Profile") diff --git a/users/migrations/0006_user_is_active_false.py b/users/migrations/0006_user_is_active_false.py index 79d778837..3cc842dbd 100644 --- a/users/migrations/0006_user_is_active_false.py +++ b/users/migrations/0006_user_is_active_false.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("users", "0005_profile")] operations = [ diff --git a/users/migrations/0007_validate_country_and_state.py b/users/migrations/0007_validate_country_and_state.py index 880383e6a..ae146fe8c 100644 --- a/users/migrations/0007_validate_country_and_state.py +++ b/users/migrations/0007_validate_country_and_state.py @@ -1,11 +1,11 @@ # Generated by Django 2.1.7 on 2019-06-05 20:56 from django.db import migrations, models + import users.models class Migration(migrations.Migration): - dependencies = [("users", "0006_user_is_active_false")] operations = [ diff --git a/users/migrations/0008_profile_fields_optional.py b/users/migrations/0008_profile_fields_optional.py index 46023337d..4b990f230 100644 --- a/users/migrations/0008_profile_fields_optional.py +++ b/users/migrations/0008_profile_fields_optional.py @@ -27,7 +27,6 @@ def remove_incomplete_profiles(apps, schema): class Migration(migrations.Migration): - dependencies = [("users", "0007_validate_country_and_state")] operations = [ diff --git a/users/migrations/0009_profile_highest_education.py b/users/migrations/0009_profile_highest_education.py index baa5a53ca..9aa310e19 100644 --- a/users/migrations/0009_profile_highest_education.py +++ b/users/migrations/0009_profile_highest_education.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("users", "0008_profile_fields_optional")] operations = [ diff --git a/users/migrations/0010_remove_username_ulid_default.py b/users/migrations/0010_remove_username_ulid_default.py index 6ceecdbad..9533eec88 100644 --- a/users/migrations/0010_remove_username_ulid_default.py +++ b/users/migrations/0010_remove_username_ulid_default.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("users", "0009_profile_highest_education")] operations = [ diff --git a/users/migrations/0011_change_username_max_len_50.py b/users/migrations/0011_change_username_max_len_50.py index 427be81e9..df92f5d9c 100644 --- a/users/migrations/0011_change_username_max_len_50.py +++ b/users/migrations/0011_change_username_max_len_50.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("users", "0010_remove_username_ulid_default")] operations = [ diff --git a/users/migrations/0012_changeemailrequest.py b/users/migrations/0012_changeemailrequest.py index 33ccbcd47..48cdf034c 100644 --- a/users/migrations/0012_changeemailrequest.py +++ b/users/migrations/0012_changeemailrequest.py @@ -1,13 +1,13 @@ # Generated by Django 2.2.8 on 2020-01-17 11:56 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + import users.models class Migration(migrations.Migration): - dependencies = [("users", "0011_change_username_max_len_50")] operations = [ diff --git a/users/migrations/0013_blocklist.py b/users/migrations/0013_blocklist.py index 93621964c..4564cccef 100644 --- a/users/migrations/0013_blocklist.py +++ b/users/migrations/0013_blocklist.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [("users", "0012_changeemailrequest")] operations = [ diff --git a/users/migrations/0014_alter_user_name.py b/users/migrations/0014_alter_user_name.py index 64e969345..c4b60d215 100644 --- a/users/migrations/0014_alter_user_name.py +++ b/users/migrations/0014_alter_user_name.py @@ -13,7 +13,6 @@ def truncate_long_user_names(apps, schema): class Migration(migrations.Migration): - dependencies = [ ("users", "0013_blocklist"), ] diff --git a/users/migrations/0015_legaladdress_vat_id.py b/users/migrations/0015_legaladdress_vat_id.py index dbe4455ac..e71a5b090 100644 --- a/users/migrations/0015_legaladdress_vat_id.py +++ b/users/migrations/0015_legaladdress_vat_id.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("users", "0014_alter_user_name"), ] diff --git a/users/migrations/0016_remove_null_vat_id.py b/users/migrations/0016_remove_null_vat_id.py index 88d75c2ee..d49677afb 100644 --- a/users/migrations/0016_remove_null_vat_id.py +++ b/users/migrations/0016_remove_null_vat_id.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("users", "0015_legaladdress_vat_id"), ] diff --git a/users/models.py b/users/models.py index cb7bf736a..0274c9ee1 100644 --- a/users/models.py +++ b/users/models.py @@ -15,7 +15,6 @@ from mitxpro.models import TimestampedModel from mitxpro.utils import now_in_utc - # Defined in edX Profile model MALE = "m" FEMALE = "f" @@ -112,9 +111,9 @@ def create_superuser(self, username, email, password, **extra_fields): extra_fields.setdefault("is_active", True) if extra_fields.get("is_staff") is not True: - raise ValueError("Superuser must have is_staff=True.") + raise ValueError("Superuser must have is_staff=True.") # noqa: EM101 if extra_fields.get("is_superuser") is not True: - raise ValueError("Superuser must have is_superuser=True.") + raise ValueError("Superuser must have is_superuser=True.") # noqa: EM101 return self._create_user(username, email, password, **extra_fields) @@ -122,7 +121,7 @@ def create_superuser(self, username, email, password, **extra_fields): class FaultyCoursewareUserManager(BaseUserManager): """User manager that defines a queryset of Users that are incorrectly configured in the courseware""" - def get_queryset(self): # pylint: disable=missing-docstring + def get_queryset(self): # noqa: D102 return ( super() .get_queryset() @@ -162,7 +161,7 @@ class User(AbstractBaseUser, TimestampedModel, PermissionsMixin): faulty_courseware_users = FaultyCoursewareUserManager() def get_full_name(self): - """Returns the user's fullname""" + """Return the user's fullname""" return self.name def __str__(self): @@ -171,12 +170,12 @@ def __str__(self): def generate_change_email_code(): - """Generates a new change email code""" + """Generate a new change email code""" return uuid.uuid4().hex def generate_change_email_expires(): - """Generates the expiry datetime for a change email request""" + """Generate the expiry datetime for a change email request""" return now_in_utc() + timedelta(minutes=settings.AUTH_CHANGE_EMAIL_TTL_IN_MINUTES) diff --git a/users/models_test.py b/users/models_test.py index 49f35023c..ed8f79f32 100644 --- a/users/models_test.py +++ b/users/models_test.py @@ -1,12 +1,11 @@ """Tests for user models""" -# pylint: disable=too-many-arguments, redefined-outer-name import factory +import pytest from django.core.exceptions import ValidationError from django.db import transaction -import pytest from affiliate.factories import AffiliateFactory -from courseware.factories import OpenEdxApiAuthFactory, CoursewareUserFactory +from courseware.factories import CoursewareUserFactory, OpenEdxApiAuthFactory from users.factories import UserFactory from users.models import LegalAddress, User @@ -14,16 +13,14 @@ @pytest.mark.parametrize( - "create_func,exp_staff,exp_superuser,exp_is_active", + "create_func,exp_staff,exp_superuser,exp_is_active", # noqa: PT006 [ - [User.objects.create_user, False, False, False], - [User.objects.create_superuser, True, True, True], + [User.objects.create_user, False, False, False], # noqa: PT007 + [User.objects.create_superuser, True, True, True], # noqa: PT007 ], ) @pytest.mark.parametrize("password", [None, "pass"]) -def test_create_user( - create_func, exp_staff, exp_superuser, exp_is_active, password -): # pylint: disable=too-many-arguments +def test_create_user(create_func, exp_staff, exp_superuser, exp_is_active, password): """Test creating a user""" username = "user1" email = "uSer@EXAMPLE.com" @@ -52,7 +49,7 @@ def test_create_user_affiliate(): "username1", email="a@b.com", name="Jane Doe", - password="asdfghjkl1", + password="asdfghjkl1", # noqa: S106 affiliate_id=affiliate.id, ) affiliate_referral_action = user.affiliate_user_actions.first() @@ -71,24 +68,24 @@ def test_create_user_affiliate(): ) def test_create_superuser_error(kwargs): """Test creating a user""" - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 User.objects.create_superuser( username=None, email="uSer@EXAMPLE.com", name="Jane Doe", - password="abc", + password="abc", # noqa: S106 **kwargs, ) @pytest.mark.parametrize( - "field, value, is_valid", + "field, value, is_valid", # noqa: PT006 [ - ["country", "US", True], - ["country", "United States", False], - ["state_or_territory", "US-MA", True], - ["state_or_territory", "MA", False], - ["state_or_territory", "Massachusets", False], + ["country", "US", True], # noqa: PT007 + ["country", "United States", False], # noqa: PT007 + ["state_or_territory", "US-MA", True], # noqa: PT007 + ["state_or_territory", "MA", False], # noqa: PT007 + ["state_or_territory", "Massachusets", False], # noqa: PT007 ], ) def test_legal_address_validation(field, value, is_valid): @@ -115,10 +112,12 @@ def test_faulty_user_qset(): good_users = users[0:2] expected_faulty_users = users[2:] OpenEdxApiAuthFactory.create_batch( - 3, user=factory.Iterator(good_users + [users[3]]) + 3, + user=factory.Iterator(good_users + [users[3]]), # noqa: RUF005 ) CoursewareUserFactory.create_batch( - 3, user=factory.Iterator(good_users + [users[4]]) + 3, + user=factory.Iterator(good_users + [users[4]]), # noqa: RUF005 ) assert set(User.faulty_courseware_users.values_list("id", flat=True)) == { diff --git a/users/serializers.py b/users/serializers.py index 3a8dff2cd..c5de4888e 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,19 +1,19 @@ """User serializers""" -from collections import defaultdict import logging import re +from collections import defaultdict -from django.db import transaction import pycountry +from django.db import transaction from rest_framework import serializers from social_django.models import UserSocialAuth from courseware.tasks import change_edx_user_email_async from ecommerce.api import fetch_and_serialize_unused_coupons +from hubspot_xpro.task_helpers import sync_hubspot_user from mail import verification_api from mitxpro.serializers import WriteableSerializerMethodField -from users.models import LegalAddress, User, Profile, ChangeEmailRequest -from hubspot_xpro.task_helpers import sync_hubspot_user +from users.models import ChangeEmailRequest, LegalAddress, Profile, User log = logging.getLogger() @@ -48,30 +48,30 @@ class LegalAddressSerializer(serializers.ModelSerializer): postal_code = serializers.CharField(max_length=10, allow_blank=True) def validate_first_name(self, value): - """Validates the first name of the user""" + """Validate the first name of the user""" if value and not USER_NAME_RE.match(value): - raise serializers.ValidationError("First name is not valid") + raise serializers.ValidationError("First name is not valid") # noqa: EM101 return value def validate_last_name(self, value): - """Validates the last name of the user""" + """Validate the last name of the user""" if value and not USER_NAME_RE.match(value): - raise serializers.ValidationError("Last name is not valid") + raise serializers.ValidationError("Last name is not valid") # noqa: EM101 return value def validate_street_address(self, value): - """Validates an incoming street address list""" + """Validate an incoming street address list""" if not value or not isinstance(value, list): raise serializers.ValidationError( - "street_address must be a list of street lines" + "street_address must be a list of street lines" # noqa: EM101 ) - if len(value) > 5: + if len(value) > 5: # noqa: PLR2004 raise serializers.ValidationError( - "street_address list must be 5 items or less" + "street_address list must be 5 items or less" # noqa: EM101 ) - if any(len(line) > 60 for line in value): + if any(len(line) > 60 for line in value): # noqa: PLR2004 raise serializers.ValidationError( - "street_address lines must be 60 characters or less" + "street_address lines must be 60 characters or less" # noqa: EM101 ) return {f"street_address_{idx+1}": line for idx, line in enumerate(value)} @@ -117,15 +117,14 @@ def validate(self, attrs): errors["postal_code"].append( f"Postal Code is required for {country.name}" ) - else: - if country.alpha_2 == "US" and not US_POSTAL_RE.match(postal_code): - errors["postal_code"].append( - "Postal Code must be in the format 'NNNNN' or 'NNNNN-NNNNN'" - ) - elif country.alpha_2 == "CA" and not CA_POSTAL_RE.match(postal_code): - errors["postal_code"].append( - "Postal Code must be in the format 'ANA NAN'" - ) + elif country.alpha_2 == "US" and not US_POSTAL_RE.match(postal_code): + errors["postal_code"].append( + "Postal Code must be in the format 'NNNNN' or 'NNNNN-NNNNN'" + ) + elif country.alpha_2 == "CA" and not CA_POSTAL_RE.match(postal_code): + errors["postal_code"].append( + "Postal Code must be in the format 'ANA NAN'" + ) if errors: raise serializers.ValidationError(errors) @@ -174,7 +173,7 @@ def get_company(self, instance): class Meta: model = LegalAddress - fields = LegalAddressSerializer.Meta.fields + ("email", "company") + fields = LegalAddressSerializer.Meta.fields + ("email", "company") # noqa: RUF005 extra_kwargs = LegalAddressSerializer.Meta.extra_kwargs @@ -254,15 +253,15 @@ def validate_username(self, value): return {"username": value} def get_email(self, instance): - """Returns the email or None in the case of AnonymousUser""" + """Return the email or None in the case of AnonymousUser""" return getattr(instance, "email", None) def get_username(self, instance): - """Returns the username or None in the case of AnonymousUser""" + """Return the username or None in the case of AnonymousUser""" return getattr(instance, "username", None) def get_unused_coupons(self, instance): - """Returns a list of unused coupons""" + """Return a list of unused coupons""" if not instance.is_anonymous: return fetch_and_serialize_unused_coupons(instance) return [] @@ -387,7 +386,7 @@ def validate(self, attrs): # verify the password verifies for the current user if not user.check_password(password): - raise serializers.ValidationError("Invalid Password") + raise serializers.ValidationError("Invalid Password") # noqa: EM101 return attrs @@ -414,12 +413,12 @@ class ChangeEmailRequestUpdateSerializer(serializers.ModelSerializer): @transaction.atomic def update(self, instance, validated_data): - """Updates an email change request""" + """Update an email change request""" if User.objects.filter(email=instance.new_email).exists(): log.debug( "User %s tried to change email address to one already in use", instance ) - raise serializers.ValidationError("Unable to change email") + raise serializers.ValidationError("Unable to change email") # noqa: EM101 result = super().update(instance, validated_data) @@ -474,7 +473,7 @@ def get_states(self, instance): if instance.alpha_2 in ("US", "CA"): return StateProvinceSerializer( instance=sorted( - list(pycountry.subdivisions.get(country_code=instance.alpha_2)), + pycountry.subdivisions.get(country_code=instance.alpha_2), key=lambda state: state.name, ), many=True, diff --git a/users/serializers_test.py b/users/serializers_test.py index 3f0a091bb..285a3853d 100644 --- a/users/serializers_test.py +++ b/users/serializers_test.py @@ -1,25 +1,24 @@ """Tests for users.serializers""" import pytest - from rest_framework.exceptions import ValidationError from affiliate.factories import AffiliateFactory from users.factories import UserFactory from users.models import ChangeEmailRequest -from users.serializers import ChangeEmailRequestUpdateSerializer -from users.serializers import LegalAddressSerializer, UserSerializer - - -# pylint:disable=redefined-outer-name +from users.serializers import ( + ChangeEmailRequestUpdateSerializer, + LegalAddressSerializer, + UserSerializer, +) -@pytest.fixture() +@pytest.fixture def mock_user_sync(mocker): """Yield a mock hubspot_xpro update task for contacts""" - yield mocker.patch("hubspot_xpro.tasks.sync_contact_with_hubspot.delay") + return mocker.patch("hubspot_xpro.tasks.sync_contact_with_hubspot.delay") -@pytest.fixture() +@pytest.fixture def sample_address(): """Return a legal address""" return { @@ -41,39 +40,39 @@ def test_validate_legal_address(sample_address): @pytest.mark.parametrize( - "field,value,error", + "field,value,error", # noqa: PT006 [ - ["first_name", "", "This field may not be blank."], - ["last_name", "", "This field may not be blank."], - ["street_address", [], "street_address must be a list of street lines"], - [ + ["first_name", "", "This field may not be blank."], # noqa: PT007 + ["last_name", "", "This field may not be blank."], # noqa: PT007 + ["street_address", [], "street_address must be a list of street lines"], # noqa: PT007 + [ # noqa: PT007 "street_address", ["a", "b", "c", "d", "e", "f"], "street_address list must be 5 items or less", ], - [ + [ # noqa: PT007 "street_address", ["x" * 61], "street_address lines must be 60 characters or less", ], - ["country", "", "This field may not be blank."], - ["country", None, "This field may not be null."], - ["state_or_territory", "", "State/territory is required for United States"], - [ + ["country", "", "This field may not be blank."], # noqa: PT007 + ["country", None, "This field may not be null."], # noqa: PT007 + ["state_or_territory", "", "State/territory is required for United States"], # noqa: PT007 + [ # noqa: PT007 "state_or_territory", "CA-QC", "Quebec is not a valid state or territory of United States", ], - ["city", "", "This field may not be blank."], - ["postal_code", "", "Postal Code is required for United States"], - [ + ["city", "", "This field may not be blank."], # noqa: PT007 + ["postal_code", "", "Postal Code is required for United States"], # noqa: PT007 + [ # noqa: PT007 "postal_code", "3082", "Postal Code must be in the format 'NNNNN' or 'NNNNN-NNNNN'", ], ], ) -def test_validate_required_fields_US_CA(sample_address, field, value, error): +def test_validate_required_fields_US_CA(sample_address, field, value, error): # noqa: N802 """Test that missing required fields causes a validation error""" sample_address[field] = value serializer = LegalAddressSerializer(data=sample_address) @@ -82,13 +81,13 @@ def test_validate_required_fields_US_CA(sample_address, field, value, error): @pytest.mark.parametrize( - "data,error", + "data,error", # noqa: PT006 [ - [ + [ # noqa: PT007 {"country": "US", "state_or_territory": "US-MA", "postal_code": "2183"}, "Postal Code must be in the format 'NNNNN' or 'NNNNN-NNNNN'", ], - [ + [ # noqa: PT007 {"country": "CA", "state_or_territory": "CA-BC", "postal_code": "AFA D"}, "Postal Code must be in the format 'ANA NAN'", ], @@ -210,7 +209,7 @@ def test_update_email_change_request_existing_email(user): ) serializer = ChangeEmailRequestUpdateSerializer(change_request, {"confirmed": True}) - with pytest.raises(ValidationError): + with pytest.raises(ValidationError): # noqa: PT012 serializer.is_valid() serializer.save() @@ -220,15 +219,13 @@ def test_create_email_change_request_same_email(user): change_request = ChangeEmailRequest.objects.create(user=user, new_email=user.email) serializer = ChangeEmailRequestUpdateSerializer(change_request, {"confirmed": True}) - with pytest.raises(ValidationError): + with pytest.raises(ValidationError): # noqa: PT012 serializer.is_valid() serializer.save() @pytest.mark.parametrize("raises_error", [False, True]) -def test_update_user_email( - mocker, user, raises_error -): # pylint: disable=too-many-arguments +def test_update_user_email(mocker, user, raises_error): """Test that update edx user email takes the correct action""" mock_update_edx_user_email = mocker.patch( @@ -277,8 +274,8 @@ def test_legal_address_serializer_invalid_name(sample_address): # Case 2: Make sure that name doesn't start with valid special character(s) # These characters are valid for a name but they shouldn't be at the start for valid_character in '^/$#*=[]`%_;<>{}"|': - sample_address["first_name"] = "{}First".format(valid_character) - sample_address["last_name"] = "{}Last".format(valid_character) + sample_address["first_name"] = f"{valid_character}First" + sample_address["last_name"] = f"{valid_character}Last" serializer = LegalAddressSerializer(data=sample_address) with pytest.raises(ValidationError): serializer.is_valid(raise_exception=True) diff --git a/users/urls.py b/users/urls.py index 7aa87c38a..456af9bce 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,13 +1,12 @@ """User url routes""" -from django.urls import include -from django.urls import path +from django.urls import include, path from rest_framework import routers from users.views import ( - UserRetrieveViewSet, - CurrentUserRetrieveUpdateViewSet, - CountriesStatesViewSet, ChangeEmailRequestViewSet, + CountriesStatesViewSet, + CurrentUserRetrieveUpdateViewSet, + UserRetrieveViewSet, ) router = routers.DefaultRouter() diff --git a/users/utils.py b/users/utils.py index 510cae2b0..de3010c12 100644 --- a/users/utils.py +++ b/users/utils.py @@ -9,7 +9,6 @@ from mitxpro.utils import get_error_response_summary from users.constants import USERNAME_MAX_LEN - User = get_user_model() log = logging.getLogger(__name__) @@ -53,7 +52,7 @@ def _reformat_for_username(string): def usernameify(full_name, email=""): """ - Generates a username based on a full name, or an email address as a fallback. + Generate a username based on a full name, or an email address as a fallback. Args: full_name (str): A full name (i.e.: User.name) @@ -76,7 +75,7 @@ def usernameify(full_name, email=""): username = _reformat_for_username(email.split("@")[0]) if not username: raise ValueError( - "Username could not be generated (full_name: '{}', email: '{}')".format( + "Username could not be generated (full_name: '{}', email: '{}')".format( # noqa: EM103, UP032 full_name, email ) ) @@ -85,7 +84,7 @@ def usernameify(full_name, email=""): def is_duplicate_username_error(exc): """ - Returns True if the given exception indicates that there was an attempt to save a User record with an + Return True if the given exception indicates that there was an attempt to save a User record with an already-existing username. Args: @@ -122,13 +121,13 @@ def ensure_active_user(user): if created_auth_token: log.info("Created edX auth token for %s", user.email) except HTTPError as exc: - log.error( + log.error( # noqa: TRY400 "%s (%s): Failed to repair (%s)", user.username, user.email, get_error_response_summary(exc.response), ) - except Exception: # pylint: disable=broad-except + except Exception: log.exception("%s (%s): Failed to repair", user.username, user.email) diff --git a/users/utils_test.py b/users/utils_test.py index 3c749e79e..2dfe0bfaa 100644 --- a/users/utils_test.py +++ b/users/utils_test.py @@ -14,23 +14,23 @@ @pytest.mark.parametrize( - "full_name,email,expected_username", + "full_name,email,expected_username", # noqa: PT006 [ - [" John Doe ", None, "john-doe"], - ["Tabby Tabberson", None, "tabby-tabberson"], - ["Àccèntèd Ñame, Ësq.", None, "àccèntèd-ñame-ësq"], - ["-Dashy_St._Underscores-", None, "dashy-st-underscores"], - ["Repeated-----Chars___Jr.", None, "repeated-chars-jr"], - ["Numbers123 !$!@ McStrange!!##^", None, "numbers-mcstrange"], - ["Кирил Френков", None, "кирил-френков"], - ["年號", None, "年號"], - ["abcdefghijklmnopqrstuvwxyz", None, "abcdefghijklmnopqrst"], - ["ai bi cı dI eİ fI", None, "ai-bi-ci-di-ei-fi"], - ["", "some.email@example.co.uk", "someemail"], + [" John Doe ", None, "john-doe"], # noqa: PT007 + ["Tabby Tabberson", None, "tabby-tabberson"], # noqa: PT007 + ["Àccèntèd Ñame, Ësq.", None, "àccèntèd-ñame-ësq"], # noqa: PT007 + ["-Dashy_St._Underscores-", None, "dashy-st-underscores"], # noqa: PT007 + ["Repeated-----Chars___Jr.", None, "repeated-chars-jr"], # noqa: PT007 + ["Numbers123 !$!@ McStrange!!##^", None, "numbers-mcstrange"], # noqa: PT007 + ["Кирил Френков", None, "кирил-френков"], # noqa: PT007 + ["年號", None, "年號"], # noqa: PT007 + ["abcdefghijklmnopqrstuvwxyz", None, "abcdefghijklmnopqrst"], # noqa: PT007 + ["ai bi cı dI eİ fI", None, "ai-bi-ci-di-ei-fi"], # noqa: PT007, RUF001 + ["", "some.email@example.co.uk", "someemail"], # noqa: PT007 ], ) def test_usernameify(mocker, full_name, email, expected_username): - """usernameify should turn a user's name into a username, or use the email if necessary""" + """Usernameify should turn a user's name into a username, or use the email if necessary""" # Change the username max length to 20 for test data simplicity's sake temp_username_max_len = 20 mocker.patch("users.utils.USERNAME_MAX_LEN", temp_username_max_len) @@ -41,16 +41,16 @@ def test_usernameify(mocker, full_name, email, expected_username): def test_usernameify_fail(): - """usernameify should raise an exception if the full name and email both fail to produce a username""" - with pytest.raises(ValueError): + """Usernameify should raise an exception if the full name and email both fail to produce a username""" + with pytest.raises(ValueError): # noqa: PT011 assert usernameify("!!!", email="???@example.com") @pytest.mark.parametrize( - "exception_text,expected_value", + "exception_text,expected_value", # noqa: PT006 [ - ["DETAILS: (username)=(ABCDEFG) already exists", True], - ["DETAILS: (email)=(ABCDEFG) already exists", False], + ["DETAILS: (username)=(ABCDEFG) already exists", True], # noqa: PT007 + ["DETAILS: (email)=(ABCDEFG) already exists", False], # noqa: PT007 ], ) def test_is_duplicate_username_error(exception_text, expected_value): @@ -76,11 +76,11 @@ def test_ensure_active_user(mock_repair_faulty_edx_user, user): @pytest.mark.parametrize( - "name, email", + "name, email", # noqa: PT006 [ - ["Mrs. Tammy Smith DDS", "HeSNMtNMfVdo@example.com"], - ["John Doe", "jd_123@example.com"], - ["Doe, Jane", "jd_456@example.com"], + ["Mrs. Tammy Smith DDS", "HeSNMtNMfVdo@example.com"], # noqa: PT007 + ["John Doe", "jd_123@example.com"], # noqa: PT007 + ["Doe, Jane", "jd_456@example.com"], # noqa: PT007 ], ) def test_format_recipient(name, email): diff --git a/users/views.py b/users/views.py index 8c727d395..20a8a7357 100644 --- a/users/views.py +++ b/users/views.py @@ -9,13 +9,13 @@ from courseware import tasks from mitxpro.permissions import UserIsOwnerPermission from mitxpro.utils import now_in_utc -from users.models import User, ChangeEmailRequest +from users.models import ChangeEmailRequest, User from users.serializers import ( - PublicUserSerializer, - UserSerializer, - CountrySerializer, ChangeEmailRequestCreateSerializer, ChangeEmailRequestUpdateSerializer, + CountrySerializer, + PublicUserSerializer, + UserSerializer, ) @@ -38,11 +38,11 @@ class CurrentUserRetrieveUpdateViewSet( permission_classes = [] def get_object(self): - """Returns the current request user""" + """Return the current request user""" # NOTE: this may be a logged in or anonymous user return self.request.user - def update(self, request, *args, **kwargs): + def update(self, request, *args, **kwargs): # noqa: D102 with transaction.atomic(): user_name = request.user.name update_result = super().update(request, *args, **kwargs) @@ -58,7 +58,7 @@ class ChangeEmailRequestViewSet( lookup_field = "code" - def get_permissions(self): + def get_permissions(self): # noqa: D102 permission_classes = [] if self.action == "create": permission_classes = [IsAuthenticated] @@ -71,8 +71,8 @@ def get_queryset(self): expires_on__gt=now_in_utc(), confirmed=False ) - def get_serializer_class(self): - if self.action == "create": + def get_serializer_class(self): # noqa: D102 + if self.action == "create": # noqa: RET503 return ChangeEmailRequestCreateSerializer elif self.action == "partial_update": return ChangeEmailRequestUpdateSerializer @@ -83,8 +83,8 @@ class CountriesStatesViewSet(viewsets.ViewSet): permission_classes = [] - def list(self, request): # pylint:disable=unused-argument + def list(self, request): # noqa: ARG002 """Get generator for countries/states list""" - queryset = sorted(list(pycountry.countries), key=lambda country: country.name) + queryset = sorted(pycountry.countries, key=lambda country: country.name) serializer = CountrySerializer(queryset, many=True) return Response(serializer.data) diff --git a/users/views_test.py b/users/views_test.py index 9034d3a11..52ea6fd6b 100644 --- a/users/views_test.py +++ b/users/views_test.py @@ -1,7 +1,7 @@ """Test for user views""" from datetime import timedelta -import pytest +import pytest from django.urls import reverse from factory import fuzzy from rest_framework import status @@ -185,7 +185,7 @@ def test_create_email_change_request_valid_email(user_drf_client, user, mocker): old_email = user.email resp = user_drf_client.patch( - "/api/change-emails/{}/".format(code), data={"confirmed": True} + f"/api/change-emails/{code}/", data={"confirmed": True} ) assert not UserSocialAuth.objects.filter(uid=old_email, user=user).exists() assert resp.status_code == status.HTTP_200_OK @@ -202,7 +202,7 @@ def test_create_email_change_request_expired_code(user_drf_client, user): ) resp = user_drf_client.patch( - "/api/change-emails/{}/".format(change_request.code), data={"confirmed": True} + f"/api/change-emails/{change_request.code}/", data={"confirmed": True} ) assert resp.status_code == status.HTTP_404_NOT_FOUND diff --git a/voucher/conftest.py b/voucher/conftest.py index edd63adfe..3115818c8 100644 --- a/voucher/conftest.py +++ b/voucher/conftest.py @@ -1,30 +1,28 @@ -# pylint: disable=redefined-outer-name """ Fixtures for voucher tests """ from datetime import datetime, timezone from types import SimpleNamespace -import pytest import factory +import pytest from django.http import HttpRequest - from faker import Faker + from courses.factories import CourseRunFactory from ecommerce.factories import ( + CompanyFactory, CouponEligibilityFactory, - ProductFactory, CouponFactory, + CouponPaymentVersionFactory, CouponRedemptionFactory, CouponVersionFactory, - CouponPaymentVersionFactory, - CompanyFactory, + ProductFactory, ) from voucher.factories import VoucherFactory from voucher.forms import VOUCHER_PARSE_ERROR from voucher.views import UploadVoucherFormView - fake = Faker() @@ -67,7 +65,7 @@ def upload_voucher_form_with_parse_error(): @pytest.fixture def upload_voucher_form_view(user): """ - Returns a mock instance of an UploadVoucherFormView with an attached User + Return a mock instance of an UploadVoucherFormView with an attached User """ request = HttpRequest() request.user = user @@ -77,7 +75,7 @@ def upload_voucher_form_view(user): @pytest.fixture def voucher_and_user(user): """ - Returns a voucher and matching user object + Return a voucher and matching user object """ voucher = VoucherFactory(user=user) return SimpleNamespace(voucher=voucher, user=user) @@ -86,7 +84,7 @@ def voucher_and_user(user): @pytest.fixture def authenticated_client(client, user): """ - Returns an authenticated client + Return an authenticated client """ client.force_login(user) return client @@ -95,7 +93,7 @@ def authenticated_client(client, user): @pytest.fixture def voucher_and_user_client(voucher_and_user, client): """ - Returns a voucher, user, and authenticated client + Return a voucher, user, and authenticated client """ user = voucher_and_user.user client.force_login(user) @@ -105,7 +103,7 @@ def voucher_and_user_client(voucher_and_user, client): @pytest.fixture def redeemed_voucher_and_user_client(voucher_and_user, client): """ - Returns a voucher, user, and authenticated client + Return a voucher, user, and authenticated client """ user = voucher_and_user.user voucher = voucher_and_user.voucher @@ -119,7 +117,7 @@ def redeemed_voucher_and_user_client(voucher_and_user, client): @pytest.fixture def voucher_and_partial_matches(voucher_and_user_client): """ - Returns a voucher with partial matching CourseRuns + Return a voucher with partial matching CourseRuns """ voucher = voucher_and_user_client.voucher company = CompanyFactory() @@ -155,7 +153,7 @@ def voucher_and_partial_matches(voucher_and_user_client): @pytest.fixture def voucher_and_exact_match(voucher_and_user_client): """ - Returns a voucher with and an exact matching and partial matching CourseRuns + Return a voucher with and an exact matching and partial matching CourseRuns """ voucher = voucher_and_user_client.voucher exact_match = CourseRunFactory( @@ -176,7 +174,7 @@ def voucher_and_exact_match(voucher_and_user_client): @pytest.fixture def voucher_and_partial_matches_with_coupons(voucher_and_partial_matches): """ - Returns a voucher with partial matching CourseRuns and valid coupons + Return a voucher with partial matching CourseRuns and valid coupons """ context = voucher_and_partial_matches products = [ @@ -210,7 +208,7 @@ def voucher_and_partial_matches_with_coupons(voucher_and_partial_matches): @pytest.fixture def voucher_and_exact_match_with_coupon(voucher_and_exact_match): """ - Returns a voucher with exact matching and partial matching CourseRuns and valid coupons + Return a voucher with exact matching and partial matching CourseRuns and valid coupons """ context = voucher_and_exact_match company = context.company diff --git a/voucher/factories.py b/voucher/factories.py index 858bf3da6..77d22d85c 100644 --- a/voucher/factories.py +++ b/voucher/factories.py @@ -3,8 +3,8 @@ """ import factory from factory.django import DjangoModelFactory -from users.factories import UserFactory +from users.factories import UserFactory from voucher.models import Voucher diff --git a/voucher/forms.py b/voucher/forms.py index c5d692012..a69840632 100644 --- a/voucher/forms.py +++ b/voucher/forms.py @@ -6,7 +6,6 @@ from voucher.utils import read_pdf - VOUCHER_PARSE_ERROR = "Failed to parse PDF" diff --git a/voucher/migrations/0001_initial.py b/voucher/migrations/0001_initial.py index 3f920d3c5..ae9538301 100644 --- a/voucher/migrations/0001_initial.py +++ b/voucher/migrations/0001_initial.py @@ -1,12 +1,11 @@ # Generated by Django 2.1.7 on 2019-05-31 18:21 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/voucher/migrations/0002_product_and_enrollment_keys.py b/voucher/migrations/0002_product_and_enrollment_keys.py index 0f8ad4f47..876b8415f 100644 --- a/voucher/migrations/0002_product_and_enrollment_keys.py +++ b/voucher/migrations/0002_product_and_enrollment_keys.py @@ -1,11 +1,10 @@ # Generated by Django 2.1.7 on 2019-06-06 17:50 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("courses", "0009_enrollment_statuses"), ("ecommerce", "0012_coupon_assignment_redeem_flag"), diff --git a/voucher/migrations/0003_pdf_filename.py b/voucher/migrations/0003_pdf_filename.py index 9b4bbb10b..d652a1873 100644 --- a/voucher/migrations/0003_pdf_filename.py +++ b/voucher/migrations/0003_pdf_filename.py @@ -1,11 +1,11 @@ # Generated by Django 2.1.9 on 2019-07-11 13:42 from django.db import migrations, models + import voucher.utils class Migration(migrations.Migration): - dependencies = [("voucher", "0002_product_and_enrollment_keys")] operations = [ diff --git a/voucher/models.py b/voucher/models.py index 8fb1d7e1e..359e4517c 100644 --- a/voucher/models.py +++ b/voucher/models.py @@ -15,7 +15,7 @@ class Voucher(TimestampedModel): an attached coupon and a selected product (course_run) """ - voucher_id = models.CharField(max_length=32, null=True, blank=True) + voucher_id = models.CharField(max_length=32, null=True, blank=True) # noqa: DJ001 employee_id = models.CharField(max_length=32) employee_name = models.CharField(max_length=128) course_start_date_input = models.DateField() diff --git a/voucher/urls.py b/voucher/urls.py index 4d4495fa5..435a0f6a3 100644 --- a/voucher/urls.py +++ b/voucher/urls.py @@ -2,6 +2,7 @@ Voucher URL patterns """ from django.urls import path + from voucher import views urlpatterns = [ diff --git a/voucher/utils.py b/voucher/utils.py index c34e76022..85e00d137 100644 --- a/voucher/utils.py +++ b/voucher/utils.py @@ -1,15 +1,14 @@ """PDF Parsing functions for Vouchers""" -# pylint: disable=R1702 +import difflib import json import logging +import re from datetime import datetime from uuid import uuid4 -import difflib -import re +import pdftotext from django.conf import settings from django.db.models import Q -import pdftotext from courses.models import CourseRun from ecommerce.api import get_valid_coupon_versions @@ -28,7 +27,7 @@ def remove_extra_spaces(text): Returns: str: The text with extra spaces removed """ - if text: + if text: # noqa: RET503 return re.sub(r"\s+", " ", text.strip()) @@ -129,7 +128,7 @@ def get_eligible_coupon_choices(voucher): ) sorted_eligible_choices = [] for match in close_matches: - sorted_eligible_choices.append( + sorted_eligible_choices.append( # noqa: PERF401 eligible_choices[eligible_choices_titles.index(match)] ) eligible_choices = sorted_eligible_choices @@ -160,7 +159,7 @@ def get_valid_voucher_coupons_version(voucher, product): ) -def read_pdf_domestic(pdf): +def read_pdf_domestic(pdf): # noqa: C901 """ Process domestic vouchers and return parsed values """ @@ -236,7 +235,7 @@ def update_column_values(column_values, elements): """ Update column values with the sliced elements """ - if len(elements) == 5: + if len(elements) == 5: # noqa: PLR2004 for column, value in zip(column_values, elements): if value: if column_values[column]: @@ -305,7 +304,7 @@ def read_pdf(pdf_file): for key in domestic_settings_keys + international_settings_keys: if not getattr(settings, key): log.warning("Required setting %s missing for read_pdf", key) - return + return # noqa: RET502 try: pdf = pdftotext.PDF(pdf_file) if any("Entity Name:" in page for page in pdf): @@ -322,12 +321,12 @@ def read_pdf(pdf_file): settings.VOUCHER_INTERNATIONAL_EMPLOYEE_ID_KEY ), "voucher_id": None, - "course_start_date_input": datetime.strptime( + "course_start_date_input": datetime.strptime( # noqa: DTZ007 values.get(settings.VOUCHER_INTERNATIONAL_DATES_KEY).split(" ")[0], "%d-%b-%Y", ).date(), "course_id_input": remove_extra_spaces(course_id_input) - if len(course_id_input) >= 3 + if len(course_id_input) >= 3 # noqa: PLR2004 else "", "course_title_input": remove_extra_spaces( values.get(settings.VOUCHER_INTERNATIONAL_COURSE_NAME_KEY) @@ -348,12 +347,12 @@ def read_pdf(pdf_file): "pdf": pdf_file, "employee_id": values.get(settings.VOUCHER_DOMESTIC_EMPLOYEE_ID_KEY), "voucher_id": values.get(settings.VOUCHER_DOMESTIC_KEY), - "course_start_date_input": datetime.strptime( + "course_start_date_input": datetime.strptime( # noqa: DTZ007 values.get(settings.VOUCHER_DOMESTIC_DATES_KEY).split(" ")[0], "%m/%d/%Y", ).date(), "course_id_input": remove_extra_spaces(course_id_input) - if len(course_id_input) >= 3 + if len(course_id_input) >= 3 # noqa: PLR2004 else "", "course_title_input": remove_extra_spaces( " ".join( @@ -362,12 +361,12 @@ def read_pdf(pdf_file): ), "employee_name": values.get(settings.VOUCHER_DOMESTIC_EMPLOYEE_KEY), } - except Exception: # pylint: disable=broad-except + except Exception: log.exception("Could not parse PDF") return None -def voucher_upload_path(instance, filename): # pylint: disable=unused-argument +def voucher_upload_path(instance, filename): # noqa: ARG001 """ Make a unique path/name for an uploaded voucher @@ -378,4 +377,4 @@ def voucher_upload_path(instance, filename): # pylint: disable=unused-argument Returns: str: The unique filepath for the voucher """ - return "vouchers/{}_{}".format(uuid4(), filename) + return f"vouchers/{uuid4()}_{filename}" diff --git a/voucher/utils_test.py b/voucher/utils_test.py index cbe7dc77a..f136fbb09 100644 --- a/voucher/utils_test.py +++ b/voucher/utils_test.py @@ -1,29 +1,27 @@ """Tests for utils.py""" +import difflib import json import re from datetime import datetime, timezone -import difflib import pytest from voucher.factories import VoucherFactory from voucher.utils import ( - read_pdf, get_current_voucher, get_eligible_coupon_choices, - voucher_upload_path, + read_pdf, remove_extra_spaces, + voucher_upload_path, ) -# pylint: disable=redefined-outer-name - pytestmark = [pytest.mark.django_db] @pytest.fixture def mock_logger(mocker): """Mock the log""" - yield mocker.patch("voucher.utils.log") + return mocker.patch("voucher.utils.log") def setup_pdf_parsing(settings): @@ -62,13 +60,13 @@ def test_remove_extra_spaces(): def test_pdf_parsing_domestic(settings): """Test that pdf parsing correctly parses domestic voucher pdfs""" setup_pdf_parsing(settings) - with open("voucher/.test/domestic_voucher.pdf", "rb") as pdf_file: + with open("voucher/.test/domestic_voucher.pdf", "rb") as pdf_file: # noqa: PTH123 values = read_pdf(pdf_file) expected_values = { "pdf": pdf_file, "employee_id": "1234567", "voucher_id": "299152-01", - "course_start_date_input": datetime.strptime( + "course_start_date_input": datetime.strptime( # noqa: DTZ007 "04/30/2018", "%m/%d/%Y" ).date(), "course_id_input": "AMxB", @@ -81,13 +79,13 @@ def test_pdf_parsing_domestic(settings): def test_pdf_parsing_domestic_offset_credits(settings): """Test that pdf parsing handles when the credits value is part of the course name column""" setup_pdf_parsing(settings) - with open("voucher/.test/domestic_voucher_test_credits.pdf", "rb") as pdf_file: + with open("voucher/.test/domestic_voucher_test_credits.pdf", "rb") as pdf_file: # noqa: PTH123 values = read_pdf(pdf_file) expected_values = { "pdf": pdf_file, "employee_id": "1234567", "voucher_id": "291510-03", - "course_start_date_input": datetime.strptime( + "course_start_date_input": datetime.strptime( # noqa: DTZ007 "04/09/2018", "%m/%d/%Y" ).date(), "course_id_input": "SysEngxB3", @@ -100,13 +98,13 @@ def test_pdf_parsing_domestic_offset_credits(settings): def test_pdf_parsing_international(settings): """Test that pdf parsing correctly parses international voucher pdfs""" setup_pdf_parsing(settings) - with open("voucher/.test/international_voucher.pdf", "rb") as pdf_file: + with open("voucher/.test/international_voucher.pdf", "rb") as pdf_file: # noqa: PTH123 values = read_pdf(pdf_file) expected_values = { "pdf": pdf_file, "employee_id": "7654321", "voucher_id": None, - "course_start_date_input": datetime.strptime( + "course_start_date_input": datetime.strptime( # noqa: DTZ007 "9-Apr-2018", "%d-%b-%Y" ).date(), "course_id_input": "SysEngBx3", diff --git a/voucher/views.py b/voucher/views.py index 2451a860c..e168ec281 100644 --- a/voucher/views.py +++ b/voucher/views.py @@ -2,27 +2,26 @@ Voucher views """ import json -from datetime import datetime, timezone import logging +from datetime import datetime, timezone +from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.urls import reverse from django.views.generic import FormView from django.views.generic.base import View -from django.contrib import messages - -from ecommerce.utils import make_checkout_url from ecommerce.models import Coupon, Product +from ecommerce.utils import make_checkout_url from mitxpro.views import get_base_context -from voucher.forms import UploadVoucherForm, VOUCHER_PARSE_ERROR +from voucher.forms import VOUCHER_PARSE_ERROR, UploadVoucherForm from voucher.models import Voucher from voucher.utils import ( get_current_voucher, - get_valid_voucher_coupons_version, get_eligible_coupon_choices, + get_valid_voucher_coupons_version, ) log = logging.getLogger() @@ -78,7 +77,7 @@ def form_invalid(self, form): return redirect(reverse("voucher:resubmit")) return super().form_invalid(form) - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs): # noqa: D102 return {**super().get_context_data(**kwargs), **get_base_context(self.request)} diff --git a/voucher/views_test.py b/voucher/views_test.py index 4dd5bfa60..aa2314b77 100644 --- a/voucher/views_test.py +++ b/voucher/views_test.py @@ -1,27 +1,24 @@ -# pylint: disable=unused-import """ Test voucher views.py """ import json from urllib.parse import urljoin -from django.urls import reverse import pytest +from django.urls import reverse -from ecommerce.factories import CouponVersionFactory, CouponEligibilityFactory +from ecommerce.factories import CouponEligibilityFactory, CouponVersionFactory from users.factories import UserFactory from voucher.factories import VoucherFactory from voucher.models import Voucher -# pylint: disable=redefined-outer-name - pytestmark = [pytest.mark.django_db] @pytest.fixture def mock_logger(mocker): """Mock the log""" - yield mocker.patch("voucher.views.log") + return mocker.patch("voucher.views.log") def test_anonymous_user_permissions(client):