From b068757e41f07d78e275eb82b15287ee18dd2316 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Mon, 27 Nov 2023 19:54:32 -0500 Subject: [PATCH 01/17] feat: modifying search bar for /courses page -Adding new features "Filter" and "Sort by" to the search bar and their respective toggle -Modifying the style of the search bar and the discovery message --- lms/djangoapps/courseware/views/views.py | 13 +- lms/envs/common.py | 8 +- lms/envs/devstack.py | 7 +- lms/templates/courseware/courses.html | 334 +++++++++++++++++++++-- 4 files changed, 332 insertions(+), 30 deletions(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 79d2976d2c97..3ada020aea28 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -276,14 +276,13 @@ def courses(request): """ courses_list = [] course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {}) - if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'): - courses_list = get_courses(request.user) + courses_list = get_courses(request.user) - if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE", - settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): - courses_list = sort_by_start_date(courses_list) - else: - courses_list = sort_by_announcement(courses_list) + if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE", + settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): + courses_list = sort_by_start_date(courses_list) + else: + courses_list = sort_by_announcement(courses_list) # Add marketable programs to the context. programs_list = get_programs_with_type(request.site, include_hidden=False) diff --git a/lms/envs/common.py b/lms/envs/common.py index c0182d47355a..91f2b8386fd6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -657,7 +657,7 @@ # .. toggle_target_removal_date: None # .. toggle_warning: The COURSE_DISCOVERY_MEANINGS setting should be properly defined. # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/7845 - 'ENABLE_COURSE_DISCOVERY': False, + 'ENABLE_COURSE_DISCOVERY': True, # .. toggle_name: FEATURES['ENABLE_COURSE_FILENAME_CCX_SUFFIX'] # .. toggle_implementation: DjangoSetting @@ -679,6 +679,12 @@ # Teams feature 'ENABLE_TEAMS': True, + # Filter feature of the filter bar + 'ENABLE_FILTER': False, + + # Sort by feature of the filter bar + 'ENABLE_SORTBY': False, + # Show video bumper in LMS 'ENABLE_VIDEO_BUMPER': False, diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index c2454ee71fc2..13f5432c092b 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -198,12 +198,17 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing 'language': LANGUAGE_MAP, } -FEATURES['ENABLE_COURSE_DISCOVERY'] = False +FEATURES['ENABLE_COURSE_DISCOVERY'] = True # Setting for overriding default filtering facets for Course discovery # COURSE_DISCOVERY_FILTERS = ["org", "language", "modes"] FEATURES['COURSES_ARE_BROWSEABLE'] = True HOMEPAGE_COURSE_MAX = 9 +FEATURES['ENABLE_FILTER'] = True +# Toggle for the "filter" feature of the search bar +FEATURES['ENABLE_SORTBY'] = False +# Toggle for the "sort by" feature of the search bar + # Software secure fake page feature flag FEATURES['ENABLE_SOFTWARE_SECURE_FAKE'] = True diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index db303073676d..0355143715c9 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -7,6 +7,8 @@ <%inherit file="../main.html" /> <% course_discovery_enabled = settings.FEATURES.get('ENABLE_COURSE_DISCOVERY') + filter_enabled = settings.FEATURES.get('ENABLE_FILTER') + sortby_enabled = settings.FEATURES.get('ENABLE_SORTBY') %> <%namespace name='static' file='../static_content.html'/> @@ -29,34 +31,289 @@ % endif + <%block name="pagetitle">${_("Courses")} +
% if course_discovery_enabled: +
+ % endif -
+ + + + + + +
    %for course in courses:
  • @@ -66,15 +323,50 @@
- - % if course_discovery_enabled: - - % endif - + +
-
+ \ No newline at end of file From 21f0c87def7f2d223671a620d986729e91909f67 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Mon, 27 Nov 2023 22:32:44 -0500 Subject: [PATCH 02/17] fix: fix indentation --- lms/templates/courseware/courses.html | 483 +++++++++++++------------- 1 file changed, 248 insertions(+), 235 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 0355143715c9..d57182826570 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -34,339 +34,352 @@ <%block name="pagetitle">${_("Courses")} -
-
-
- % if course_discovery_enabled: -
+
+
+ % if course_discovery_enabled: +
-
- - % endif - - - + % endif + + + + - - -
-
    - %for course in courses: -
  • - <%include file="../course.html" args="course=course" /> -
  • - %endfor -
-
- - -
+ }); + +
+
\ No newline at end of file From f747dbf6f1e834ed1f514148d9f5e9f040b34cb7 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 29 Nov 2023 16:05:59 -0500 Subject: [PATCH 03/17] fix: PEP8 style --- lms/djangoapps/courseware/views/views.py | 73 +++++++++++++++--------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 3ada020aea28..a12a5f6b2ec9 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -2,7 +2,6 @@ Courseware views functions """ - import json import logging import urllib @@ -142,7 +141,6 @@ log = logging.getLogger("edx.courseware") - # Only display the requirements on learner dashboard for # credit and verified modes. REQUIREMENTS_DISPLAY_MODES = CourseMode.CREDIT_MODES + [CourseMode.VERIFIED] @@ -279,7 +277,7 @@ def courses(request): courses_list = get_courses(request.user) if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE", - settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): + settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): courses_list = sort_by_start_date(courses_list) else: courses_list = sort_by_announcement(courses_list) @@ -441,6 +439,7 @@ class StaticCourseTabView(EdxFragmentView): """ View that displays a static course tab with a given name. """ + @method_decorator(ensure_csrf_cookie) @method_decorator(ensure_valid_course_key) def get(self, request, course_id, tab_slug, **kwargs): # lint-amnesty, pylint: disable=arguments-differ @@ -461,13 +460,15 @@ def get(self, request, course_id, tab_slug, **kwargs): # lint-amnesty, pylint: return super().get(request, course=course, tab=tab, **kwargs) - def render_to_fragment(self, request, course=None, tab=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_to_fragment(self, request, course=None, tab=None, + **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders the static tab to a fragment. """ return get_static_tab_fragment(request, course, tab) - def render_standalone_response(self, request, fragment, course=None, tab=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_standalone_response(self, request, fragment, course=None, tab=None, + **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders this static tab's fragment to HTML for a standalone page. """ @@ -484,6 +485,7 @@ class CourseTabView(EdxFragmentView): """ View that displays a course tab page. """ + @method_decorator(ensure_csrf_cookie) @method_decorator(ensure_valid_course_key) @method_decorator(data_sharing_consent_required) @@ -588,7 +590,9 @@ def handle_exceptions(request, course_key, course, exception): """ Handle exceptions raised when rendering a view. """ - if isinstance(exception, Redirect) or isinstance(exception, Http404): # lint-amnesty, pylint: disable=consider-merging-isinstance + if isinstance(exception, Redirect) or isinstance(exception, + Http404): # lint-amnesty, pylint: + # disable=consider-merging-isinstance raise # lint-amnesty, pylint: disable=misplaced-bare-raise if settings.DEBUG: raise # lint-amnesty, pylint: disable=misplaced-bare-raise @@ -654,14 +658,16 @@ def create_page_context(self, request, course=None, tab=None, **kwargs): ) return context - def render_to_fragment(self, request, course=None, page_context=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_to_fragment(self, request, course=None, page_context=None, + **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders the course tab to a fragment. """ tab = page_context['tab'] return tab.render_to_fragment(request, course, **kwargs) - def render_standalone_response(self, request, fragment, course=None, tab=None, page_context=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_standalone_response(self, request, fragment, course=None, tab=None, page_context=None, + **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders this course tab's fragment to HTML for a standalone page. """ @@ -1031,7 +1037,9 @@ def _progress(request, course_key, student_id): return response -def _downloadable_certificate_message(course, cert_downloadable_status): # lint-amnesty, pylint: disable=missing-function-docstring +def _downloadable_certificate_message(course, + cert_downloadable_status): # lint-amnesty, pylint: + # disable=missing-function-docstring if certs_api.has_html_certificates_enabled(course): if certs_api.get_active_web_certificate(course) is not None: return _downloadable_cert_data( @@ -1169,7 +1177,7 @@ def _course_home_redirect_enabled(): Returns: boolean True or False """ if configuration_helpers.get_value( - 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False) + 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False) ) and configuration_helpers.get_value( 'ENABLE_COURSE_HOME_REDIRECT', settings.FEATURES.get('ENABLE_COURSE_HOME_REDIRECT', True) ): @@ -1338,7 +1346,9 @@ def get_course_lti_endpoints(request, course_id): for block in lti_noauth_blocks ] - return HttpResponse(json.dumps(endpoints), content_type='application/json') # lint-amnesty, pylint: disable=http-response-with-content-type-json, http-response-with-json-dumps + return HttpResponse(json.dumps(endpoints), + content_type='application/json') # lint-amnesty, pylint: + # disable=http-response-with-content-type-json, http-response-with-json-dumps @login_required @@ -1536,7 +1546,8 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): set_custom_attribute('block_type', usage_key.block_type) requested_view = request.GET.get('view', 'student_view') - if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: disable=consider-using-in + if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: + # disable=consider-using-in return HttpResponseBadRequest( f"Rendering of the xblock view '{bleach.clean(requested_view, strip=True)}' is not supported." ) @@ -1697,6 +1708,7 @@ class XBlockContentInspector: this class has the job of detecting certain patterns in XBlock content that would imply these dependencies, so we know when to include them or not. """ + def __init__(self, block, fragment): self.block = block self.fragment = fragment @@ -1711,14 +1723,14 @@ def has_mathjax_content(self): """ # The following pairs are used to mark Mathjax syntax in XBlocks. There # are other options for the wiki, but we don't worry about those here. - MATHJAX_TAG_PAIRS = [ + mathjax_tag_pairs = [ (r"\(", r"\)"), (r"\[", r"\]"), ("[mathjaxinline]", "[/mathjaxinline]"), ("[mathjax]", "[/mathjax]"), ] content = self.fragment.body_html() - for (start_tag, end_tag) in MATHJAX_TAG_PAIRS: + for (start_tag, end_tag) in mathjax_tag_pairs: if start_tag in content and end_tag in content: return True @@ -1929,6 +1941,7 @@ def _replace_url_query(self, parsed_url, query): @method_decorator(transaction.non_atomic_requests, name='dispatch') class PublicVideoXBlockEmbedView(BasePublicVideoXBlockView): """ View for viewing public videos embedded within Twitter or other social media """ + def get_template_and_context(self, course, video_block): """ Render the embed view """ fragment = video_block.render('public_view', context={ @@ -1945,7 +1958,8 @@ def get_template_and_context(self, course, video_block): # string identifying the name of this installation, such as "edX". FINANCIAL_ASSISTANCE_HEADER = _( '{platform_name} now offers financial assistance for learners who want to earn Verified Certificates but' - ' who may not be able to pay the Verified Certificate fee. Eligible learners may receive up to 90{percent_sign} off' # lint-amnesty, pylint: disable=line-too-long + ' who may not be able to pay the Verified Certificate fee. Eligible learners may receive up to 90{percent_sign} off' + # lint-amnesty, pylint: disable=line-too-long ' the Verified Certificate fee for a course.\nTo apply for financial assistance, enroll in the' ' audit track for a course that offers Verified Certificates, and then complete this application.' ' Note that you must complete a separate application for each course you take.\n We plan to use this' @@ -1955,16 +1969,22 @@ def get_template_and_context(self, course, video_block): def _get_fa_header(header): - return header.\ + return header. \ format(percent_sign="%", platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)).split('\n') FA_INCOME_LABEL = gettext_noop('Annual Household Income') -FA_REASON_FOR_APPLYING_LABEL = gettext_noop('Tell us about your current financial situation. Why do you need assistance?') # lint-amnesty, pylint: disable=line-too-long -FA_GOALS_LABEL = gettext_noop('Tell us about your learning or professional goals. How will a Verified Certificate in this course help you achieve these goals?') # lint-amnesty, pylint: disable=line-too-long +FA_REASON_FOR_APPLYING_LABEL = gettext_noop( + 'Tell us about your current financial situation. Why do you need assistance?') # lint-amnesty, pylint: +# disable=line-too-long +FA_GOALS_LABEL = gettext_noop( + 'Tell us about your learning or professional goals. How will a Verified Certificate in this course help you ' + 'achieve these goals?') # lint-amnesty, pylint: disable=line-too-long -FA_EFFORT_LABEL = gettext_noop('Tell us about your plans for this course. What steps will you take to help you complete the course work and receive a certificate?') # lint-amnesty, pylint: disable=line-too-long +FA_EFFORT_LABEL = gettext_noop( + 'Tell us about your plans for this course. What steps will you take to help you complete the course work and ' + 'receive a certificate?') # lint-amnesty, pylint: disable=line-too-long FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 1250 and 2500 characters or so in your response.') @@ -2110,7 +2130,8 @@ def financial_assistance_form(request, course_id=None): '$85,000 - $100,000', 'More than $100,000'] annual_incomes = [ - {'name': _(income), 'value': income} for income in incomes # lint-amnesty, pylint: disable=translation-of-non-string + {'name': _(income), 'value': income} for income in incomes + # lint-amnesty, pylint: disable=translation-of-non-string ] if course_id and _use_new_financial_assistance_flow(course_id): submit_url = 'submit_financial_assistance_request_v2' @@ -2219,12 +2240,12 @@ def get_financial_aid_courses(user, course_id=None): for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created'): if enrollment.mode != CourseMode.VERIFIED and \ - enrollment.course_overview and \ - enrollment.course_overview.eligible_for_financial_aid and \ - CourseMode.objects.filter( - Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC)), - course_id=enrollment.course_id, - mode_slug=CourseMode.VERIFIED).exists(): + enrollment.course_overview and \ + enrollment.course_overview.eligible_for_financial_aid and \ + CourseMode.objects.filter( + Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC)), + course_id=enrollment.course_id, + mode_slug=CourseMode.VERIFIED).exists(): # This is a workaround to set course_id before disabling the field in case of new financial assistance flow. if str(enrollment.course_overview) == course_id: financial_aid_courses = [{ From 8c14328fc11cecdf67e0fba3753d647887a5c0e2 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 29 Nov 2023 17:35:49 -0500 Subject: [PATCH 04/17] fix: error --- lms/djangoapps/courseware/views/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index a12a5f6b2ec9..7b019d955982 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -2130,7 +2130,7 @@ def financial_assistance_form(request, course_id=None): '$85,000 - $100,000', 'More than $100,000'] annual_incomes = [ - {'name': _(income), 'value': income} for income in incomes + {"name": _(income), 'value': income} for income in incomes # lint-amnesty, pylint: disable=translation-of-non-string ] if course_id and _use_new_financial_assistance_flow(course_id): From 876e331531e157f465fc25a80ba3ebc70cc3857d Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 29 Nov 2023 18:31:26 -0500 Subject: [PATCH 05/17] fix: style --- lms/djangoapps/courseware/views/views.py | 44 ++++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 7b019d955982..af7a46a15132 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -460,15 +460,15 @@ def get(self, request, course_id, tab_slug, **kwargs): # lint-amnesty, pylint: return super().get(request, course=course, tab=tab, **kwargs) - def render_to_fragment(self, request, course=None, tab=None, - **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_to_fragment(self, request, course=None, tab=None, **kwargs): + # lint-amnesty, pylint: disable=arguments-differ """ Renders the static tab to a fragment. """ return get_static_tab_fragment(request, course, tab) - def render_standalone_response(self, request, fragment, course=None, tab=None, - **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_standalone_response(self, request, fragment, course=None, tab=None, **kwargs): + # lint-amnesty, pylint: disable=arguments-differ """ Renders this static tab's fragment to HTML for a standalone page. """ @@ -590,8 +590,8 @@ def handle_exceptions(request, course_key, course, exception): """ Handle exceptions raised when rendering a view. """ - if isinstance(exception, Redirect) or isinstance(exception, - Http404): # lint-amnesty, pylint: + if isinstance(exception, Redirect) or isinstance(exception, Http404): + # lint-amnesty, pylint: # disable=consider-merging-isinstance raise # lint-amnesty, pylint: disable=misplaced-bare-raise if settings.DEBUG: @@ -658,16 +658,16 @@ def create_page_context(self, request, course=None, tab=None, **kwargs): ) return context - def render_to_fragment(self, request, course=None, page_context=None, - **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_to_fragment(self, request, course=None, page_context=None, **kwargs): + # lint-amnesty, pylint: disable=arguments-differ """ Renders the course tab to a fragment. """ tab = page_context['tab'] return tab.render_to_fragment(request, course, **kwargs) - def render_standalone_response(self, request, fragment, course=None, tab=None, page_context=None, - **kwargs): # lint-amnesty, pylint: disable=arguments-differ + def render_standalone_response(self, request, fragment, course=None, tab=None, page_context=None, **kwargs): + # lint-amnesty, pylint: disable=arguments-differ """ Renders this course tab's fragment to HTML for a standalone page. """ @@ -1037,8 +1037,14 @@ def _progress(request, course_key, student_id): return response -def _downloadable_certificate_message(course, - cert_downloadable_status): # lint-amnesty, pylint: +def _downloadable_certificate_message(course, cert_downloadable_status): + # lint-amnesty, pylint: + """ + + @param course: + @param cert_downloadable_status: + @return: + """ # disable=missing-function-docstring if certs_api.has_html_certificates_enabled(course): if certs_api.get_active_web_certificate(course) is not None: @@ -1346,8 +1352,8 @@ def get_course_lti_endpoints(request, course_id): for block in lti_noauth_blocks ] - return HttpResponse(json.dumps(endpoints), - content_type='application/json') # lint-amnesty, pylint: + return HttpResponse(json.dumps(endpoints), content_type='application/json') + # lint-amnesty, pylint: # disable=http-response-with-content-type-json, http-response-with-json-dumps @@ -1975,16 +1981,18 @@ def _get_fa_header(header): FA_INCOME_LABEL = gettext_noop('Annual Household Income') -FA_REASON_FOR_APPLYING_LABEL = gettext_noop( - 'Tell us about your current financial situation. Why do you need assistance?') # lint-amnesty, pylint: +FA_REASON_FOR_APPLYING_LABEL = gettext_noop('Tell us about your current financial situation. Why do you need assistance?') +# lint-amnesty, pylint: # disable=line-too-long FA_GOALS_LABEL = gettext_noop( 'Tell us about your learning or professional goals. How will a Verified Certificate in this course help you ' - 'achieve these goals?') # lint-amnesty, pylint: disable=line-too-long + 'achieve these goals?') +# lint-amnesty, pylint: disable=line-too-long FA_EFFORT_LABEL = gettext_noop( 'Tell us about your plans for this course. What steps will you take to help you complete the course work and ' - 'receive a certificate?') # lint-amnesty, pylint: disable=line-too-long + 'receive a certificate?') +# lint-amnesty, pylint: disable=line-too-long FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 1250 and 2500 characters or so in your response.') From 04e6ad7da28fa650c1b67d393dbbf768ca2a891c Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 29 Nov 2023 20:07:45 -0500 Subject: [PATCH 06/17] fix: revert file --- lms/djangoapps/courseware/views/views.py | 94 +++++++++--------------- 1 file changed, 33 insertions(+), 61 deletions(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index af7a46a15132..79d2976d2c97 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -2,6 +2,7 @@ Courseware views functions """ + import json import logging import urllib @@ -141,6 +142,7 @@ log = logging.getLogger("edx.courseware") + # Only display the requirements on learner dashboard for # credit and verified modes. REQUIREMENTS_DISPLAY_MODES = CourseMode.CREDIT_MODES + [CourseMode.VERIFIED] @@ -274,13 +276,14 @@ def courses(request): """ courses_list = [] course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {}) - courses_list = get_courses(request.user) + if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'): + courses_list = get_courses(request.user) - if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE", - settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): - courses_list = sort_by_start_date(courses_list) - else: - courses_list = sort_by_announcement(courses_list) + if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE", + settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): + courses_list = sort_by_start_date(courses_list) + else: + courses_list = sort_by_announcement(courses_list) # Add marketable programs to the context. programs_list = get_programs_with_type(request.site, include_hidden=False) @@ -439,7 +442,6 @@ class StaticCourseTabView(EdxFragmentView): """ View that displays a static course tab with a given name. """ - @method_decorator(ensure_csrf_cookie) @method_decorator(ensure_valid_course_key) def get(self, request, course_id, tab_slug, **kwargs): # lint-amnesty, pylint: disable=arguments-differ @@ -460,15 +462,13 @@ def get(self, request, course_id, tab_slug, **kwargs): # lint-amnesty, pylint: return super().get(request, course=course, tab=tab, **kwargs) - def render_to_fragment(self, request, course=None, tab=None, **kwargs): - # lint-amnesty, pylint: disable=arguments-differ + def render_to_fragment(self, request, course=None, tab=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders the static tab to a fragment. """ return get_static_tab_fragment(request, course, tab) - def render_standalone_response(self, request, fragment, course=None, tab=None, **kwargs): - # lint-amnesty, pylint: disable=arguments-differ + def render_standalone_response(self, request, fragment, course=None, tab=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders this static tab's fragment to HTML for a standalone page. """ @@ -485,7 +485,6 @@ class CourseTabView(EdxFragmentView): """ View that displays a course tab page. """ - @method_decorator(ensure_csrf_cookie) @method_decorator(ensure_valid_course_key) @method_decorator(data_sharing_consent_required) @@ -590,9 +589,7 @@ def handle_exceptions(request, course_key, course, exception): """ Handle exceptions raised when rendering a view. """ - if isinstance(exception, Redirect) or isinstance(exception, Http404): - # lint-amnesty, pylint: - # disable=consider-merging-isinstance + if isinstance(exception, Redirect) or isinstance(exception, Http404): # lint-amnesty, pylint: disable=consider-merging-isinstance raise # lint-amnesty, pylint: disable=misplaced-bare-raise if settings.DEBUG: raise # lint-amnesty, pylint: disable=misplaced-bare-raise @@ -658,16 +655,14 @@ def create_page_context(self, request, course=None, tab=None, **kwargs): ) return context - def render_to_fragment(self, request, course=None, page_context=None, **kwargs): - # lint-amnesty, pylint: disable=arguments-differ + def render_to_fragment(self, request, course=None, page_context=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders the course tab to a fragment. """ tab = page_context['tab'] return tab.render_to_fragment(request, course, **kwargs) - def render_standalone_response(self, request, fragment, course=None, tab=None, page_context=None, **kwargs): - # lint-amnesty, pylint: disable=arguments-differ + def render_standalone_response(self, request, fragment, course=None, tab=None, page_context=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ """ Renders this course tab's fragment to HTML for a standalone page. """ @@ -1037,15 +1032,7 @@ def _progress(request, course_key, student_id): return response -def _downloadable_certificate_message(course, cert_downloadable_status): - # lint-amnesty, pylint: - """ - - @param course: - @param cert_downloadable_status: - @return: - """ - # disable=missing-function-docstring +def _downloadable_certificate_message(course, cert_downloadable_status): # lint-amnesty, pylint: disable=missing-function-docstring if certs_api.has_html_certificates_enabled(course): if certs_api.get_active_web_certificate(course) is not None: return _downloadable_cert_data( @@ -1183,7 +1170,7 @@ def _course_home_redirect_enabled(): Returns: boolean True or False """ if configuration_helpers.get_value( - 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False) + 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False) ) and configuration_helpers.get_value( 'ENABLE_COURSE_HOME_REDIRECT', settings.FEATURES.get('ENABLE_COURSE_HOME_REDIRECT', True) ): @@ -1352,9 +1339,7 @@ def get_course_lti_endpoints(request, course_id): for block in lti_noauth_blocks ] - return HttpResponse(json.dumps(endpoints), content_type='application/json') - # lint-amnesty, pylint: - # disable=http-response-with-content-type-json, http-response-with-json-dumps + return HttpResponse(json.dumps(endpoints), content_type='application/json') # lint-amnesty, pylint: disable=http-response-with-content-type-json, http-response-with-json-dumps @login_required @@ -1552,8 +1537,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): set_custom_attribute('block_type', usage_key.block_type) requested_view = request.GET.get('view', 'student_view') - if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: - # disable=consider-using-in + if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: disable=consider-using-in return HttpResponseBadRequest( f"Rendering of the xblock view '{bleach.clean(requested_view, strip=True)}' is not supported." ) @@ -1714,7 +1698,6 @@ class XBlockContentInspector: this class has the job of detecting certain patterns in XBlock content that would imply these dependencies, so we know when to include them or not. """ - def __init__(self, block, fragment): self.block = block self.fragment = fragment @@ -1729,14 +1712,14 @@ def has_mathjax_content(self): """ # The following pairs are used to mark Mathjax syntax in XBlocks. There # are other options for the wiki, but we don't worry about those here. - mathjax_tag_pairs = [ + MATHJAX_TAG_PAIRS = [ (r"\(", r"\)"), (r"\[", r"\]"), ("[mathjaxinline]", "[/mathjaxinline]"), ("[mathjax]", "[/mathjax]"), ] content = self.fragment.body_html() - for (start_tag, end_tag) in mathjax_tag_pairs: + for (start_tag, end_tag) in MATHJAX_TAG_PAIRS: if start_tag in content and end_tag in content: return True @@ -1947,7 +1930,6 @@ def _replace_url_query(self, parsed_url, query): @method_decorator(transaction.non_atomic_requests, name='dispatch') class PublicVideoXBlockEmbedView(BasePublicVideoXBlockView): """ View for viewing public videos embedded within Twitter or other social media """ - def get_template_and_context(self, course, video_block): """ Render the embed view """ fragment = video_block.render('public_view', context={ @@ -1964,8 +1946,7 @@ def get_template_and_context(self, course, video_block): # string identifying the name of this installation, such as "edX". FINANCIAL_ASSISTANCE_HEADER = _( '{platform_name} now offers financial assistance for learners who want to earn Verified Certificates but' - ' who may not be able to pay the Verified Certificate fee. Eligible learners may receive up to 90{percent_sign} off' - # lint-amnesty, pylint: disable=line-too-long + ' who may not be able to pay the Verified Certificate fee. Eligible learners may receive up to 90{percent_sign} off' # lint-amnesty, pylint: disable=line-too-long ' the Verified Certificate fee for a course.\nTo apply for financial assistance, enroll in the' ' audit track for a course that offers Verified Certificates, and then complete this application.' ' Note that you must complete a separate application for each course you take.\n We plan to use this' @@ -1975,24 +1956,16 @@ def get_template_and_context(self, course, video_block): def _get_fa_header(header): - return header. \ + return header.\ format(percent_sign="%", platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)).split('\n') FA_INCOME_LABEL = gettext_noop('Annual Household Income') -FA_REASON_FOR_APPLYING_LABEL = gettext_noop('Tell us about your current financial situation. Why do you need assistance?') -# lint-amnesty, pylint: -# disable=line-too-long -FA_GOALS_LABEL = gettext_noop( - 'Tell us about your learning or professional goals. How will a Verified Certificate in this course help you ' - 'achieve these goals?') -# lint-amnesty, pylint: disable=line-too-long - -FA_EFFORT_LABEL = gettext_noop( - 'Tell us about your plans for this course. What steps will you take to help you complete the course work and ' - 'receive a certificate?') -# lint-amnesty, pylint: disable=line-too-long +FA_REASON_FOR_APPLYING_LABEL = gettext_noop('Tell us about your current financial situation. Why do you need assistance?') # lint-amnesty, pylint: disable=line-too-long +FA_GOALS_LABEL = gettext_noop('Tell us about your learning or professional goals. How will a Verified Certificate in this course help you achieve these goals?') # lint-amnesty, pylint: disable=line-too-long + +FA_EFFORT_LABEL = gettext_noop('Tell us about your plans for this course. What steps will you take to help you complete the course work and receive a certificate?') # lint-amnesty, pylint: disable=line-too-long FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 1250 and 2500 characters or so in your response.') @@ -2138,8 +2111,7 @@ def financial_assistance_form(request, course_id=None): '$85,000 - $100,000', 'More than $100,000'] annual_incomes = [ - {"name": _(income), 'value': income} for income in incomes - # lint-amnesty, pylint: disable=translation-of-non-string + {'name': _(income), 'value': income} for income in incomes # lint-amnesty, pylint: disable=translation-of-non-string ] if course_id and _use_new_financial_assistance_flow(course_id): submit_url = 'submit_financial_assistance_request_v2' @@ -2248,12 +2220,12 @@ def get_financial_aid_courses(user, course_id=None): for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created'): if enrollment.mode != CourseMode.VERIFIED and \ - enrollment.course_overview and \ - enrollment.course_overview.eligible_for_financial_aid and \ - CourseMode.objects.filter( - Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC)), - course_id=enrollment.course_id, - mode_slug=CourseMode.VERIFIED).exists(): + enrollment.course_overview and \ + enrollment.course_overview.eligible_for_financial_aid and \ + CourseMode.objects.filter( + Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC)), + course_id=enrollment.course_id, + mode_slug=CourseMode.VERIFIED).exists(): # This is a workaround to set course_id before disabling the field in case of new financial assistance flow. if str(enrollment.course_overview) == course_id: financial_aid_courses = [{ From 20119b33aa4ae33295b9c10bfe28e675a0e62f07 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 29 Nov 2023 21:04:37 -0500 Subject: [PATCH 07/17] fix: revert feat and style --- lms/templates/courseware/courses.html | 39 ++++++--------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index d57182826570..e922c7ffbd47 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -96,6 +96,14 @@ % endif + % if course_discovery_enabled: + + % endif + @@ -115,18 +123,7 @@ } - .find-courses .wrapper-search-context { - width: 100%; - margin-left: auto; - margin-right: auto; - } - .courses-container { - max-width: 70%; - height: auto; - padding: 0px, 0px, 0px, 0px; - - } .search-results-count { height: 22px; @@ -302,27 +299,7 @@ /* Vertically align the items in center */ } - .courses-list { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - } - - .courses-listing-item { - flex: 0 1 25%; - /* flex-grow: 0, flex-shrink: 1, flex-basis: 25% */ - box-sizing: border-box; - /* Includes padding and border in the element's total width */ - /* Add margin or padding as needed, keeping in mind the total width */ - } - .find-courses .courses-container .courses:not(.no-course-discovery), - .university-profile .courses-container .courses:not(.no-course-discovery) { - float: left; - display: block; - margin-right: 2.35765%; - width: 100%; - }
Date: Fri, 1 Dec 2023 23:26:07 -0500 Subject: [PATCH 08/17] fix: refine your search --- lms/templates/courseware/courses.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index e922c7ffbd47..dd89dc6ce4a3 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -96,14 +96,6 @@ % endif - % if course_discovery_enabled: - - % endif - @@ -313,6 +305,14 @@

${_('Refine Your Search')}

+ % if course_discovery_enabled: + + % endif + From 48d9f0062de2583cbf203dd7aae51a5b47929b21 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Tue, 5 Dec 2023 18:26:59 -0500 Subject: [PATCH 12/17] fix: fix XSS --- lms/templates/courseware/courses.html | 91 ++++++++++++++------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 34f417cd0d80..83a96ed2b313 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -16,8 +16,8 @@ % if course_discovery_enabled: <%block name="header_extras"> % for template_name in ["course_card", "filter_bar", "filter", "facet", "facet_option"]: - % endfor <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory"> @@ -324,49 +324,56 @@

${_('Refine Your Search')}

% endif - - + + \ No newline at end of file From 084f9a757589303a147d6f64826fa04d019fe948 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Tue, 5 Dec 2023 18:41:26 -0500 Subject: [PATCH 13/17] Revert "fix: fix XSS" This reverts commit 48d9f0062de2583cbf203dd7aae51a5b47929b21. --- lms/templates/courseware/courses.html | 91 +++++++++++++-------------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 83a96ed2b313..34f417cd0d80 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -16,8 +16,8 @@ % if course_discovery_enabled: <%block name="header_extras"> % for template_name in ["course_card", "filter_bar", "filter", "facet", "facet_option"]: - % endfor <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory"> @@ -324,56 +324,49 @@

${_('Refine Your Search')}

% endif - + } + searchForm.addEventListener('submit', function (event) { + event.preventDefault(); + + var searchTerm = document.getElementById('discovery-input').value.trim(); + + setTimeout(function() { + var currentMessage = document.getElementById('discovery-message').textContent; + updateSearchResultsCount(currentMessage, searchTerm); + }, 500); + }); + }); + + \ No newline at end of file From c6c727057302b87f8ba41fac891b78832ba0f3b3 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Tue, 5 Dec 2023 19:58:46 -0500 Subject: [PATCH 14/17] fix: fix style --- lms/templates/courseware/courses.html | 47 +++++++++++++-------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 34f417cd0d80..05a1312bee41 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -329,30 +329,29 @@

${_('Refine Your Search')}

var searchForm = document.getElementById('discovery-form'); function updateSearchResultsCount(currentMessage, searchTerm) { - var message; - - if (searchTerm === '') { - // If no search term is entered, use the current discovery message - message = currentMessage; - } else { - // If a search term is entered, format the message - var numberOfCourses = currentMessage.includes("any") ? 0 : parseInt(currentMessage.match(/\d+/)[0]); - var courseWord = numberOfCourses === 1 ? "course" : "courses"; // Singular or plural - message = "" + numberOfCourses + " " + courseWord + " find for \"" + searchTerm + "\""; - } - - var resultsContainer = document.getElementById('search-results-container'); - var existingElement = resultsContainer.querySelector('.search-results-count'); - - if (existingElement) { - existingElement.innerHTML = message; // Use innerHTML to interpret HTML tags - } else { - var newElement = document.createElement('div'); - newElement.className = 'search-results-count'; - newElement.innerHTML = message; // Use innerHTML to interpret HTML tags - resultsContainer.appendChild(newElement); - } - } + // ... + + var resultsContainer = document.getElementById('search-results-container'); + var existingElement = resultsContainer.querySelector('.search-results-count'); + + if (!existingElement) { + existingElement = document.createElement('div'); + existingElement.className = 'search-results-count'; + resultsContainer.appendChild(existingElement); + } + + // Clear existing content + existingElement.innerHTML = ''; + + // Create and append the bold element for the number of courses + var boldElement = document.createElement('b'); + boldElement.textContent = numberOfCourses; + existingElement.appendChild(boldElement); + + // Append the rest of the text + existingElement.appendChild(document.createTextNode(" " + courseWord + " found for \"" + escapeHtml(searchTerm) + "\"")); + } + searchForm.addEventListener('submit', function (event) { event.preventDefault(); From 2b56a1f8471e34d99ab13cbf3555aab2173ede69 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 6 Dec 2023 19:39:17 -0500 Subject: [PATCH 15/17] Revert "fix: fix style" This reverts commit c6c727057302b87f8ba41fac891b78832ba0f3b3. --- lms/templates/courseware/courses.html | 47 ++++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 05a1312bee41..34f417cd0d80 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -329,29 +329,30 @@

${_('Refine Your Search')}

var searchForm = document.getElementById('discovery-form'); function updateSearchResultsCount(currentMessage, searchTerm) { - // ... - - var resultsContainer = document.getElementById('search-results-container'); - var existingElement = resultsContainer.querySelector('.search-results-count'); - - if (!existingElement) { - existingElement = document.createElement('div'); - existingElement.className = 'search-results-count'; - resultsContainer.appendChild(existingElement); - } - - // Clear existing content - existingElement.innerHTML = ''; - - // Create and append the bold element for the number of courses - var boldElement = document.createElement('b'); - boldElement.textContent = numberOfCourses; - existingElement.appendChild(boldElement); - - // Append the rest of the text - existingElement.appendChild(document.createTextNode(" " + courseWord + " found for \"" + escapeHtml(searchTerm) + "\"")); - } - + var message; + + if (searchTerm === '') { + // If no search term is entered, use the current discovery message + message = currentMessage; + } else { + // If a search term is entered, format the message + var numberOfCourses = currentMessage.includes("any") ? 0 : parseInt(currentMessage.match(/\d+/)[0]); + var courseWord = numberOfCourses === 1 ? "course" : "courses"; // Singular or plural + message = "" + numberOfCourses + " " + courseWord + " find for \"" + searchTerm + "\""; + } + + var resultsContainer = document.getElementById('search-results-container'); + var existingElement = resultsContainer.querySelector('.search-results-count'); + + if (existingElement) { + existingElement.innerHTML = message; // Use innerHTML to interpret HTML tags + } else { + var newElement = document.createElement('div'); + newElement.className = 'search-results-count'; + newElement.innerHTML = message; // Use innerHTML to interpret HTML tags + resultsContainer.appendChild(newElement); + } + } searchForm.addEventListener('submit', function (event) { event.preventDefault(); From c7df3cfdcad578d57ce5a87a8331cf5d129fed82 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 6 Dec 2023 19:58:40 -0500 Subject: [PATCH 16/17] fix: fix xss --- lms/templates/courseware/courses.html | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 34f417cd0d80..4ec38457874b 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -332,24 +332,26 @@

${_('Refine Your Search')}

var message; if (searchTerm === '') { - // If no search term is entered, use the current discovery message + // Use textContent to safely set the text message = currentMessage; } else { - // If a search term is entered, format the message + // Sanitize and escape user input + searchTerm = encodeHTML(searchTerm); + var numberOfCourses = currentMessage.includes("any") ? 0 : parseInt(currentMessage.match(/\d+/)[0]); - var courseWord = numberOfCourses === 1 ? "course" : "courses"; // Singular or plural - message = "" + numberOfCourses + " " + courseWord + " find for \"" + searchTerm + "\""; + var courseWord = numberOfCourses === 1 ? "course" : "courses"; + message = numberOfCourses + " " + courseWord + " found for \"" + searchTerm + "\""; } var resultsContainer = document.getElementById('search-results-container'); var existingElement = resultsContainer.querySelector('.search-results-count'); if (existingElement) { - existingElement.innerHTML = message; // Use innerHTML to interpret HTML tags + existingElement.textContent = message; // Use textContent for security } else { var newElement = document.createElement('div'); newElement.className = 'search-results-count'; - newElement.innerHTML = message; // Use innerHTML to interpret HTML tags + newElement.textContent = message; // Use textContent for security resultsContainer.appendChild(newElement); } } @@ -365,6 +367,12 @@

${_('Refine Your Search')}

}, 500); }); }); + + // Function to escape HTML in user input + function encodeHTML(str){ + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + } + From 99405acd470012bd18918360e744e1a5cd0d8747 Mon Sep 17 00:00:00 2001 From: qtw97 Date: Wed, 6 Dec 2023 20:16:23 -0500 Subject: [PATCH 17/17] fix: fix bold style --- lms/templates/courseware/courses.html | 57 +++++++++++++++------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 4ec38457874b..eed7797fe5a2 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -329,32 +329,37 @@

${_('Refine Your Search')}

var searchForm = document.getElementById('discovery-form'); function updateSearchResultsCount(currentMessage, searchTerm) { - var message; - - if (searchTerm === '') { - // Use textContent to safely set the text - message = currentMessage; - } else { - // Sanitize and escape user input - searchTerm = encodeHTML(searchTerm); - - var numberOfCourses = currentMessage.includes("any") ? 0 : parseInt(currentMessage.match(/\d+/)[0]); - var courseWord = numberOfCourses === 1 ? "course" : "courses"; - message = numberOfCourses + " " + courseWord + " found for \"" + searchTerm + "\""; - } - - var resultsContainer = document.getElementById('search-results-container'); - var existingElement = resultsContainer.querySelector('.search-results-count'); - - if (existingElement) { - existingElement.textContent = message; // Use textContent for security - } else { - var newElement = document.createElement('div'); - newElement.className = 'search-results-count'; - newElement.textContent = message; // Use textContent for security - resultsContainer.appendChild(newElement); - } - } + var resultsContainer = document.getElementById('search-results-container'); + var existingElement = resultsContainer.querySelector('.search-results-count'); + + // Create a new element or clear the existing one + var messageElement = existingElement || document.createElement('div'); + messageElement.className = 'search-results-count'; + messageElement.textContent = ''; // Clear any existing content + + if (searchTerm === '') { + messageElement.textContent = currentMessage; + } else { + var numberOfCourses = currentMessage.includes("any") ? 0 : parseInt(currentMessage.match(/\d+/)[0]); + var courseWord = numberOfCourses === 1 ? "course" : "courses"; + + // Create and append the bold element for the number + var boldElement = document.createElement('b'); + boldElement.textContent = numberOfCourses; + messageElement.appendChild(boldElement); + + // Append the rest of the message + messageElement.appendChild(document.createTextNode(" " + courseWord + " found for \"" + encodeHTML(searchTerm) + "\"")); + } + + // Append the new element if it wasn't already in the DOM + if (!existingElement) { + resultsContainer.appendChild(messageElement); + } + } + + // Continue with the rest of your script... + searchForm.addEventListener('submit', function (event) { event.preventDefault();