Repository: openfun/richie

Issue:
Move course run enroll button to the contact block
## Feature Request

**Is your feature request related to a problem or unsupported use case? Please describe.**
Change the course detail template to be equal than the fun-mooc.
On fun-mooc site it is given much more visibility to the current open for enrollment course run button, because it was customized to be on the contact django block.
I've done exactly the same thing for nau.edu.pt.
So I think we should make this the default case for Richie.

**Describe the solution you'd like**
I've copy paste the fun-mooc `course_detail.html` that change the blocks `contact` and `runs_open`.

**Describe alternatives you've considered**
Add same information bellow the enrollment count, but I think the fun-mooc's version is better. 

**Discovery, Documentation, Adoption, Migration Strategy**
If we decide to go further this change, we need to document it on the UPGRADE.md, because the django blocks on the `course_detail.html` could be used to customize the UI.

**Do you want to work on it through a Pull Request?**
If we approve this ideia, I could send a PR.

FUN-MOOC version:
![image](https://user-images.githubusercontent.com/67018/155501008-5a2adc4b-e47b-4ac9-a3af-8be63d307baf.png)

NAU version:
![image](https://user-images.githubusercontent.com/67018/155501074-7871f97b-d92d-4a29-a100-28ef3310859c.png)

Demo version:
![image](https://user-images.githubusercontent.com/67018/155501476-3774b11c-fa42-48ab-a645-42903ba55e00.png)



Discussion:
+1

Resolving patch:
diff --git a/src/richie/apps/courses/templatetags/extra_tags.py b/src/richie/apps/courses/templatetags/extra_tags.py
--- a/src/richie/apps/courses/templatetags/extra_tags.py
+++ b/src/richie/apps/courses/templatetags/extra_tags.py
@@ -5,7 +5,7 @@
 from django.core.exceptions import ObjectDoesNotExist
 from django.template.defaultfilters import stringfilter
 from django.template.loader import render_to_string
-from django.utils import timezone
+from django.utils import timezone, translation
 from django.utils.translation import get_language
 from django.utils.translation import gettext as _
 from django.utils.translation import to_locale
@@ -227,6 +227,33 @@ def has_connected_lms(course_run):
     return LMSHandler.select_lms(course_run.resource_link) is not None
 
 
+@register.filter()
+def visible_on_course_page(course_runs, edit_mode_active=None):
+    """
+    Determine if the passed course run should be visible on the course page, if on edit mode
+    show all the course runs.
+    """
+    if edit_mode_active:
+        return course_runs
+    return list(filter(lambda run: run.is_visible_on_course_page, course_runs))
+
+
+@register.filter()
+def sort_runs_by_language_and_start_date(course_runs):
+    """
+    Order course runs by: firstly runs that contains the language of the current user and only
+    after the runs that don't match the current user authenticated language. On both groups, they
+    should be sorted by course start date.
+    """
+    current_language = translation.get_language()
+    return list(
+        sorted(
+            course_runs,
+            key=lambda run: (current_language not in run.languages, run.start),
+        )
+    )
+
+
 @register.simple_tag(takes_context=True)
 def course_enrollment_widget_props(context):
     """


Test patch:
diff --git a/tests/apps/courses/test_templates_course_detail_rendering.py b/tests/apps/courses/test_templates_course_detail_rendering.py
--- a/tests/apps/courses/test_templates_course_detail_rendering.py
+++ b/tests/apps/courses/test_templates_course_detail_rendering.py
@@ -1422,23 +1422,35 @@ def test_templates_course_detail_runs_catalog_visibility(self):
         """
         course = CourseFactory(page_title="my course")
         page = course.extended_object
-        course_run_title_hidden = "A hidden course run"
+        course_run_title_upcoming_hidden = "A hidden course run"
         CourseRunFactory(
             direct_course=course,
-            title=course_run_title_hidden,
+            title=course_run_title_upcoming_hidden,
             catalog_visibility=CourseRunCatalogVisibility.HIDDEN,
+            start=self.now + timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now + timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=2),
         )
-        course_run_title_about = "A visible course run only on course page"
+        course_run_title_open_about = "A visible course run only on course page"
         CourseRunFactory(
             direct_course=course,
-            title=course_run_title_about,
+            title=course_run_title_open_about,
             catalog_visibility=CourseRunCatalogVisibility.COURSE_ONLY,
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=1),
         )
-        course_run_title_both = "A visible course run"
+        course_run_title_open_both = "A visible course run"
         CourseRunFactory(
             direct_course=course,
-            title=course_run_title_both,
+            title=course_run_title_open_both,
             catalog_visibility=CourseRunCatalogVisibility.COURSE_AND_SEARCH,
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=1),
         )
         self.assertTrue(page.publish("en"))
 
@@ -1446,9 +1458,34 @@ def test_templates_course_detail_runs_catalog_visibility(self):
         url = page.get_absolute_url()
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
-        self.assertNotContains(response, course_run_title_hidden)
-        self.assertContains(response, course_run_title_about)
-        self.assertContains(response, course_run_title_both)
+        self.assertNotContains(
+            response,
+            course_run_title_upcoming_hidden,
+            msg_prefix=("A hidden upcoming run should not be visible on view mode"),
+        )
+        self.assertNotContains(
+            response,
+            "Upcoming",
+            msg_prefix=(
+                "The hidden upcoming run should not be visible so the upcoming title should not be"
+                "visible"
+            ),
+        )
+        self.assertContains(
+            response,
+            course_run_title_open_about,
+            msg_prefix=(
+                "An open run with `about` on catalog visibility should be visible on view mode"
+            ),
+        )
+        self.assertContains(
+            response,
+            course_run_title_open_both,
+            msg_prefix=(
+                "An open run with `both` on catalog visibility should be visible "
+                "on view mode of the course page"
+            ),
+        )
 
         # edit mode, create user, login with him and open course page on edit mode
         staff = UserFactory(is_staff=True, is_superuser=True)
@@ -1456,6 +1493,331 @@ def test_templates_course_detail_runs_catalog_visibility(self):
         url = f"{url:s}?edit"
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
-        self.assertContains(response, course_run_title_about)
-        self.assertContains(response, course_run_title_both)
-        self.assertContains(response, course_run_title_hidden)
+        self.assertContains(
+            response,
+            course_run_title_open_about,
+            msg_prefix=(
+                "An open run with `about`on the catalog visibility should be visible "
+                "on edit mode of the course page"
+            ),
+        )
+        self.assertContains(
+            response,
+            course_run_title_open_both,
+            msg_prefix=(
+                "An open run with `both`on the catalog visibility should be visible "
+                "on edit mode of the course page"
+            ),
+        )
+        self.assertContains(
+            response,
+            course_run_title_upcoming_hidden,
+            msg_prefix=(
+                "A hidden upcoming run should be visible "
+                "on edit mode of the course page"
+            ),
+        )
+        self.assertContains(
+            response,
+            "Upcoming",
+            msg_prefix=(
+                "The hidden upcoming run is visible on edit mode "
+                "so the Upcoming title should be visible"
+            ),
+        )
+
+    @timezone.override(pytz.utc)
+    def test_templates_course_detail_hidden_courses(self):
+        """
+        Check that each run on different state (Open, to be scheduled, hidden, ongoing, archived)
+        is not visible on view mode, also check that each title group is not visible.
+        On edit mode they should all be visible.
+        """
+        course = CourseFactory(page_title="my course")
+        page = course.extended_object
+        course_run_title_open_hidden = "An open hidden course run"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_open_hidden,
+            catalog_visibility=CourseRunCatalogVisibility.HIDDEN,
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=2),
+        )
+        course_run_title_to_be_scheduled_hidden = "A to be scheduled hidden course run"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_to_be_scheduled_hidden,
+            catalog_visibility=CourseRunCatalogVisibility.HIDDEN,
+            start=None,
+            end=None,
+        )
+        course_run_title_upcoming_hidden = "A hidden upcoming course run"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_upcoming_hidden,
+            catalog_visibility=CourseRunCatalogVisibility.HIDDEN,
+            start=self.now + timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now + timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=2),
+        )
+        course_run_title_ongoing_hidden = "An ongoing hidden course run"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_ongoing_hidden,
+            catalog_visibility=CourseRunCatalogVisibility.HIDDEN,
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=1),
+            enrollment_start=self.now - timedelta(hours=2),
+            enrollment_end=self.now - timedelta(hours=1),
+        )
+        course_run_title_archived_hidden = "An archived hidden course run"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_archived_hidden,
+            catalog_visibility=CourseRunCatalogVisibility.HIDDEN,
+            start=self.now - timedelta(hours=2),
+            end=self.now - timedelta(hours=1),
+            enrollment_start=self.now - timedelta(hours=3),
+            enrollment_end=self.now - timedelta(hours=2),
+        )
+        self.assertTrue(page.publish("en"))
+
+        # View mode
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertNotContains(response, course_run_title_open_hidden)
+        self.assertNotContains(response, "Enroll now")
+        self.assertContains(response, "No open course runs")
+
+        self.assertNotContains(response, course_run_title_to_be_scheduled_hidden)
+        self.assertNotContains(response, "To be scheduled")
+
+        self.assertNotContains(response, course_run_title_upcoming_hidden)
+        self.assertNotContains(response, "Upcoming")
+
+        self.assertNotContains(response, course_run_title_ongoing_hidden)
+        self.assertNotContains(response, "Ongoing")
+
+        self.assertNotContains(response, course_run_title_archived_hidden)
+        self.assertNotContains(response, "Archived")
+
+        # edit mode, create user, login with him and open course page on edit mode
+        staff = UserFactory(is_staff=True, is_superuser=True)
+        self.client.login(username=staff.username, password="password")
+        url = f"{url:s}?edit"
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertContains(response, course_run_title_open_hidden)
+        self.assertContains(response, "Enroll now")
+
+        self.assertContains(response, course_run_title_to_be_scheduled_hidden)
+        self.assertContains(response, "To be scheduled")
+
+        self.assertContains(response, course_run_title_upcoming_hidden)
+        self.assertContains(response, "Upcoming")
+
+        self.assertContains(response, course_run_title_ongoing_hidden)
+        self.assertContains(response, "Ongoing")
+
+        self.assertContains(response, course_run_title_archived_hidden)
+        self.assertContains(response, "Archived")
+
+    @timezone.override(pytz.utc)
+    def test_templates_course_detail_open_runs_presentation_ordering(
+        self,
+    ):
+        """
+        Verify the order of 4 open for enrollment course runs.
+        Firstly runs that contains the language of the current user and only after the runs that
+        don't match the current user authenticated language. On both groups, they should be sorted
+        by course start date.
+        """
+        course = CourseFactory(page_title="my course")
+        page = course.extended_object
+        course_run_title_fr_minus_2h = "French course run that have started -2h"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_fr_minus_2h,
+            languages=["fr"],
+            start=self.now - timedelta(hours=2),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=2),
+            enrollment_end=self.now + timedelta(hours=1),
+        )
+        course_run_title_fr_en_plus_1h = (
+            "Course run in both English and French that starts +1h"
+        )
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_fr_en_plus_1h,
+            languages=["en", "fr"],
+            start=self.now + timedelta(hours=1),
+            end=self.now + timedelta(hours=3),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=2),
+        )
+        course_run_title_en_minus_1h = "A course run in English that started -1h"
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_en_minus_1h,
+            languages=["en"],
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=1),
+        )
+        course_run_title_fr_plus_2h = (
+            "French course run that have will start on the next 2h"
+        )
+        CourseRunFactory(
+            direct_course=course,
+            title=course_run_title_fr_plus_2h,
+            languages=["fr"],
+            start=self.now + timedelta(hours=2),
+            end=self.now + timedelta(hours=3),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=1),
+        )
+        # viewing the page as en language
+        self.assertTrue(page.publish("en"))
+
+        # view mode
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        response_content = response.content.decode("UTF-8")
+
+        # get each course title index position
+        fr_minus_2h = response_content.index(course_run_title_fr_minus_2h)
+        fr_en_plus_1h_pos = response_content.index(course_run_title_fr_en_plus_1h)
+        en_minus_1h_pos = response_content.index(course_run_title_en_minus_1h)
+        fr_plus_2h = response_content.index(course_run_title_fr_plus_2h)
+
+        # Check the order of the open runs (language; start run):
+        # 1. en    -1h
+        # 2. fr,en +1h
+        # 3. fr    -2h
+        # 4. fr    +2h
+        self.assertLess(en_minus_1h_pos, fr_en_plus_1h_pos)
+        self.assertLess(fr_en_plus_1h_pos, fr_minus_2h)
+        self.assertLess(fr_minus_2h, fr_plus_2h)
+
+    @timezone.override(pytz.utc)
+    def test_templates_course_detail_scroll_to_open_course_runs_multiple_runs(
+        self,
+    ):
+        """
+        Test if it is shown an anchor that scrolls to the multiple open course runs
+        """
+        course = CourseFactory(page_title="my course")
+        page = course.extended_object
+        for i in range(2):  # pylint: disable=unused-variable
+            CourseRunFactory(
+                direct_course=course,
+                start=self.now - timedelta(hours=1),
+                end=self.now + timedelta(hours=2),
+                enrollment_start=self.now - timedelta(hours=1),
+                enrollment_end=self.now + timedelta(hours=2),
+            )
+
+        self.assertTrue(page.publish("en"))
+
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertContains(
+            response, "2 course runs are currently open for this course"
+        )
+
+    @timezone.override(pytz.utc)
+    def test_templates_course_detail_scroll_to_open_course_runs_single_run(
+        self,
+    ):
+        """
+        Test if the anchor that scrolls the viewport to the open course runs is missing
+        when there is a single open course run.
+        """
+        course = CourseFactory(page_title="my course")
+        page = course.extended_object
+
+        CourseRunFactory(
+            direct_course=course,
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=2),
+        )
+
+        self.assertTrue(page.publish("en"))
+
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertNotContains(
+            response, "course runs are currently open for this course"
+        )
+
+    @timezone.override(pytz.utc)
+    def test_templates_course_detail_scroll_to_open_course_runs_no_open_runs(
+        self,
+    ):
+        """
+        Test if the anchor that scrolls the viewport to the open course runs is missing
+        when there is a single open course run.
+        """
+        course = CourseFactory(page_title="my course")
+        page = course.extended_object
+
+        self.assertTrue(page.publish("en"))
+
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertNotContains(
+            response, "course runs are currently open for this course"
+        )
+
+    def test_templates_course_detail_no_runs_msg(self):
+        """
+        Test if the `No course runs` message is displayed when the course doesn't have any run.
+        """
+        course = CourseFactory()
+        page = course.extended_object
+
+        self.assertTrue(page.publish("en"))
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertContains(response, "No course runs")
+
+    def test_templates_course_detail_no_other_runs_msg(self):
+        """
+        Test if the `No other course runs` message is displayed when there is a single open
+        course run.
+        """
+        course = CourseFactory()
+        page = course.extended_object
+        CourseRunFactory(
+            direct_course=course,
+            start=self.now - timedelta(hours=1),
+            end=self.now + timedelta(hours=2),
+            enrollment_start=self.now - timedelta(hours=1),
+            enrollment_end=self.now + timedelta(hours=2),
+        )
+
+        self.assertTrue(page.publish("en"))
+        url = page.get_absolute_url()
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertContains(response, "No other course runs")