From 716944fb53678eb1647897963ab4814476ba73d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 19:31:25 +0000 Subject: [PATCH 1/8] Update redis Docker tag to v6.2.14 (#2346) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 294e18937d..56099bcdf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - db-data:/var/lib/postgresql/data redis: - image: redis:6.0.5 + image: redis:6.2.14 ports: - "6379" From aa4afcbb60a87cf02b03e6fc2fe5c25e4b4b5af7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:20:15 -0400 Subject: [PATCH 2/8] Update dependency django-anymail to v11 (#2341) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- poetry.lock | 17 +++++++++-------- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index bfad69f660..939a17491c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -963,23 +963,24 @@ bcrypt = ["bcrypt"] [[package]] name = "django-anymail" -version = "8.6" -description = "Django email backends and webhooks for Amazon SES, Mailgun, Mailjet, Mandrill, Postal, Postmark, SendGrid, SendinBlue, and SparkPost" +version = "11.1" +description = "Django email backends and webhooks for Amazon SES, Brevo, MailerSend, Mailgun, Mailjet, Mandrill, Postal, Postmark, Resend, SendGrid, SparkPost and Unisender Go (EmailBackend, transactional email tracking and inbound email signals)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" files = [ - {file = "django-anymail-8.6.tar.gz", hash = "sha256:783342d49dd07d68778b81dd12a94c86e1d217463a68a85450a0513fabe31345"}, - {file = "django_anymail-8.6-py3-none-any.whl", hash = "sha256:49d83d7c16316ca86a624097496881d59b7d71b16bf1c5211cffa5b19ef98d0c"}, + {file = "django_anymail-11.1-py3-none-any.whl", hash = "sha256:f1a5b2de713f46fbdf311f875a9546511e4aa9bfa1c3f8350d21b43e75579acc"}, + {file = "django_anymail-11.1.tar.gz", hash = "sha256:ec373c0a2d79f5e6a3ecda819c8087af0b0e114e4ec1d8576dea4c786f9b2a59"}, ] [package.dependencies] django = ">=2.0" requests = ">=2.4.3" +urllib3 = ">=1.25.0" [package.extras] -amazon-ses = ["boto3"] -dev = ["flake8", "sphinx", "sphinx-rtd-theme", "tox", "twine", "wheel"] +amazon-ses = ["boto3 (>=1.10.17)"] postal = ["cryptography"] +resend = ["svix"] [[package]] name = "django-compat" @@ -4246,4 +4247,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "3.9.18" -content-hash = "d60649a132bca316a4246da510a9fb918bb2015209070da8ca411564eaaca2f1" +content-hash = "2bbcbb61e331734585677e08ab0bb998c292ef2d7ea51f06e567a6a9f18f0307" diff --git a/pyproject.toml b/pyproject.toml index e6c9ccd5cd..dc8ca8f399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ celery-redbeat = "^2.0.0" deepdiff = "^6.6.1" dj-database-url = "^0.5.0" django = "^3.2.18" -django-anymail = {extras = ["mailgun"], version = "^8.4"} +django-anymail = {extras = ["mailgun"], version = "^11.1"} django-cors-headers = "^3.11.0" django-countries = "^7.2.1" django-filter = "^2.4.0" From e5eb236e07e38549f4cc151b99cdbac4715bdc78 Mon Sep 17 00:00:00 2001 From: Anna Gavrilman Date: Wed, 14 Aug 2024 08:46:19 -0400 Subject: [PATCH 3/8] Add time_commitment and durations to the courses api (#2334) --- courses/serializers/v2/courses.py | 22 ++++++++++++++++++++++ courses/serializers/v2/courses_test.py | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/courses/serializers/v2/courses.py b/courses/serializers/v2/courses.py index 6ea3ddf47c..fc258ce401 100644 --- a/courses/serializers/v2/courses.py +++ b/courses/serializers/v2/courses.py @@ -33,6 +33,8 @@ class CourseSerializer(BaseCourseSerializer): topics = serializers.SerializerMethodField() certificate_type = serializers.SerializerMethodField() required_prerequisites = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() + time_commitment = serializers.SerializerMethodField() def get_required_prerequisites(self, instance): """ @@ -46,6 +48,24 @@ def get_required_prerequisites(self, instance): and instance.page.prerequisites != "" ) + def get_duration(self, instance): + """ + Get the duration of the course from the course page CMS. + """ + if hasattr(instance, "page") and hasattr(instance.page, "length"): + return instance.page.length + + return None + + def get_time_commitment(self, instance): + """ + Get the time commitment of the course from the course page CMS. + """ + if hasattr(instance, "page") and hasattr(instance.page, "effort"): + return instance.page.effort + + return None + def get_next_run_id(self, instance): """Get next run id""" run = instance.first_unexpired_run @@ -88,6 +108,8 @@ class Meta: "topics", "certificate_type", "required_prerequisites", + "duration", + "time_commitment", ] diff --git a/courses/serializers/v2/courses_test.py b/courses/serializers/v2/courses_test.py index d5530e0a38..e57dd89b2b 100644 --- a/courses/serializers/v2/courses_test.py +++ b/courses/serializers/v2/courses_test.py @@ -68,6 +68,8 @@ def test_serialize_course( "certificate_type": certificate_type, "topics": [{"name": topic.name} for topic in topics], "required_prerequisites": True, + "duration": course.page.length, + "time_commitment": course.page.effort, "programs": BaseProgramSerializer(course.programs, many=True).data if all_runs else None, @@ -104,6 +106,8 @@ def test_serialize_course_required_prerequisites( "certificate_type": "Certificate of Completion", "topics": [], "required_prerequisites": expected_required_prerequisites, + "duration": course.page.length, + "time_commitment": course.page.effort, "programs": None, }, ) From e0798c31d234c7f46d828cd92c439506769e04fc Mon Sep 17 00:00:00 2001 From: Anna Gavrilman Date: Wed, 14 Aug 2024 14:42:39 -0400 Subject: [PATCH 4/8] Generate certificates twice a day (#2348) --- main/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/settings.py b/main/settings.py index e2e1d7173b..a9bdced140 100644 --- a/main/settings.py +++ b/main/settings.py @@ -786,7 +786,7 @@ ) CRON_COURSE_CERTIFICATES_HOURS = get_string( name="CRON_COURSE_CERTIFICATES_HOURS", - default=0, + default="0,12", description="'hours' value for the 'generate-course-certificate' scheduled task (defaults to midnight)", ) CRON_COURSE_CERTIFICATES_DAYS = get_string( From 5256acb40e7de549e31a14e8eec50a0938f9f54d Mon Sep 17 00:00:00 2001 From: jenniw Date: Fri, 16 Aug 2024 08:44:04 -0400 Subject: [PATCH 5/8] Add availability to Courses API endpoint (#2308) Co-authored-by: Anna --- courses/serializers/v2/courses.py | 12 ++++++++++++ courses/serializers/v2/courses_test.py | 2 ++ courses/utils.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/courses/serializers/v2/courses.py b/courses/serializers/v2/courses.py index fc258ce401..5816fd9d67 100644 --- a/courses/serializers/v2/courses.py +++ b/courses/serializers/v2/courses.py @@ -16,6 +16,7 @@ ProductRelatedField, ) from courses.serializers.v1.departments import DepartmentSerializer +from courses.utils import get_archived_courseruns from flexiblepricing.api import is_courseware_flexible_price_approved from main import features from openedx.constants import EDX_ENROLLMENT_AUDIT_MODE, EDX_ENROLLMENT_VERIFIED_MODE @@ -32,6 +33,7 @@ class CourseSerializer(BaseCourseSerializer): programs = serializers.SerializerMethodField() topics = serializers.SerializerMethodField() certificate_type = serializers.SerializerMethodField() + availability = serializers.SerializerMethodField() required_prerequisites = serializers.SerializerMethodField() duration = serializers.SerializerMethodField() time_commitment = serializers.SerializerMethodField() @@ -95,6 +97,15 @@ def get_certificate_type(self, instance): return "MicroMasters Credential" return "Certificate of Completion" + def get_availability(self, instance): + """Get course availability""" + archived_course_runs = get_archived_courseruns( + instance.courseruns.filter(is_self_paced=False) + ) + if archived_course_runs.count() == 0: + return "dated" + return "anytime" + class Meta: model = models.Course fields = [ @@ -110,6 +121,7 @@ class Meta: "required_prerequisites", "duration", "time_commitment", + "availability", ] diff --git a/courses/serializers/v2/courses_test.py b/courses/serializers/v2/courses_test.py index e57dd89b2b..5958df3ad3 100644 --- a/courses/serializers/v2/courses_test.py +++ b/courses/serializers/v2/courses_test.py @@ -66,6 +66,7 @@ def test_serialize_course( "departments": [{"name": department}], "page": CoursePageSerializer(course.page).data, "certificate_type": certificate_type, + "availability": "dated", "topics": [{"name": topic.name} for topic in topics], "required_prerequisites": True, "duration": course.page.length, @@ -105,6 +106,7 @@ def test_serialize_course_required_prerequisites( "page": CoursePageSerializer(course.page).data, "certificate_type": "Certificate of Completion", "topics": [], + "availability": "dated", "required_prerequisites": expected_required_prerequisites, "duration": course.page.length, "time_commitment": course.page.effort, diff --git a/courses/utils.py b/courses/utils.py index 8ea1f7d432..3a8d15c8f4 100644 --- a/courses/utils.py +++ b/courses/utils.py @@ -173,3 +173,21 @@ def get_unenrollable_courses(queryset): .filter(courseruns__id__in=courseruns_qs.values_list("id", flat=True)) .distinct() ) + + +def get_archived_courseruns(queryset): + """ + Returns course runs that are archived. This is defined as: + - The course run end date has passed + - The course run enrollment end date is in the future or None. + This logic is set to match the logic found in frontend/public/src/lib/courseApi.js isRunArchived + + Args: + queryset: Queryset of CourseRun objects + """ + now = now_in_utc() + return queryset.filter( + get_enrollable_course_run_filter(now) + & Q(end_date__lt=now) + & (Q(enrollment_end__isnull=True) | Q(enrollment_end__gt=now)) + ) From 2409e031c676c76d2bcdd864fd1e665f4ec1be27 Mon Sep 17 00:00:00 2001 From: Anna Gavrilman Date: Fri, 16 Aug 2024 09:39:01 -0400 Subject: [PATCH 6/8] Add Go To Course button (#2349) --- frontend/public/scss/dashboard.scss | 19 +++-- .../public/src/components/EnrolledItemCard.js | 80 ++++++++++++------- .../src/components/EnrolledItemCard_test.js | 25 ------ frontend/public/src/components/Loader.js | 2 +- frontend/public/src/lib/util.js | 3 + 5 files changed, 65 insertions(+), 64 deletions(-) diff --git a/frontend/public/scss/dashboard.scss b/frontend/public/scss/dashboard.scss index 9e8dffad1f..d670b3add5 100644 --- a/frontend/public/scss/dashboard.scss +++ b/frontend/public/scss/dashboard.scss @@ -74,7 +74,7 @@ $enrolled-passed-fg: #ffffff; div.row { margin: 0 !important; - padding: 20px; + padding: 20px 30px; height: 100%; } @@ -108,6 +108,7 @@ $enrolled-passed-fg: #ffffff; } .course-card-text-details { + padding: 0 0 0 20px; @include media-breakpoint-down(sm) { padding: 15px 0px 15px 0px; @@ -147,8 +148,7 @@ $enrolled-passed-fg: #ffffff; } button.dot-menu { - // Setting height so the button element doesn't expand to cover the full height of the row - height: $material-icon-height; + vertical-align: -webkit-baseline-middle; } .certificate-container { @@ -200,7 +200,7 @@ $enrolled-passed-fg: #ffffff; .upgrade-item-description { width: 100%; flex-direction: row !important; - padding: 25px 30px; + padding: 15px 30px; border-top: $home-page-border-grey; background: $home-page-grey-lite; @@ -209,6 +209,7 @@ $enrolled-passed-fg: #ffffff; } div.certificate-upgrade-message { + padding-left: 30px; width: 80%; font-size: 16px; line-height: 30px; @@ -270,9 +271,13 @@ $enrolled-passed-fg: #ffffff; } } } - - .get-cert-button-container button, .finaid-link-container { - font-size: 14px !important; + .goto-course-wrapper { + a { + width: fit-content; + } + } + .get-cert-button-container button, .finaid-link-container, a.btn-primary { + font-size: 14px; @include media-breakpoint-down(sm) { width: 100%; diff --git a/frontend/public/src/components/EnrolledItemCard.js b/frontend/public/src/components/EnrolledItemCard.js index cbc19c6d69..59b0b6caa9 100644 --- a/frontend/public/src/components/EnrolledItemCard.js +++ b/frontend/public/src/components/EnrolledItemCard.js @@ -1,7 +1,11 @@ /* global SETTINGS:false */ import React from "react" import moment from "moment" -import { parseDateString, formatPrettyDateTimeAmPmTz } from "../lib/util" +import { + parseDateString, + formatPrettyDateTimeAmPmTz, + formatPrettyMonthDate +} from "../lib/util" import { Formik, Form, Field } from "formik" import { Dropdown, @@ -402,21 +406,6 @@ export class EnrolledItemCard extends React.Component< const { menuVisibility } = this.state - const title = isLinkableCourseRun(enrollment.run, currentUser) ? ( - - redirectToCourseHomepage(enrollment.run.courseware_url, ev) - } - target="_blank" - rel="noopener noreferrer" - > - {enrollment.run.course.title} - - ) : ( - enrollment.run.course.title - ) - const financialAssistanceLink = isFinancialAssistanceAvailable(enrollment.run) && !enrollment.approved_flexible_price_exists ? ( @@ -430,30 +419,31 @@ export class EnrolledItemCard extends React.Component< const certificateLinksStyles = isProgramCard ? "upgrade-item-description d-md-flex align-items-start justify-content-between flex-column" : - "upgrade-item-description d-md-flex align-items-start justify-content-between" + "upgrade-item-description d-md-flex" const certificateLinksIntStyles = isProgramCard ? "d-flex d-md-flex flex-column align-items-start justify-content-center" : - "d-flex d-md-flex flex-column align-items-start justify-content-center" + "d-flex d-md-flex flex-column justify-content-center" const certificateLinks = enrollment.run.products.length > 0 && enrollment.enrollment_mode === "audit" && enrollment.run.is_upgradable ? (
-
-

- Upgrade today and, upon passing, receive your - certificate signed by MIT faculty to highlight the knowledge and - skills you've gained from this MITx course. -

-
-
- {financialAssistanceLink} -
+
+
+ Upgrade today and, upon passing, receive your + certificate signed by MIT faculty to highlight the knowledge and + skills you've gained from this MITx course.{" "} + + Upgrade expires:{" "} + {formatPrettyDateTimeAmPmTz( + parseDateString(enrollment.run.upgrade_deadline) + )} +
) : null @@ -492,8 +482,8 @@ export class EnrolledItemCard extends React.Component< )}
-
-
+
+
{enrollment.certificate ? ( @@ -509,7 +499,35 @@ export class EnrolledItemCard extends React.Component< ) : null}
-

{title}

+

{enrollment.run.course.title}

+
+ { let helper, @@ -23,7 +22,6 @@ describe("EnrolledItemCard", () => { userEnrollment, currentUser, enrollmentCardProps, - isFinancialAssistanceAvailableStub, toggleProgramDrawer, redirectToCourseHomepage @@ -48,10 +46,6 @@ describe("EnrolledItemCard", () => { .returns(Promise), addUserNotification: helper.sandbox.stub().returns(Function) } - isFinancialAssistanceAvailableStub = helper.sandbox.stub( - courseApi, - "isFinancialAssistanceAvailable" - ) toggleProgramDrawer = helper.sandbox.stub().returns(Function) redirectToCourseHomepage = helper.sandbox.stub().returns(Function) @@ -251,25 +245,6 @@ describe("EnrolledItemCard", () => { assert.isTrue(verifiedUnenrollmodal.exists()) }) }) - ;[[true], [false]].forEach(([approvedFlexiblePrice]) => { - it("renders the financial assistance link", async () => { - isFinancialAssistanceAvailableStub.returns(true) - userEnrollment = makeCourseRunEnrollmentWithProduct() - userEnrollment["enrollment_mode"] = "audit" - userEnrollment["approved_flexible_price_exists"] = approvedFlexiblePrice - enrollmentCardProps.enrollment = userEnrollment - const inner = await renderedCard() - const extraLinks = inner.find(".finaid-link") - if (approvedFlexiblePrice) { - const text = extraLinks.find("a").at(0) - assert.isFalse(text.exists()) - } else { - const text = extraLinks.find("a").at(0).text() - assert.equal(text, "Financial assistance?") - } - }) - }) - it("renders the program unenrollment verification modal", async () => { enrollmentCardProps.enrollment = makeProgramEnrollment() diff --git a/frontend/public/src/components/Loader.js b/frontend/public/src/components/Loader.js index ffc690487f..0b965ebdd3 100644 --- a/frontend/public/src/components/Loader.js +++ b/frontend/public/src/components/Loader.js @@ -24,7 +24,7 @@ const Loader = (props: LoaderProps) => { notNil(delayMs) ? delayMs : defaultLoaderDelayMs ) return () => clearTimeout(timer) - }, []) + }, [isLoading, delayMs]) if (!isLoading) { return children diff --git a/frontend/public/src/lib/util.js b/frontend/public/src/lib/util.js index 334c5f3d31..f9ccd96a4e 100644 --- a/frontend/public/src/lib/util.js +++ b/frontend/public/src/lib/util.js @@ -152,6 +152,9 @@ export const formatPrettyDate = (momentDate: Moment) => export const formatPrettyShortDate = (momentDate: Moment) => momentDate.format("MMM D, YYYY") +export const formatPrettyMonthDate = (momentDate: Moment) => + momentDate.format("MMMM D") + export const formatPrettyDateUtc = (momentDate: Moment) => momentDate.tz("UTC").format("MMMM D, YYYY") From fbb3cedae2dd16387bcb763f3d6a9d01fac7cb89 Mon Sep 17 00:00:00 2001 From: Anna Gavrilman Date: Tue, 20 Aug 2024 09:53:25 -0400 Subject: [PATCH 7/8] Revert "Update nginx Docker tag to v1.27" (#2352) --- nginx/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 51e3fd8756..2c67e9d747 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -2,7 +2,7 @@ # it's primary purpose is to emulate heroku-buildpack-nginx's # functionality that compiles config/nginx.conf.erb # See https://github.com/heroku/heroku-buildpack-nginx/blob/fefac6c569f28182b3459cb8e34b8ccafc403fde/bin/start-nginx -FROM nginx:1.27 +FROM nginx:1.21 # Logs are configured to a relatic path under /etc/nginx # but the container expects /var/log From 571042a871166032a1f990d450acd1f0d1ee60e4 Mon Sep 17 00:00:00 2001 From: Doof Date: Tue, 20 Aug 2024 13:53:56 +0000 Subject: [PATCH 8/8] Release 0.99.0 --- RELEASE.rst | 11 +++++++++++ main/settings.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index 75fbe66d7e..47ed6fa867 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,17 @@ Release Notes ============= +Version 0.99.0 +-------------- + +- Revert "Update nginx Docker tag to v1.27" (#2352) +- Add Go To Course button (#2349) +- Add availability to Courses API endpoint (#2308) +- Generate certificates twice a day (#2348) +- Add time_commitment and durations to the courses api (#2334) +- Update dependency django-anymail to v11 (#2341) +- Update redis Docker tag to v6.2.14 (#2346) + Version 0.98.14 (Released August 15, 2024) --------------- diff --git a/main/settings.py b/main/settings.py index 5aab2933e4..77df7dc1ab 100644 --- a/main/settings.py +++ b/main/settings.py @@ -29,7 +29,7 @@ from main.celery_utils import OffsettingSchedule from main.sentry import init_sentry -VERSION = "0.98.14" +VERSION = "0.99.0" log = logging.getLogger()