diff --git a/Dockerfile b/Dockerfile index ac5f629b50..318f053eda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -81,7 +81,6 @@ USER mitodl RUN --mount=type=cache,target=/opt/uv-cache,uid=1000,gid=1000 \ uv sync --frozen --no-install-project -RUN uv pip install watchfiles FROM code AS jupyter-notebook diff --git a/RELEASE.rst b/RELEASE.rst index 831169548b..fdcb607d1e 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,14 @@ Release Notes ============= +Version 1.159.2 +--------------- + +- Filter active cert issue_date in db query (#3748) +- Add some viz into misconfigured contracts (#3744) +- fix: update the beat task timing for retry_failed_edx_enrollments (#3723) +- Enable live reload for the django app (#3738) + Version 1.159.1 (Released July 09, 2026) --------------- diff --git a/b2b/views/v0/manager.py b/b2b/views/v0/manager.py index 6b27e029b0..5f827ffd5e 100644 --- a/b2b/views/v0/manager.py +++ b/b2b/views/v0/manager.py @@ -118,6 +118,12 @@ def _create_discount_codes_for_contract( # For contract attachment JIT code generation, any product associated w/ the contract should # work for creating the discount code, so we just grab the first one. product = contract.get_products().first() + if not product: + log.exception( + "Contract %s has no product, cannot provision JIT discount codes", contract + ) + return new_discounts + for _ in range(count_to_provision): discount = _create_discount_with_product( product, Decimal(0), REDEMPTION_TYPE_ONE_TIME diff --git a/b2b/views/v0/manager_test.py b/b2b/views/v0/manager_test.py index 36663a696a..72d640140f 100644 --- a/b2b/views/v0/manager_test.py +++ b/b2b/views/v0/manager_test.py @@ -1428,6 +1428,51 @@ def test_bulk_assign_provisions_codes_for_uncapped_contract( ) +def test_bulk_assign_no_product_for_uncapped_contract( + org_setup, manager_drf_client, mocker, caplog +): + """bulk_assign doesnt attempt JIT code provisioning for a misconfigured contract without a product.""" + mocker.patch("b2b.views.v0.manager.queue_send_enrollment_code_assignment_email") + _, _, (contract_1, *_), *_ = org_setup + + uncapped_contract = ContractPageFactory.create( + membership_type=CONTRACT_MEMBERSHIP_CODE, + max_learners=None, + organization=contract_1.organization, + ) + + records = [ + {"email": "learner1@example.com", "name": "Learner One"}, + {"email": "learner2@example.com", "name": "Learner Two"}, + ] + + bulk_assign_url = reverse( + "b2b:b2b-manager-org-contract-bulk-assign", + kwargs={ + "parent_lookup_organization": uncapped_contract.organization.id, + "pk": uncapped_contract.id, + }, + ) + + resp = manager_drf_client.post(bulk_assign_url, data=records, format="json") + + assert resp.status_code == status.HTTP_200_OK + + resp_data = resp.json() + assert len(resp_data["assigned"]) == 0 + assert len(resp_data["errors"]) == 2 + assert all("No available code." in err["detail"] for err in resp_data["errors"]) + + assert uncapped_contract.get_discounts().count() == 0 + assert ( + DiscountContractAttachmentRedemption.objects.filter( + contract=uncapped_contract + ).count() + == 0 + ) + assert "has no product" in caplog.text + + def test_bulk_assign_skips_already_assigned_or_redeemed( org_setup, manager_drf_client, mocker ): diff --git a/courses/models.py b/courses/models.py index c871c65c6e..600f5a7d5c 100644 --- a/courses/models.py +++ b/courses/models.py @@ -17,6 +17,7 @@ from django.db import models from django.db.models import Exists, OuterRef, Prefetch, Q from django.db.models.constraints import CheckConstraint, UniqueConstraint +from django.db.models.functions import Now from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -1715,11 +1716,7 @@ def get_queryset(self): Returns: QuerySet: queryset for un-revoked certificates """ - return ( - super() - .get_queryset() - .filter(is_revoked=False, issue_date__lte=now_in_utc()) - ) + return super().get_queryset().filter(is_revoked=False, issue_date__lte=Now()) class BaseCertificate(models.Model): diff --git a/main/settings.py b/main/settings.py index 432c7bb4ff..c545b81e8f 100644 --- a/main/settings.py +++ b/main/settings.py @@ -37,7 +37,7 @@ from main.sentry import init_sentry from openapi.settings_spectacular import open_spectacular_settings -VERSION = "1.159.1" +VERSION = "1.159.2" log = logging.getLogger() @@ -967,7 +967,7 @@ "sync-courseruns-data": { "task": "courses.tasks.sync_courseruns_data", "schedule": crontab( - minute=0, + minute=10, hour=CRON_COURSERUN_SYNC_HOURS, day_of_week=CRON_COURSERUN_SYNC_DAYS, day_of_month="*", diff --git a/pyproject.toml b/pyproject.toml index 66a3d599d3..adf140f0a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,7 @@ dev = [ "pytest-xdist[psutil]>=3.6.1,<4", "mitol-drf-lint>=2025.3.27", "pytest-urllib3>=0.2.0", + "granian[reload]>=2.5.4,<3", ] [tool.uv] diff --git a/scripts/run-django-dev.sh b/scripts/run-django-dev.sh index 95c8f9f14c..96fdb31ad2 100755 --- a/scripts/run-django-dev.sh +++ b/scripts/run-django-dev.sh @@ -24,4 +24,4 @@ for url in "${health_urls[@]}"; do fi done -exec granian --interface wsgi --host 0.0.0.0 --port "${PORT:-8013}" --workers 2 main.wsgi:application +exec granian --interface wsgi --host 0.0.0.0 --port "${PORT:-8013}" --workers 2 --reload main.wsgi:application diff --git a/uv.lock b/uv.lock index c8506b725a..9f15819769 100644 --- a/uv.lock +++ b/uv.lock @@ -41,6 +41,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/76/783b75a21ce3563b8709050de030ae253853b147bd52e141edc1025aa268/anyascii-0.3.3-py3-none-any.whl", hash = "sha256:f5ab5e53c8781a36b5a40e1296a0eeda2f48c649ef10c3921c1381b1d00dee7a", size = 345090, upload-time = "2025-06-29T03:33:28.356Z" }, ] +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + [[package]] name = "anys" version = "0.3.1" @@ -1325,6 +1338,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/6f/f45fa86c36fcc34f6e613bb223b10fd36c6acd9f7aa43d4f65d0f1eff4cc/granian-2.7.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:efccd6818a1ac4cba7eededf5e2768f56d4a8c7c93bd5e3a8d7a901510976944", size = 3971242, upload-time = "2026-04-23T11:55:53.834Z" }, ] +[package.optional-dependencies] +reload = [ + { name = "watchfiles" }, +] + [[package]] name = "greenlet" version = "3.3.2" @@ -2093,6 +2111,7 @@ dev = [ { name = "faker" }, { name = "flaky" }, { name = "freezegun" }, + { name = "granian", extra = ["reload"] }, { name = "ipdb" }, { name = "mitol-drf-lint" }, { name = "pdbpp" }, @@ -2200,6 +2219,7 @@ dev = [ { name = "faker", specifier = "<=40.15.0,>=40.15.0" }, { name = "flaky", specifier = ">=3.7.0,<4" }, { name = "freezegun", specifier = ">=1.2,<2" }, + { name = "granian", extras = ["reload"], specifier = ">=2.5.4,<3" }, { name = "ipdb", specifier = ">=0.13.13,<0.14" }, { name = "mitol-drf-lint", specifier = ">=2025.3.27" }, { name = "pdbpp", specifier = ">=0.11.6,<0.12" }, @@ -3841,6 +3861,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/5c/0326104a7606c076a99eb59cb0fb200e22fc54b449a71ed719b92e098c07/wagtail_metadata-5.0.0-py2.py3-none-any.whl", hash = "sha256:fcc2bbb71e83b353ceef05d69a4cde3f2fbe645c9e5035e46f7829fc234a6405", size = 12616, upload-time = "2023-09-07T00:12:26.797Z" }, ] +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0"