Repository: openfun/richie

Issue:
Send enrolled user to LMS only when the course run is open
## Bug Report

**Problematic behavior/code**
When a user is enrolled on a course, we show him/her a button that sends him/her to the course on the LMS. This leads to a 500 error in OpenEdX if the course is not yet open.

**Expected Behavior**
The user should not be proposed a link before the start date of the course.

**Steps to Reproduce**
1. Enroll to a course for which the start date is in the future
2. Click on the button to access the course
3. And then the bug happens!

**Environment**
- Richie version: 2.0.1
- Platform: Docker



Discussion:

Resolving patch:
diff --git a/sandbox/settings.py b/sandbox/settings.py
--- a/sandbox/settings.py
+++ b/sandbox/settings.py
@@ -253,11 +253,21 @@ class Base(StyleguideMixin, DRFMixin, RichieCoursesConfigurationMixin, Configura
         # (i) Info - {base_url} is RICHIE_AUTHENTICATION_DELEGATION.BASE_URL
         # (i) If you need to bind user data into href url, wrap the property between ()
         # e.g: for user.username = johndoe, /u/(username) will be /u/johndoe
-        "PROFILE_URLS": values.ListValue(
-            [
-                {"label": _("Profile"), "href": _("{base_url:s}/u/(username)")},
-                {"label": _("Account"), "href": _("{base_url:s}/account/settings")},
-            ],
+        "PROFILE_URLS": values.DictValue(
+            {
+                "dashboard": {
+                    "label": _("Dashboard"),
+                    "href": _("{base_url:s}/dashboard"),
+                },
+                "profile": {
+                    "label": _("Profile"),
+                    "href": _("{base_url:s}/u/(username)"),
+                },
+                "account": {
+                    "label": _("Account"),
+                    "href": _("{base_url:s}/account/settings"),
+                },
+            },
             environ_name="AUTHENTICATION_PROFILE_URLS",
             environ_prefix=None,
         ),
diff --git a/src/richie/apps/core/context_processors.py b/src/richie/apps/core/context_processors.py
--- a/src/richie/apps/core/context_processors.py
+++ b/src/richie/apps/core/context_processors.py
@@ -53,8 +53,8 @@ def site_metas(request):
 
         context["AUTHENTICATION"] = {
             "profile_urls": json.dumps(
-                [
-                    {
+                {
+                    key: {
                         "label": str(url["label"]),
                         "action": str(
                             url["href"].format(
@@ -62,8 +62,10 @@ def site_metas(request):
                             )
                         ),
                     }
-                    for url in authentication_delegation.get("PROFILE_URLS", [])
-                ]
+                    for key, url in authentication_delegation.get(
+                        "PROFILE_URLS", {}
+                    ).items()
+                }
             ),
         }
 
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
@@ -4,7 +4,12 @@
 from django import template
 from django.core.exceptions import ObjectDoesNotExist
 from django.template.loader import render_to_string
+from django.utils import timezone
+from django.utils.translation import get_language
+from django.utils.translation import gettext as _
+from django.utils.translation import to_locale
 
+import arrow
 from classytags.arguments import Argument, MultiValueArgument
 from classytags.core import Options, Tag
 from classytags.utils import flatten_context
@@ -229,12 +234,31 @@ def course_enrollment_widget_props(context):
     """
     course_run = context["run"]
 
+    profile_urls = json.loads(
+        context.get("AUTHENTICATION", {}).get("profile_urls", "{}")
+    )
+    dashboard_link = profile_urls.get("dashboard", {}).get("action")
+
+    starts_in_message = None
+    if course_run.start > timezone.now():
+        course_start = arrow.get(course_run.start)
+        humanized_course_start = course_start.humanize(
+            arrow.now(), locale=to_locale(get_language())
+        )
+        # Translators: delay indicates when the course will start as a duration.
+        # In english the string will be "The course will start in 3 days"
+        starts_in_message = _("The course will start {delay:s}").format(
+            delay=humanized_course_start
+        )
+
     return json.dumps(
         {
             "courseRun": {
                 "id": course_run.id,
                 "resource_link": course_run.resource_link,
                 "priority": course_run.state["priority"],
+                "starts_in_message": starts_in_message,
+                "dashboard_link": dashboard_link,
             }
         }
     )


Test patch:
diff --git a/src/frontend/js/utils/test/factories.ts b/src/frontend/js/utils/test/factories.ts
--- a/src/frontend/js/utils/test/factories.ts
+++ b/src/frontend/js/utils/test/factories.ts
@@ -18,6 +18,7 @@ export const CourseRunFactory = createSpec({
   enrollment_end: derived(() => faker.date.past()().toISOString()),
   languages: faker.random.locale(),
   state: CourseStateFactory,
+  starts_in_message: null,
 });
 
 export const EnrollmentFactory = createSpec({
diff --git a/tests/apps/core/test_authentication_delegation.py b/tests/apps/core/test_authentication_delegation.py
--- a/tests/apps/core/test_authentication_delegation.py
+++ b/tests/apps/core/test_authentication_delegation.py
@@ -18,12 +18,12 @@ class UserMenuTests(CMSTestCase):
         RICHIE_AUTHENTICATION_DELEGATION={
             "BASE_URL": "https://richie.education:9999",
             "BACKEND": "richie.apps.courses.lms.base.BaseLMSBackend",
-            "PROFILE_URLS": [
-                {
+            "PROFILE_URLS": {
+                "profile": {
                     "label": "Profile",
                     "href": _("{base_url:s}/profile"),
                 }
-            ],
+            },
         }
     )
     def test_user_menu_with_profile_urls(self):
@@ -85,7 +85,7 @@ def test_user_menu_without_profile_urls(self):
         """
         If RICHIE_AUTHENTICATION_DELEGATION.PROFILE_URLS is not defined,
         user menu should render correctly and
-        profileUrls data-props should be an empty array
+        profileUrls data-props should be an empty object
         """
         page = create_page(
             title="Home",
@@ -96,4 +96,4 @@ def test_user_menu_without_profile_urls(self):
         response = self.client.get(page.get_public_url())
 
         self.assertContains(response, "richie-react richie-react--user-login")
-        self.assertContains(response, "data-props='{\"profileUrls\": []}'")
+        self.assertContains(response, "data-props='{\"profileUrls\": {}}'")
diff --git a/tests/apps/courses/test_templatetags_extra_tags_course_enrollment_widget_props.py b/tests/apps/courses/test_templatetags_extra_tags_course_enrollment_widget_props.py
--- a/tests/apps/courses/test_templatetags_extra_tags_course_enrollment_widget_props.py
+++ b/tests/apps/courses/test_templatetags_extra_tags_course_enrollment_widget_props.py
@@ -2,6 +2,10 @@
 Unit tests for the `course_enrollment_widget_props` template filter.
 """
 import json
+from datetime import timedelta
+from unittest import mock
+
+from django.utils import timezone
 
 from cms.test_utils.testcases import CMSTestCase
 
@@ -17,22 +21,70 @@ class CourseEnrollmentWidgetPropsTagTestCase(CMSTestCase):
     def test_course_enrollment_widget_props_tag(self):
         """
         CourseEnrollment required id, resource_link and state.priority course run's properties.
+        Dashboard url is added if defined in profile_urls.
         course_enrollment_widget_props should return these properties wrapped into
         a courseRun object as a stringified json.
         """
+        now = timezone.now()
         course_run = CourseRunFactory(
-            resource_link="http://example.edx:8073/courses/course-v1:edX+DemoX+Demo_Course/course/"
+            resource_link=(
+                "http://example.edx:8073/courses/course-v1:edX+DemoX+Demo_Course/course/"
+            ),
+            start=now + timedelta(days=1),
         )
-        context = {"run": course_run}
-        self.assertEqual(
-            course_enrollment_widget_props(context),
-            json.dumps(
-                {
-                    "courseRun": {
-                        "id": course_run.id,
-                        "resource_link": course_run.resource_link,
-                        "priority": course_run.direct_course.state["priority"],
+        profile_urls = json.dumps(
+            {
+                "dashboard": {"action": "http://example.edx:8073/dashboard"},
+                "profile": {"action": "http://example.edx:8073/u/edx"},
+                "account": {"action": "http://example.edx:8073/account/settings"},
+            }
+        )
+        context = {"run": course_run, "AUTHENTICATION": {"profile_urls": profile_urls}}
+
+        with mock.patch.object(timezone, "now", return_value=now):
+            self.assertEqual(
+                course_enrollment_widget_props(context),
+                json.dumps(
+                    {
+                        "courseRun": {
+                            "id": course_run.id,
+                            "resource_link": course_run.resource_link,
+                            "priority": course_run.direct_course.state["priority"],
+                            "starts_in_message": "The course will start in a day",
+                            "dashboard_link": "http://example.edx:8073/dashboard",
+                        }
                     }
-                }
+                ),
+            )
+
+    def test_course_enrollment_widget_props_tag_undefined_profile_urls(self):
+        """
+        CourseEnrollment required id, resource_link and state.priority course run's properties.
+        Dashboard url is null if not defined in profile_urls.
+        course_enrollment_widget_props should return these properties wrapped into
+        a courseRun object as a stringified json.
+        """
+        now = timezone.now()
+        course_run = CourseRunFactory(
+            resource_link=(
+                "http://example.edx:8073/courses/course-v1:edX+DemoX+Demo_Course/course/"
             ),
+            start=now + timedelta(days=1),
         )
+        context = {"run": course_run}
+
+        with mock.patch.object(timezone, "now", return_value=now):
+            self.assertEqual(
+                course_enrollment_widget_props(context),
+                json.dumps(
+                    {
+                        "courseRun": {
+                            "id": course_run.id,
+                            "resource_link": course_run.resource_link,
+                            "priority": course_run.direct_course.state["priority"],
+                            "starts_in_message": "The course will start in a day",
+                            "dashboard_link": None,
+                        }
+                    }
+                ),
+            )