From d9808ca305b0833ae1ce1d7ee78c273c902370f2 Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Mon, 1 Jul 2024 17:15:25 -0400 Subject: [PATCH] feat: removing visible_date attribute (#2511) removing the reliance on the visible_date attribute * re-factoring some tests, not just to make them work with the new code, but also because some of these tests were never ideal. They weren't all testing the right things, some of them never tested the code path that used certificate available date, and definitely a bunch have non-useful names and no comments. Several were testing impossible (or, definitionally broken) situations like program certificates with no corresponding course certificates. * added django stubs for type checking * added some type checking * modified a test not only to work with the new code but also to do a better job of validating what was looking for * added mypy * told coverage not to look for type checking conditionals * all tests now use certificate available date (which has another pending ticket to be removed as a toggle) * upgraded the Python version used to build the documentation because accessible-pygments requires a version greater than 3.8. * updating one more github CI python version --- .coveragerc | 4 + .github/workflows/ci.yml | 6 +- credentials/apps/api/v2/tests/test_views.py | 39 +---- credentials/apps/credentials/api.py | 5 +- .../apps/credentials/tests/test_api.py | 32 ++-- .../apps/credentials/tests/test_utils.py | 17 +- .../apps/credentials/tests/test_views.py | 115 +++++-------- credentials/apps/credentials/utils.py | 151 +++++++----------- .../rest_api/v1/tests/test_serializers.py | 6 +- credentials/apps/records/tests/test_api.py | 58 +------ credentials/apps/records/tests/test_utils.py | 57 +++++-- mypy.ini | 28 ++++ requirements/all.txt | 65 +++----- requirements/base.txt | 16 +- requirements/dev.in | 3 + requirements/dev.txt | 54 +++---- requirements/docs.txt | 32 ++-- requirements/pip.txt | 4 +- requirements/pip_tools.txt | 12 +- requirements/production.txt | 24 +-- requirements/test.txt | 36 +---- requirements/translations.txt | 8 +- 22 files changed, 273 insertions(+), 499 deletions(-) create mode 100644 mypy.ini diff --git a/.coveragerc b/.coveragerc index 4c99a7d20..6e64df923 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,3 +10,7 @@ omit = *admin.py *static* *templates* +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 390f99f4e..3b864f2f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,13 +67,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["py38"] + python-version: ["py311"] django-version: ["django42"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.11" architecture: x64 - name: Setup Nodejs Env run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV @@ -106,7 +106,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.11" architecture: x64 - name: Install Dependencies run: make requirements diff --git a/credentials/apps/api/v2/tests/test_views.py b/credentials/apps/api/v2/tests/test_views.py index 13676d59a..d39edef38 100644 --- a/credentials/apps/api/v2/tests/test_views.py +++ b/credentials/apps/api/v2/tests/test_views.py @@ -83,7 +83,7 @@ def test_authentication(self): def test_create(self): program = ProgramFactory(site=self.site) - program_certificate = ProgramCertificateFactory(site=self.site, program_uuid=program.uuid) + program_certificate = ProgramCertificateFactory(site=self.site, program_uuid=program.uuid, program=program) expected_username = self.user.username expected_attribute_name = "fake-name" expected_attribute_value = "fake-value" @@ -356,43 +356,6 @@ def test_list_type_filtering(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data["results"], self.serialize_user_credential([program_cred], many=True)) - # This test should be removed in MICROBA-1198 in favor of the next test. - def test_list_visible_filtering(self): - """Verify the endpoint can filter by visible date.""" - program_certificate = ProgramCertificateFactory(site=self.site) - course_run = CourseRunFactory() - course_certificate = CourseCertificateFactory(course_id=course_run.key, course_run=course_run, site=self.site) - - course_cred = UserCredentialFactory(credential=course_certificate) - program_cred = UserCredentialFactory(credential=program_certificate) - - UserCredentialAttributeFactory( - user_credential=program_cred, - name="visible_date", - value="9999-01-01T01:01:01Z", - ) - - self.authenticate_user(self.user) - self.add_user_permission(self.user, "view_usercredential") - - both = [course_cred, program_cred] - - response = self.client.get(self.list_path) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data["results"], self.serialize_user_credential(both, many=True)) - - response = self.client.get(self.list_path + "?only_visible=True") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data["results"], self.serialize_user_credential([course_cred], many=True)) - - response = self.client.get(self.list_path + "?only_visible=False") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data["results"], self.serialize_user_credential(both, many=True)) - - # The following test is the same as the previous test, but with the - # USE_CERTIFICATE_AVAILABLE_DATE waffle switch enabled. Clean up previous - # tests in MICROBA-1198. - @override_switch("credentials.use_certificate_available_date", active=True) def test_list_visible_filtering_with_certificate_available_date(self): """Verify the endpoint can filter by visible date.""" course = CourseFactory.create(site=self.site) diff --git a/credentials/apps/credentials/api.py b/credentials/apps/credentials/api.py index 0ff91a10e..55d2b270c 100644 --- a/credentials/apps/credentials/api.py +++ b/credentials/apps/credentials/api.py @@ -17,13 +17,10 @@ if TYPE_CHECKING: - from django.contrib.auth import get_user_model from django.contrib.sites.models import Site from credentials.apps.catalog.models import CourseRun - User = get_user_model() - logger = logging.getLogger(__name__) @@ -235,7 +232,7 @@ def get_credential_dates(user_credentials, many): return get_credential_visible_date(user_credentials, use_date_override=True) -def process_course_credential_update(user: "User", course_run_key: str, mode: str, credential_status: str) -> None: +def process_course_credential_update(user, course_run_key: str, mode: str, credential_status: str) -> None: """ A utility function responsible for creating or updating a course credential associated with a learner. Primarily used when consuming events from the Event Bus. diff --git a/credentials/apps/credentials/tests/test_api.py b/credentials/apps/credentials/tests/test_api.py index f49e6aa7f..a54bb3abc 100644 --- a/credentials/apps/credentials/tests/test_api.py +++ b/credentials/apps/credentials/tests/test_api.py @@ -1,7 +1,6 @@ from unittest.mock import patch import ddt -from ddt import unpack from django.contrib.contenttypes.models import ContentType from django.test import TestCase from testfixtures import LogCapture @@ -156,7 +155,9 @@ def setUp(self): ) for course_run in self.course_runs ] - self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) + self.program_cert = ProgramCertificateFactory.create( + program_uuid=self.program.uuid, site=self.site, program=self.program + ) self.course_credential_content_type = ContentType.objects.get( app_label="credentials", model="coursecertificate" ) @@ -177,7 +178,18 @@ def setUp(self): credential=self.program_cert, ) - def test_get_user_credentials_by_content_type_zero(self): + def test_get_user_credentials_by_content_type_when_no_valid_types(self): + """get_user_credentials_by_content_type returns empty when there's no creds of the type""" + course_cert_content_types = ContentType.objects.filter(app_label="credentials", model__in=["goldstar"]) + for course_user_credential in self.course_user_credentials: + course_user_credential.delete() + result = get_user_credentials_by_content_type( + self.user.username, course_cert_content_types, UserCredentialStatus.AWARDED.value + ) + assert len(result) == 0 + + def test_get_user_credentials_by_content_type_when_no_creds(self): + """get_user_credentials_by_content_type returns empty when there's no applicable creds""" course_cert_content_types = ContentType.objects.filter( app_label="credentials", model__in=["coursecertificate", "programcertificate"] ) @@ -190,10 +202,8 @@ def test_get_user_credentials_by_content_type_zero(self): assert len(result) == 0 def test_get_user_credentials_by_content_type_course_only(self): - course_cert_content_types = ContentType.objects.filter( - app_label="credentials", model__in=["coursecertificate", "programcertificate"] - ) - self.program_user_credential.delete() + """get_user_credentials_by_content_type returns course certificates when asked""" + course_cert_content_types = ContentType.objects.filter(app_label="credentials", model__in=["coursecertificate"]) result = get_user_credentials_by_content_type( self.user.username, course_cert_content_types, UserCredentialStatus.AWARDED.value ) @@ -202,11 +212,10 @@ def test_get_user_credentials_by_content_type_course_only(self): assert result[1] == self.course_user_credentials[1] def test_get_user_credentials_by_content_type_program_only(self): + """get_user_credentials_by_content_type returns program certificates when asked""" course_cert_content_types = ContentType.objects.filter( - app_label="credentials", model__in=["coursecertificate", "programcertificate"] + app_label="credentials", model__in=["programcertificate"] ) - for course_user_credential in self.course_user_credentials: - course_user_credential.delete() result = get_user_credentials_by_content_type( self.user.username, course_cert_content_types, UserCredentialStatus.AWARDED.value ) @@ -214,6 +223,7 @@ def test_get_user_credentials_by_content_type_program_only(self): assert result[0] == self.program_user_credential def test_get_user_credentials_by_content_type_course_and_program(self): + """get_user_credentials_by_content_type returns courses and programs when asked""" course_cert_content_types = ContentType.objects.filter( app_label="credentials", model__in=["coursecertificate", "programcertificate"] ) @@ -292,7 +302,7 @@ def setUp(self): [ProgramCertificate, UserCredentialStatus.AWARDED], [ProgramCertificate, UserCredentialStatus.REVOKED], ) - @unpack + @ddt.unpack def test_create_credential(self, credential_type, cert_status): """ Happy path. This test verifies the functionality of the `_update_or_create_credentials` utility function. diff --git a/credentials/apps/credentials/tests/test_utils.py b/credentials/apps/credentials/tests/test_utils.py index 0c71a40f3..c62699680 100644 --- a/credentials/apps/credentials/tests/test_utils.py +++ b/credentials/apps/credentials/tests/test_utils.py @@ -1,4 +1,3 @@ -import datetime import textwrap from unittest import mock @@ -15,11 +14,7 @@ from credentials.apps.core.tests.mixins import SiteMixin from credentials.apps.credentials.models import ProgramCompletionEmailConfiguration from credentials.apps.credentials.tests.factories import ProgramCertificateFactory -from credentials.apps.credentials.utils import ( - datetime_from_visible_date, - send_program_certificate_created_message, - validate_duplicate_attributes, -) +from credentials.apps.credentials.utils import send_program_certificate_created_message, validate_duplicate_attributes User = get_user_model() @@ -47,16 +42,6 @@ def test_with_duplicate_attributes(self): self.assertFalse(validate_duplicate_attributes(attributes)) - def test_datetime_from_visible_date(self): - """Verify that we convert LMS dates correctly.""" - self.assertIsNone(datetime_from_visible_date("")) - self.assertIsNone(datetime_from_visible_date("2018-07-31")) - self.assertIsNone(datetime_from_visible_date("2018-07-31T09:32:46+00:00")) # should be Z for timezone - self.assertEqual( - datetime_from_visible_date("2018-07-31T09:32:46Z"), - datetime.datetime(2018, 7, 31, 9, 32, 46, tzinfo=datetime.timezone.utc), - ) - @override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend") class ProgramCertificateIssuedEmailTests(SiteMixin, TestCase): diff --git a/credentials/apps/credentials/tests/test_views.py b/credentials/apps/credentials/tests/test_views.py index 71a761251..914883730 100644 --- a/credentials/apps/credentials/tests/test_views.py +++ b/credentials/apps/credentials/tests/test_views.py @@ -13,7 +13,6 @@ from django.urls import reverse from django.utils.text import slugify from faker import Faker -from waffle.testutils import override_switch from credentials.apps.catalog.data import OrganizationDetails, ProgramDetails from credentials.apps.catalog.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory @@ -48,7 +47,11 @@ def setUp(self): for course_run in self.course_runs ] self.program = ProgramFactory(title="TestProgram1", course_runs=self.course_runs, site=self.site) - self.program_certificate = factories.ProgramCertificateFactory(site=self.site, program_uuid=self.program.uuid) + self.program_certificate = factories.ProgramCertificateFactory( + site=self.site, + program_uuid=self.program.uuid, + program=self.program, + ) self.program_certificate.program = self.program self.program_certificate.save() self.signatory_1 = factories.SignatoryFactory() @@ -64,11 +67,6 @@ def setUp(self): ) for course_cert in self.course_certificates ] - self.visible_date_attr = factories.UserCredentialAttributeFactory( - user_credential=self.user_credential, - name="visible_date", - value="1970-01-01T01:01:01Z", - ) self.platform_name = self.site.siteconfiguration.platform_name user = UserFactory(username=self.MOCK_USER_DATA["username"]) self.client.login(username=user.username, password=USER_PASSWORD) @@ -80,12 +78,14 @@ def _render_user_credential( program_certificate=None, custom_orgs=None, test_user_data=None, + expected_status_code=None, ): """Helper method to render a user certificate.""" user_credential = user_credential or self.user_credential program_certificate = program_certificate or self.program_certificate program_uuid = program_certificate.program_uuid credential_title = program_certificate.title or self.PROGRAM_NAME + expected_status_code = expected_status_code or 200 if custom_orgs: organizations = custom_orgs @@ -113,7 +113,7 @@ def _render_user_credential( user_data.return_value = test_user_data if test_user_data else self.MOCK_USER_DATA mock_program_details.return_value = mocked_program_data response = self.client.get(user_credential.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, expected_status_code) return response @@ -242,33 +242,40 @@ def test_invalid_uuid(self): response = self.client.get(path) self.assertEqual(response.status_code, 404) + @ddt.data((200, True), (404, False)) + @ddt.unpack @responses.activate - def test_invalid_site(self): - """Verify that the view returns a 404 if user_credentials are displayed on a site - they are not associated with. - """ - domain = "unused.testsite" - site_configuration = SiteConfigurationFactory( - site__domain=domain, + def test_url_only_renders_on_correct_site(self, expected_return, is_same_site): + """Verify that the view only renders an accessible URL if the credentials are from the + site being rendered""" + if is_same_site: + site = self.site + course_runs = self.course_runs + else: + domain = "unused.testsite" + site_configuration = SiteConfigurationFactory( + site__domain=domain, + ) + site = site_configuration.site + course = CourseFactory.create(site=site) + course_runs = CourseRunFactory.create_batch(2, course=course) + test_program = ProgramFactory(title="TestProgram2", course_runs=course_runs, site=site) + test_program_certificate = factories.ProgramCertificateFactory( + site=site, + program_uuid=test_program.uuid, + program=test_program, ) - test_site = site_configuration.site - test_program_certificate = factories.ProgramCertificateFactory(site=test_site) test_signatory_1 = factories.SignatoryFactory() test_signatory_2 = factories.SignatoryFactory() test_program_certificate.signatories.add(test_signatory_1, test_signatory_2) test_user_credential = factories.UserCredentialFactory( username=self.MOCK_USER_DATA["username"], credential=test_program_certificate ) - response = self.client.get(test_user_credential.get_absolute_url()) - self.assertEqual(response.status_code, 404) - # Change the program certificate site to the client's site and check that the - # response returns the user's certificate. - test_program_certificate.site = self.site - test_program_certificate.save() response = self._render_user_credential( - user_credential=test_user_credential, program_certificate=test_program_certificate + user_credential=test_user_credential, + expected_status_code=expected_return, ) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, expected_return) def test_invalid_credential(self): """Verify the view returns 404 for attempts to render unsupported credentials.""" @@ -276,40 +283,6 @@ def test_invalid_credential(self): response = self.client.get(self.user_credential.get_absolute_url()) self.assertEqual(response.status_code, 404) - # These four tests should be removed in MICROBA-1198 in favor of the next - # three tests. - def test_future_visible_date(self): - """Verify that the view returns 404 when the uuid is valid but certificate is not yet visible.""" - self.visible_date_attr.value = "9999-01-01T01:01:01Z" - self.visible_date_attr.save() - response = self.client.get(self.user_credential.get_absolute_url()) - self.assertEqual(response.status_code, 404) - - # (This test is not replicated below because the certificate_available_date - # field has validation that will prevent non-valid date data.) - @responses.activate - def test_invalid_visible_date(self): - """Verify that the view just returns normally when the valid_date attribute can't be understood.""" - self.visible_date_attr.value = "hello" - self.visible_date_attr.save() - self._render_user_credential() # Will raise exception if not 200 status - - @responses.activate - def test_no_visible_date(self): - """Verify that the view just returns normally when there isn't a valid_date attribute.""" - self.visible_date_attr.delete() - self._render_user_credential() # Will raise exception if not 200 status - - @responses.activate - def test_visible_date_as_issue_date(self): - """Verify that the view renders the visible_date as the issue date.""" - response = self._render_user_credential() - self.assertContains(response, "Issued January 1970") - - # The following three tests are the same as the previous four, but with the - # USE_CERTIFICATE_AVAILABLE_DATE waffle switch enabled. Clean up previous - # tests in MICROBA-1198. - @override_switch("credentials.use_certificate_available_date", True) def test_future_certificate_available_date(self): """Verify that the view returns 404 when the uuid is valid but certificate is not yet visible.""" self.course_certificates[0].certificate_available_date = "9999-05-11T03:14:01Z" @@ -317,7 +290,6 @@ def test_future_certificate_available_date(self): response = self.client.get(self.user_credential.get_absolute_url()) self.assertEqual(response.status_code, 404) - @override_switch("credentials.use_certificate_available_date", active=True) @responses.activate def test_no_certificate_available_date(self): """Verify that the view just returns normally when there isn't a valid_date attribute.""" @@ -325,29 +297,12 @@ def test_no_certificate_available_date(self): self.course_certificates[0].save() self._render_user_credential() # Will raise exception if not 200 status - @override_switch("credentials.use_certificate_available_date", active=True) @responses.activate def test_visible_certificate_available_date(self): - """Verify that the view renders the visible_date as the issue date.""" + """Verify that the view renders the date at which the certificate is visible as the issue date.""" response = self._render_user_credential() self.assertContains(response, "Issued May 1994") - @override_switch("credentials.use_certificate_available_date", active=True) - @responses.activate - def test_visible_date_as_issue_date_with_no_cert_availability_date_date(self): - """Verify that the view renders the visible_date as the issue date.""" - for course_user_credential in self.course_user_credentials: - factories.UserCredentialAttributeFactory( - user_credential=course_user_credential, - name="visible_date", - value="2021-01-01T01:01:01Z", - ) - for cert in self.course_certificates: - cert.certificate_available_date = None - cert.save() - response = self._render_user_credential() - self.assertContains(response, "Issued January 2021") - @responses.activate def test_signatory_organization_name_override(self): """Verify that the view response contain signatory organization name if signatory have organization.""" @@ -374,7 +329,9 @@ def test_render_language(self, language_set, expected_text): """ if language_set: ProgramCertificate.objects.update_or_create( - program_uuid=self.program_certificate.program_uuid, defaults={"language": "es_419"} + program_uuid=self.program_certificate.program_uuid, + defaults={"language": "es_419"}, + program=self.program, ) response = self._render_user_credential() self.assertContains(response, expected_text) diff --git a/credentials/apps/credentials/utils.py b/credentials/apps/credentials/utils.py index 2a6a5af92..03706ef32 100644 --- a/credentials/apps/credentials/utils.py +++ b/credentials/apps/credentials/utils.py @@ -2,6 +2,7 @@ import logging import textwrap from itertools import groupby +from typing import TYPE_CHECKING, Dict, Optional from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -11,13 +12,15 @@ from credentials.apps.catalog.data import ProgramStatus from credentials.apps.core.api import get_user_by_username from credentials.apps.credentials.messages import ProgramCertificateIssuedMessage -from credentials.apps.credentials.models import ( - ProgramCompletionEmailConfiguration, - UserCredential, - UserCredentialAttribute, -) +from credentials.apps.credentials.models import ProgramCompletionEmailConfiguration, UserCredential +if TYPE_CHECKING: + from django.db.models import DateTimeField + from django.db.models.query import QuerySet + + from credentials.apps.credentials.models import ProgramCertificate + log = logging.getLogger(__name__) VISIBLE_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" @@ -52,43 +55,19 @@ def keyfunc(attribute): return True -def datetime_from_visible_date(date): - """Turn a string version of a datetime, provided to us by the LMS in a particular format it uses for - visible_date attributes, and turn it into a datetime object.""" - try: - parsed = datetime.datetime.strptime(date, VISIBLE_DATE_FORMAT) - # The timezone is always UTC (as indicated by the Z). It looks like in python3.7, we could - # just use %z instead of replacing the tzinfo with a UTC value. - return parsed.replace(tzinfo=datetime.timezone.utc) - except ValueError as e: - log.exception("%s", e) - return None - - -# TODO: Refactor this when removing USE_CERTIFICATE_AVAILABLE_DATE toggle: -# MICROBA-1198 -def filter_visible(qs): +def filter_visible(qs: "QuerySet") -> "QuerySet": """ Filters a UserCredentials queryset by excluding credentials that aren't supposed to be visible yet. """ + visible_course_certs = _filter_visible_course_certificates(qs.filter(course_credentials__isnull=False)) + visible_program_certs = _filter_visible_program_certificates(qs.filter(program_credentials__isnull=False)) + visible_certs = visible_course_certs | visible_program_certs - if settings.USE_CERTIFICATE_AVAILABLE_DATE.is_enabled(): - visible_course_certs = _filter_visible_course_certificates(qs.filter(course_credentials__isnull=False)) - visible_program_certs = _filter_visible_program_certificates(qs.filter(program_credentials__isnull=False)) - visible_certs = visible_course_certs | visible_program_certs - - return visible_certs + return visible_certs - # The visible_date attribute holds a string value, not a datetime one. But we can compare as a string - # because the format is so strict - it will still lexically compare as less/greater-than. - nowstr = datetime.datetime.now(datetime.timezone.utc).strftime(VISIBLE_DATE_FORMAT) - return qs.filter( - Q(attributes__name="visible_date", attributes__value__lte=nowstr) | ~Q(attributes__name="visible_date") - ) - -def _filter_visible_course_certificates(query_set): +def _filter_visible_course_certificates(query_set: "QuerySet") -> "QuerySet": """ Filters a UserCredentials queryset by excluding credentials that aren’t supposed to be visible yet according to their certificate_available_date. @@ -107,7 +86,7 @@ def _filter_visible_course_certificates(query_set): ) -def _filter_visible_program_certificates(query_set): +def _filter_visible_program_certificates(query_set: "QuerySet") -> "QuerySet": """ Filters a UserCredentials queryset by excluding credentials that aren’t supposed to be visible yet according to their certificate_available_date. @@ -120,7 +99,7 @@ def _filter_visible_program_certificates(query_set): (QuerySet): A queryset of program UserCredentials that should be visible. """ now = datetime.datetime.now(datetime.timezone.utc) - visible_program_cert_ids = [] + visible_program_cert_ids = [] # type: list[Optional[int]] for user_credential in query_set: program_visible_date = _get_program_certificate_visible_date(user_credential) if program_visible_date and program_visible_date <= now: @@ -128,7 +107,7 @@ def _filter_visible_program_certificates(query_set): return UserCredential.objects.filter(pk__in=visible_program_cert_ids) -def _get_program_certificate_visible_date(user_program_credential): +def _get_program_certificate_visible_date(user_program_credential: UserCredential) -> Optional[datetime.datetime]: """ Finds the program credential visible date by finding the latest associated course certificate_available_date. If a course credential has no @@ -142,7 +121,7 @@ def _get_program_certificate_visible_date(user_program_credential): (DateTime or None): The date on which the program credential should be visible. (It shouldn’t return None but is technically possible.) """ - last_date = None + last_date = None # type: Optional[datetime.datetime] for course_run in user_program_credential.credential.program.course_runs.all(): # Does the user have a course cert for this course run? course_run_cert = UserCredential.objects.filter( @@ -157,11 +136,11 @@ def _get_program_certificate_visible_date(user_program_credential): return last_date -def _get_issue_date_for_course_credential(course_run_user_credentials): +def _get_issue_date_for_course_credential(course_run_user_credentials: UserCredential) -> "DateTimeField": """ Retrieves the issue date for a given course run UserCredential. This method - attempts to find the date based on the certificate availability date and - the visible date attribute. + attempts to find the date based on the certificate availability date + and falls back to created date. Arguments: course_run_user_credentials (UserCredential): A Course Run UserCredential @@ -170,22 +149,13 @@ def _get_issue_date_for_course_credential(course_run_user_credentials): datetime: The datetime that the credential should be visible and was issued. """ if course_run_user_credentials.credential.certificate_available_date: - date = course_run_user_credentials.credential.certificate_available_date - else: - try: - visible_date = UserCredentialAttribute.objects.prefetch_related("user_credential__credential").get( - user_credential=course_run_user_credentials, name="visible_date" - ) - date = datetime_from_visible_date(visible_date.value) - except UserCredentialAttribute.DoesNotExist: - date = course_run_user_credentials.created - log.info(f"UserCredential {course_run_user_credentials.id} does not have a visible_date attribute") - return date - - -# TODO: Refactor this when removing USE_CERTIFICATE_AVAILABLE_DATE toggle: -# MICROBA-1198 -def get_credential_visible_dates(user_credentials, use_date_override=False): + return course_run_user_credentials.credential.certificate_available_date + return course_run_user_credentials.created + + +def get_credential_visible_dates( + user_credentials, use_date_override: bool = False +) -> Dict[UserCredential, "DateTimeField"]: """ Calculates visible date for a collection of UserCredentials. Returns a dictionary of {UserCredential: datetime}. @@ -204,55 +174,42 @@ def get_credential_visible_dates(user_credentials, use_date_override=False): ... } """ - - if settings.USE_CERTIFICATE_AVAILABLE_DATE.is_enabled(): - visible_date_dict = {} - - for user_credential in user_credentials: - date = None - # If this is a course credential - if user_credential.course_credentials.exists(): - date = _get_issue_date_for_course_credential(user_credential) - - # Date override only applies to Course Run UserCredential dates - # we should reconsider this if we ever decide they should - # impact the issue date of Program Certs. - if use_date_override: - try: - date = user_credential.date_override.date - except ObjectDoesNotExist: - pass - - # If this is a program credential - if user_credential.program_credentials.exists(): - date = _get_program_certificate_visible_date(user_credential) - - visible_date_dict[user_credential] = date - - return visible_date_dict - - visible_dates = UserCredentialAttribute.objects.prefetch_related("user_credential__credential").filter( - user_credential__in=user_credentials, name="visible_date" - ) - - visible_date_dict = { - visible_date.user_credential: datetime_from_visible_date(visible_date.value) for visible_date in visible_dates - } + visible_date_dict = {} for user_credential in user_credentials: - current = visible_date_dict.get(user_credential) - if current is None: - visible_date_dict[user_credential] = user_credential.created + date = None + # If this is a course credential + if user_credential.course_credentials.exists(): + date = _get_issue_date_for_course_credential(user_credential) + + # Date override only applies to Course Run UserCredential dates + # we should reconsider this if we ever decide they should + # impact the issue date of Program Certs. + if use_date_override: + try: + date = user_credential.date_override.date + except ObjectDoesNotExist: + pass + + # If this is a program credential + elif user_credential.program_credentials.exists(): + date = _get_program_certificate_visible_date(user_credential) + + visible_date_dict[user_credential] = date return visible_date_dict -def get_credential_visible_date(user_credential, use_date_override=False): +def get_credential_visible_date( + user_credential: UserCredential, use_date_override: bool = False +) -> Dict[UserCredential, "DateTimeField"]: """Simpler, one-credential version of get_credential_visible_dates.""" return get_credential_visible_dates([user_credential], use_date_override)[user_credential] -def send_program_certificate_created_message(username, program_certificate, lms_user_id): +def send_program_certificate_created_message( + username: str, program_certificate: "ProgramCertificate", lms_user_id: int +) -> None: """ If the learner has earned a Program Certificate then we go ahead and send them an automated email congratulating them for their achievement. Emails to learners in credit eligible Programs will contain additional information. diff --git a/credentials/apps/records/rest_api/v1/tests/test_serializers.py b/credentials/apps/records/rest_api/v1/tests/test_serializers.py index e4dfbad82..45aeeb279 100644 --- a/credentials/apps/records/rest_api/v1/tests/test_serializers.py +++ b/credentials/apps/records/rest_api/v1/tests/test_serializers.py @@ -37,7 +37,9 @@ def setUp(self): ) for course_run in self.course_runs ] - self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) + self.program_cert = ProgramCertificateFactory.create( + program_uuid=self.program.uuid, site=self.site, program=self.program + ) self.course_credential_content_type = ContentType.objects.get( app_label="credentials", model="coursecertificate" ) @@ -79,6 +81,8 @@ def serialize_program_record_details(self): ).data def test_valid_data_zero_programs(self): + """Verify the serializer produces an empty list if there are no programs""" + self.program_cert.delete() self.program.delete() serializer = self.serialize_program_records() expected = [] diff --git a/credentials/apps/records/tests/test_api.py b/credentials/apps/records/tests/test_api.py index c708d415a..6fb6ccb0f 100644 --- a/credentials/apps/records/tests/test_api.py +++ b/credentials/apps/records/tests/test_api.py @@ -4,11 +4,9 @@ import datetime -from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import slugify from django.test import TestCase -from edx_toggles.toggles.testutils import override_waffle_switch from credentials.apps.catalog.tests.factories import ( CourseFactory, @@ -23,7 +21,6 @@ from credentials.apps.credentials.tests.factories import ( CourseCertificateFactory, ProgramCertificateFactory, - UserCredentialAttributeFactory, UserCredentialFactory, ) from credentials.apps.records.api import ( @@ -100,8 +97,12 @@ def setUp(self): course_id=self.course3_courserunA.key, site=self.site ) # create program certificate configurations so we can grant program credentials to the test learners - self.program1_cert_config = ProgramCertificateFactory.create(program_uuid=self.program1.uuid, site=self.site) - self.program2_cert_config = ProgramCertificateFactory.create(program_uuid=self.program2.uuid, site=self.site) + self.program1_cert_config = ProgramCertificateFactory.create( + program_uuid=self.program1.uuid, site=self.site, program=self.program1 + ) + self.program2_cert_config = ProgramCertificateFactory.create( + program_uuid=self.program2.uuid, site=self.site, program=self.program2 + ) # create grade for learner in course-run1 of course1 self.course1_courserunA_grade = UserGradeFactory( username=self.user.username, @@ -397,53 +398,6 @@ def test_get_transformed_grade_data_no_grade_no_credential(self): self._assert_results(expected_highest_attempt_dict, highest_attempt_dict) assert expected_issue_date_course1 == last_updated - def test_get_transformed_grade_data_earned_credential_with_visible_date(self): - """ - A test that verifies an edge case of the `_get_transformed_grade_data` utility function. If a course credential - is associated with a visible date that is set in the future, then it should not be included as part of the - results. In this test scenario, we add a "visible date" attribute to the learner's (course) credential instance - in "course1_courserun2", and then verify the data that is returned by this function. - """ - UserCredentialAttributeFactory( - user_credential=self.course_credential_course1_courserunB, - name="visible_date", - value="9999-01-01T01:01:01Z", - ) - - expected_issue_date_course1 = get_credential_dates(self.course_credential_course1_courserunA, False) - expected_issue_date_course2 = get_credential_dates(self.course_credential_course2_courserunA, False) - expected_result = [ - { - "name": self.course1.title, - "school": ",".join(self.course1.owners.values_list("name", flat=True)), - "attempts": 1, - "course_id": self.course1_courserunA.key, - "issue_date": expected_issue_date_course1.isoformat(), - "percent_grade": 0.75, - "letter_grade": "C", - }, - { - "name": self.course2.title, - "school": ",".join(self.course2.owners.values_list("name", flat=True)), - "attempts": 1, - "course_id": self.course2_courserunA.key, - "issue_date": expected_issue_date_course2.isoformat(), - "percent_grade": 0.85, - "letter_grade": "B", - }, - ] - expected_highest_attempt_dict = { - self.course1: self.course1_courserunA_grade, - self.course2: self.course2_courserunA_grade, - } - - result, highest_attempt_dict, last_updated = _get_transformed_grade_data(self.program1, self.user) - self._assert_results(expected_result[0], result[0]) - self._assert_results(expected_result[1], result[1]) - self._assert_results(expected_highest_attempt_dict, highest_attempt_dict) - assert expected_issue_date_course2 == last_updated - - @override_waffle_switch(settings.USE_CERTIFICATE_AVAILABLE_DATE, active=True) def test_get_transformed_grade_data_earned_credential_with_certificate_available_date(self): """ A test that verifies an edge case of the `_get_transformed_grade_data` utility function. If a course credential diff --git a/credentials/apps/records/tests/test_utils.py b/credentials/apps/records/tests/test_utils.py index 96b7ef695..ca7fe274a 100644 --- a/credentials/apps/records/tests/test_utils.py +++ b/credentials/apps/records/tests/test_utils.py @@ -49,7 +49,11 @@ def setUp(self): self.client.login(username=self.user.username, password=USER_PASSWORD) self.program = ProgramFactory(site=self.site) self.pathway = PathwayFactory(site=self.site, programs=[self.program]) - self.pc = ProgramCertificateFactory(site=self.site, program_uuid=self.program.uuid) + self.pc = ProgramCertificateFactory( + site=self.site, + program_uuid=self.program.uuid, + program=self.program, + ) self.pcr = ProgramCertRecordFactory(program=self.program, user=self.user) self.data = {"username": self.USERNAME, "pathway_id": self.pathway.id} self.url = reverse("records:share_program", kwargs={"uuid": self.program.uuid.hex}) @@ -123,7 +127,11 @@ def setUp(self): ) for course_run in self.course_runs ] - self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) + self.program_cert = ProgramCertificateFactory.create( + program_uuid=self.program.uuid, + site=self.site, + program=self.program, + ) self.course_credential_content_type = ContentType.objects.get( app_label="credentials", model="coursecertificate" ) @@ -204,6 +212,7 @@ def test_course_credentials_to_course_runs_multiple(self): class GetCredentialsTests(SiteMixin, TestCase): + def setUp(self): super().setUp() self.user = UserFactory() @@ -221,7 +230,11 @@ def setUp(self): ) for course_run in self.course_runs ] - self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) + self.program_cert = ProgramCertificateFactory.create( + program_uuid=self.program.uuid, + site=self.site, + program=self.program, + ) self.course_credential_content_type = ContentType.objects.get( app_label="credentials", model="coursecertificate" ) @@ -243,6 +256,7 @@ def setUp(self): ) def test_get_credentials_both_empty(self): + """Verifies that empty sets are returned when there are no course or program certificates""" for course_cert in self.course_certs: course_cert.delete() self.program_cert.delete() @@ -251,20 +265,13 @@ def test_get_credentials_both_empty(self): assert program_results == [] def test_get_credentials_course_only(self): + """Verify that the correct results are returned when there are course certificates + but not program certificates""" self.program_cert.delete() course_results, program_results = get_credentials(self.user.username) assert course_results == self.course_user_credentials assert program_results == [] - def test_get_credentials_program_only(self): - for course_cert in self.course_certs: - course_cert.delete() - for course_run in self.course_runs: - course_run.delete() - course_results, program_results = get_credentials(self.user.username) - assert course_results == [] - assert program_results[0] == self.program_user_credential - def test_get_credentials_both_course_and_program(self): course_results, program_results = get_credentials(self.user.username) assert course_results == self.course_user_credentials @@ -289,7 +296,11 @@ def setUp(self): ) for course_run in self.course_runs ] - self.program_cert = ProgramCertificateFactory.create(program_uuid=self.program.uuid, site=self.site) + self.program_cert = ProgramCertificateFactory.create( + program_uuid=self.program.uuid, + site=self.site, + program=self.program, + ) self.course_credential_content_type = ContentType.objects.get( app_label="credentials", model="coursecertificate" ) @@ -333,7 +344,11 @@ def test_get_user_program_data_include_retired_programs_false(self): site=self.site, status=ProgramStatus.RETIRED.value, ) - self.program_cert2 = ProgramCertificateFactory.create(program_uuid=self.program2.uuid, site=self.site) + self.program_cert2 = ProgramCertificateFactory.create( + program_uuid=self.program2.uuid, + site=self.site, + program=self.program2, + ) self.program_user_credential2 = UserCredentialFactory.create( username=self.user.username, credential_content_type=self.program_credential_content_type, @@ -354,7 +369,11 @@ def test_get_user_program_data_include_retired_programs_true(self): site=self.site, status=ProgramStatus.RETIRED.value, ) - self.program_cert2 = ProgramCertificateFactory.create(program_uuid=self.program2.uuid, site=self.site) + self.program_cert2 = ProgramCertificateFactory.create( + program_uuid=self.program2.uuid, + site=self.site, + program=self.program2, + ) self.program_user_credential2 = UserCredentialFactory.create( username=self.user.username, credential_content_type=self.program_credential_content_type, @@ -372,8 +391,8 @@ def test_get_user_program_data_include_retired_programs_true(self): assert not result[1]["empty"] def test_get_user_program_data_zero_programs(self): - self.program.delete() self.program_cert.delete() + self.program.delete() self.program_user_credential.delete() result = get_user_program_data(self.user.username, self.site) assert result == [] @@ -389,7 +408,11 @@ def test_get_user_program_data_multiple_programs(self): self.program2 = ProgramFactory( title="TestProgram2", course_runs=self.course_runs, authoring_organizations=self.orgs, site=self.site ) - self.program_cert2 = ProgramCertificateFactory.create(program_uuid=self.program2.uuid, site=self.site) + self.program_cert2 = ProgramCertificateFactory.create( + program_uuid=self.program2.uuid, + site=self.site, + program=self.program2, + ) self.program_user_credential2 = UserCredentialFactory.create( username=self.user.username, credential_content_type=self.program_credential_content_type, diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..656e84823 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,28 @@ +[mypy] +follow_imports = silent +ignore_missing_imports = True +allow_untyped_globals = True +plugins = + mypy_django_plugin.main, + mypy_drf_plugin.main +files = + credentials + +[mypy.plugins.django-stubs] +django_settings_module = "credentials.test" + +# Selectively ignore packages known to be lacking type hints +[mypy-ddt.*] +ignore_missing_imports = True +[mypy-edx_api_doc_tools.*] +ignore_missing_imports = True +[mypy-edx_django_utils.*] +ignore_missing_imports = True +[mypy-edx_rest_api_client.*] +ignore_missing_imports = True +[mypy-edx_rest_framework_extensions.*] +ignore_missing_imports = True +[mypy-search.*] +ignore_missing_imports = True +[mypy-rules.*] +ignore_missing_imports = True diff --git a/requirements/all.txt b/requirements/all.txt index 5ffadfc54..408db1337 100644 --- a/requirements/all.txt +++ b/requirements/all.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -10,6 +10,7 @@ asgiref==3.8.1 # -r requirements/production.txt # django # django-cors-headers + # django-stubs astroid==3.2.2 # via # -r requirements/dev.txt @@ -26,24 +27,17 @@ backoff==2.2.1 # -r requirements/dev.txt # -r requirements/production.txt # segment-analytics-python -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/dev.txt - # -r requirements/production.txt - # django - # djangorestframework black==24.4.2 # via -r requirements/dev.txt bleach==6.1.0 # via # -r requirements/dev.txt # -r requirements/production.txt -boto3==1.34.132 +boto3==1.34.135 # via # -r requirements/production.txt # django-ses -botocore==1.34.132 +botocore==1.34.135 # via # -r requirements/production.txt # boto3 @@ -149,6 +143,8 @@ django==4.2.13 # django-simple-history # django-statici18n # django-storages + # django-stubs + # django-stubs-ext # django-waffle # djangorestframework # drf-jwt @@ -217,6 +213,12 @@ django-storages==1.14.3 # via # -r requirements/dev.txt # -r requirements/production.txt +django-stubs==5.0.2 + # via -r requirements/dev.txt +django-stubs-ext==5.0.2 + # via + # -r requirements/dev.txt + # django-stubs django-waffle==4.1.0 # via # -r requirements/dev.txt @@ -315,13 +317,9 @@ edx-toggles==5.2.0 # -r requirements/dev.txt # -r requirements/production.txt # edx-event-bus-kafka -exceptiongroup==1.2.1 - # via - # -r requirements/dev.txt - # pytest factory-boy==3.3.0 # via -r requirements/dev.txt -faker==25.9.1 +faker==26.0.0 # via # -r requirements/dev.txt # factory-boy @@ -354,12 +352,6 @@ idna==3.7 # -r requirements/dev.txt # -r requirements/production.txt # requests -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # -r requirements/dev.txt - # -r requirements/production.txt - # markdown inflection==0.5.1 # via # -r requirements/dev.txt @@ -409,10 +401,13 @@ mccabe==0.7.0 # via # -r requirements/dev.txt # pylint +mypy==1.10.1 + # via -r requirements/dev.txt mypy-extensions==1.0.0 # via # -r requirements/dev.txt # black + # mypy mysqlclient==2.2.4 # via # -r requirements/dev.txt @@ -513,7 +508,7 @@ pyjwt[crypto]==2.8.0 # edx-rest-api-client # segment-analytics-python # social-auth-core -pylint==3.2.3 +pylint==3.2.5 # via # -r requirements/dev.txt # edx-lint @@ -537,7 +532,7 @@ pymemcache==4.0.0 # via # -r requirements/dev.txt # -r requirements/production.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/dev.txt # -r requirements/production.txt @@ -697,29 +692,24 @@ text-unidecode==1.3 # -r requirements/dev.txt # -r requirements/production.txt # python-slugify -tomli==2.0.1 - # via - # -r requirements/dev.txt - # black - # pylint - # pyproject-api - # pytest - # tox tomlkit==0.12.5 # via # -r requirements/dev.txt # pylint tox==4.15.1 # via -r requirements/dev.txt +types-pyyaml==6.0.12.20240311 + # via + # -r requirements/dev.txt + # django-stubs typing-extensions==4.12.2 # via # -r requirements/dev.txt # -r requirements/production.txt - # asgiref - # astroid - # black + # django-stubs + # django-stubs-ext # edx-opaque-keys - # pylint + # mypy # qrcode uritemplate==4.1.1 # via @@ -748,11 +738,6 @@ xss-utils==0.6.0 # via # -r requirements/dev.txt # -r requirements/production.txt -zipp==3.19.2 - # via - # -r requirements/dev.txt - # -r requirements/production.txt - # importlib-metadata zope-event==5.0 # via # -r requirements/production.txt diff --git a/requirements/base.txt b/requirements/base.txt index 86be8e979..e372ecac0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -14,11 +14,6 @@ attrs==23.2.0 # openedx-events backoff==2.2.1 # via segment-analytics-python -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # django - # djangorestframework bleach==6.1.0 # via -r requirements/base.in certifi==2024.6.2 @@ -170,10 +165,6 @@ fontawesomefree==6.5.1 # via -r requirements/base.in idna==3.7 # via requests -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # markdown inflection==0.5.1 # via drf-yasg itypes==1.2.0 @@ -233,7 +224,7 @@ pyjwt[crypto]==2.8.0 # social-auth-core pymemcache==4.0.0 # via -r requirements/base.in -pymongo==4.7.3 +pymongo==4.8.0 # via edx-opaque-keys pynacl==1.5.0 # via edx-django-utils @@ -314,7 +305,6 @@ text-unidecode==1.3 # via python-slugify typing-extensions==4.12.2 # via - # asgiref # edx-opaque-keys # qrcode uritemplate==4.1.1 @@ -329,5 +319,3 @@ webencodings==0.5.1 # via bleach xss-utils==0.6.0 # via -r requirements/base.in -zipp==3.19.2 - # via importlib-metadata diff --git a/requirements/dev.in b/requirements/dev.in index 6999c1301..a51235dcb 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -17,3 +17,6 @@ pywatchman; "linux" in sys_platform # For local debugging django-debug-toolbar + +django-stubs # Typing stubs for Django, so it works with mypy +mypy \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 4ce860f89..12dee1af1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -9,6 +9,7 @@ asgiref==3.8.1 # -r requirements/test.txt # django # django-cors-headers + # django-stubs astroid==3.2.2 # via # -r requirements/test.txt @@ -23,12 +24,6 @@ backoff==2.2.1 # via # -r requirements/test.txt # segment-analytics-python -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/test.txt - # django - # djangorestframework black==24.4.2 # via -r requirements/test.txt bleach==6.1.0 @@ -121,6 +116,8 @@ django==4.2.13 # django-simple-history # django-statici18n # django-storages + # django-stubs + # django-stubs-ext # django-waffle # djangorestframework # drf-jwt @@ -167,6 +164,10 @@ django-statici18n==2.5.0 # via -r requirements/test.txt django-storages==1.14.3 # via -r requirements/test.txt +django-stubs==5.0.2 + # via -r requirements/dev.in +django-stubs-ext==5.0.2 + # via django-stubs django-waffle==4.1.0 # via # -r requirements/test.txt @@ -237,13 +238,9 @@ edx-toggles==5.2.0 # via # -r requirements/test.txt # edx-event-bus-kafka -exceptiongroup==1.2.1 - # via - # -r requirements/test.txt - # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==25.9.1 +faker==26.0.0 # via # -r requirements/test.txt # factory-boy @@ -264,11 +261,6 @@ idna==3.7 # via # -r requirements/test.txt # requests -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # -r requirements/test.txt - # markdown inflection==0.5.1 # via # -r requirements/test.txt @@ -306,10 +298,13 @@ mccabe==0.7.0 # via # -r requirements/test.txt # pylint +mypy==1.10.1 + # via -r requirements/dev.in mypy-extensions==1.0.0 # via # -r requirements/test.txt # black + # mypy mysqlclient==2.2.4 # via -r requirements/test.txt newrelic==9.11.0 @@ -388,7 +383,7 @@ pyjwt[crypto]==2.8.0 # edx-rest-api-client # segment-analytics-python # social-auth-core -pylint==3.2.3 +pylint==3.2.5 # via # -r requirements/test.txt # edx-lint @@ -410,7 +405,7 @@ pylint-plugin-utils==0.8.2 # pylint-django pymemcache==4.0.0 # via -r requirements/test.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/test.txt # edx-opaque-keys @@ -538,28 +533,21 @@ text-unidecode==1.3 # via # -r requirements/test.txt # python-slugify -tomli==2.0.1 - # via - # -r requirements/test.txt - # black - # pylint - # pyproject-api - # pytest - # tox tomlkit==0.12.5 # via # -r requirements/test.txt # pylint tox==4.15.1 # via -r requirements/test.txt +types-pyyaml==6.0.12.20240311 + # via django-stubs typing-extensions==4.12.2 # via # -r requirements/test.txt - # asgiref - # astroid - # black + # django-stubs + # django-stubs-ext # edx-opaque-keys - # pylint + # mypy # qrcode uritemplate==4.1.1 # via @@ -582,7 +570,3 @@ webencodings==0.5.1 # bleach xss-utils==0.6.0 # via -r requirements/test.txt -zipp==3.19.2 - # via - # -r requirements/test.txt - # importlib-metadata diff --git a/requirements/docs.txt b/requirements/docs.txt index 7f10ec2e8..5ef47f5bf 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade # -accessible-pygments==0.0.4 +accessible-pygments==0.0.5 # via pydata-sphinx-theme -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx babel==2.15.0 # via @@ -18,7 +18,7 @@ certifi==2024.6.2 # via requests charset-normalizer==3.3.2 # via requests -docutils==0.19 +docutils==0.21.2 # via # pydata-sphinx-theme # sphinx @@ -26,10 +26,6 @@ idna==3.7 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # sphinx jinja2==3.1.4 # via sphinx jsx-lexer==2.0.1 @@ -40,7 +36,7 @@ packaging==24.1 # via # pydata-sphinx-theme # sphinx -pydata-sphinx-theme==0.14.4 +pydata-sphinx-theme==0.15.4 # via sphinx-book-theme pygments==2.18.0 # via @@ -48,32 +44,30 @@ pygments==2.18.0 # jsx-lexer # pydata-sphinx-theme # sphinx -pytz==2024.1 - # via babel requests==2.32.3 # via sphinx snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 # via beautifulsoup4 -sphinx==6.2.1 +sphinx==7.3.7 # via # -r requirements/docs.in # pydata-sphinx-theme # sphinx-book-theme -sphinx-book-theme==1.0.1 +sphinx-book-theme==1.1.3 # via -r requirements/docs.in -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.8 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.6 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.7 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx typing-extensions==4.12.2 # via pydata-sphinx-theme @@ -81,5 +75,3 @@ urllib3==1.26.19 # via # -c requirements/constraints.txt # requests -zipp==3.19.2 - # via importlib-metadata diff --git a/requirements/pip.txt b/requirements/pip.txt index c7fd0782f..fe304ec4e 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -8,7 +8,7 @@ wheel==0.43.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==24.1 +pip==24.1.1 # via -r requirements/pip.in setuptools==70.1.1 # via -r requirements/pip.in diff --git a/requirements/pip_tools.txt b/requirements/pip_tools.txt index cd5c274bd..0b0b25e96 100644 --- a/requirements/pip_tools.txt +++ b/requirements/pip_tools.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -8,10 +8,6 @@ build==1.2.1 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # build packaging==24.1 # via build pip-tools==7.4.1 @@ -20,14 +16,8 @@ pyproject-hooks==1.1.0 # via # build # pip-tools -tomli==2.0.1 - # via - # build - # pip-tools wheel==0.43.0 # via pip-tools -zipp==3.19.2 - # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/production.txt b/requirements/production.txt index bd756ab5f..6239054b6 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -18,17 +18,11 @@ backoff==2.2.1 # via # -r requirements/base.txt # segment-analytics-python -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/base.txt - # django - # djangorestframework bleach==6.1.0 # via -r requirements/base.txt -boto3==1.34.132 +boto3==1.34.135 # via django-ses -botocore==1.34.132 +botocore==1.34.135 # via # boto3 # s3transfer @@ -217,11 +211,6 @@ idna==3.7 # via # -r requirements/base.txt # requests -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # -r requirements/base.txt - # markdown inflection==0.5.1 # via # -r requirements/base.txt @@ -315,7 +304,7 @@ pyjwt[crypto]==2.8.0 # social-auth-core pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/base.txt # edx-opaque-keys @@ -430,7 +419,6 @@ text-unidecode==1.3 typing-extensions==4.12.2 # via # -r requirements/base.txt - # asgiref # edx-opaque-keys # qrcode uritemplate==4.1.1 @@ -450,10 +438,6 @@ webencodings==0.5.1 # bleach xss-utils==0.6.0 # via -r requirements/base.txt -zipp==3.19.2 - # via - # -r requirements/base.txt - # importlib-metadata zope-event==5.0 # via gevent zope-interface==6.4.post2 diff --git a/requirements/test.txt b/requirements/test.txt index 3007d960b..9c1c7b72f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade @@ -22,12 +22,6 @@ backoff==2.2.1 # via # -r requirements/base.txt # segment-analytics-python -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/base.txt - # django - # djangorestframework black==24.4.2 # via -r requirements/test.in bleach==6.1.0 @@ -220,11 +214,9 @@ edx-toggles==5.2.0 # via # -r requirements/base.txt # edx-event-bus-kafka -exceptiongroup==1.2.1 - # via pytest factory-boy==3.3.0 # via -r requirements/test.in -faker==25.9.1 +faker==26.0.0 # via factory-boy fastavro==1.9.4 # via @@ -242,11 +234,6 @@ idna==3.7 # via # -r requirements/base.txt # requests -importlib-metadata==6.11.0 - # via - # -c requirements/common_constraints.txt - # -r requirements/base.txt - # markdown inflection==0.5.1 # via # -r requirements/base.txt @@ -356,7 +343,7 @@ pyjwt[crypto]==2.8.0 # edx-rest-api-client # segment-analytics-python # social-auth-core -pylint==3.2.3 +pylint==3.2.5 # via # edx-lint # pylint-celery @@ -372,7 +359,7 @@ pylint-plugin-utils==0.8.2 # pylint-django pymemcache==4.0.0 # via -r requirements/base.txt -pymongo==4.7.3 +pymongo==4.8.0 # via # -r requirements/base.txt # edx-opaque-keys @@ -495,13 +482,6 @@ text-unidecode==1.3 # via # -r requirements/base.txt # python-slugify -tomli==2.0.1 - # via - # black - # pylint - # pyproject-api - # pytest - # tox tomlkit==0.12.5 # via pylint tox==4.15.1 @@ -509,11 +489,7 @@ tox==4.15.1 typing-extensions==4.12.2 # via # -r requirements/base.txt - # asgiref - # astroid - # black # edx-opaque-keys - # pylint # qrcode uritemplate==4.1.1 # via @@ -534,7 +510,3 @@ webencodings==0.5.1 # bleach xss-utils==0.6.0 # via -r requirements/base.txt -zipp==3.19.2 - # via - # -r requirements/base.txt - # importlib-metadata diff --git a/requirements/translations.txt b/requirements/translations.txt index 2a1dc0784..ea01e77d8 100644 --- a/requirements/translations.txt +++ b/requirements/translations.txt @@ -1,15 +1,11 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # make upgrade # asgiref==3.8.1 # via django -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # django django==4.2.13 # via # -c requirements/common_constraints.txt @@ -28,5 +24,3 @@ pyyaml==6.0.1 # via edx-i18n-tools sqlparse==0.5.0 # via django -typing-extensions==4.12.2 - # via asgiref