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/courses/serializers/v2/courses.py b/courses/serializers/v2/courses.py index 6ea3ddf47c..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,7 +33,10 @@ 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() def get_required_prerequisites(self, instance): """ @@ -46,6 +50,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 @@ -75,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 = [ @@ -88,6 +119,9 @@ class Meta: "topics", "certificate_type", "required_prerequisites", + "duration", + "time_commitment", + "availability", ] diff --git a/courses/serializers/v2/courses_test.py b/courses/serializers/v2/courses_test.py index d5530e0a38..5958df3ad3 100644 --- a/courses/serializers/v2/courses_test.py +++ b/courses/serializers/v2/courses_test.py @@ -66,8 +66,11 @@ 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, + "time_commitment": course.page.effort, "programs": BaseProgramSerializer(course.programs, many=True).data if all_runs else None, @@ -103,7 +106,10 @@ 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, "programs": None, }, ) 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)) + ) 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" 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. -
-