Add message for setting course goal.
LEARNER-2307
This commit is contained in:
committed by
Robert Raposa
parent
a58df6c0fa
commit
bc76ffe5dc
0
lms/djangoapps/course_goals/__init__.py
Normal file
0
lms/djangoapps/course_goals/__init__.py
Normal file
76
lms/djangoapps/course_goals/api.py
Normal file
76
lms/djangoapps/course_goals/api.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Course Goals Python API
|
||||
"""
|
||||
from enum import Enum
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import Text
|
||||
|
||||
from .models import CourseGoal
|
||||
|
||||
|
||||
def add_course_goal(user, course_id, goal_key):
|
||||
"""
|
||||
Add a new course goal for the provided user and course.
|
||||
|
||||
Arguments:
|
||||
user: The user that is setting the goal
|
||||
course_id (string): The id for the course the goal refers to
|
||||
goal_key (string): The goal key that maps to one of the
|
||||
enumerated goal keys from CourseGoalOption.
|
||||
|
||||
"""
|
||||
# Create and save a new course goal
|
||||
course_key = CourseKey.from_string(str(course_id))
|
||||
new_goal = CourseGoal(user=user, course_key=course_key, goal_key=goal_key)
|
||||
new_goal.save()
|
||||
|
||||
|
||||
def get_course_goal(user, course_key):
|
||||
"""
|
||||
Given a user and a course_key, return their course goal.
|
||||
|
||||
If a course goal does not exist, returns None.
|
||||
"""
|
||||
course_goals = CourseGoal.objects.filter(user=user, course_key=course_key)
|
||||
return course_goals[0] if course_goals else None
|
||||
|
||||
|
||||
def remove_course_goal(user, course_key):
|
||||
"""
|
||||
Given a user and a course_key, remove the course goal.
|
||||
"""
|
||||
course_goal = get_course_goal(user, course_key)
|
||||
if course_goal:
|
||||
course_goal.delete()
|
||||
|
||||
|
||||
class CourseGoalOption(Enum):
|
||||
"""
|
||||
Types of goals that a user can select.
|
||||
|
||||
These options are set to a string goal key so that they can be
|
||||
referenced elsewhere in the code when necessary.
|
||||
"""
|
||||
CERTIFY = 'certify'
|
||||
COMPLETE = 'complete'
|
||||
EXPLORE = 'explore'
|
||||
UNSURE = 'unsure'
|
||||
|
||||
@classmethod
|
||||
def get_course_goal_keys(self):
|
||||
return [key.value for key in self]
|
||||
|
||||
|
||||
def get_goal_text(goal_option):
|
||||
"""
|
||||
This function is used to translate the course goal option into
|
||||
a translated, user-facing string to be used to represent that
|
||||
particular goal.
|
||||
"""
|
||||
return {
|
||||
CourseGoalOption.CERTIFY.value: Text(_('Earn a certificate')),
|
||||
CourseGoalOption.COMPLETE.value: Text(_('Complete the course')),
|
||||
CourseGoalOption.EXPLORE.value: Text(_('Explore the course')),
|
||||
CourseGoalOption.UNSURE.value: Text(_('Not sure yet')),
|
||||
}[goal_option]
|
||||
29
lms/djangoapps/course_goals/migrations/0001_initial.py
Normal file
29
lms/djangoapps/course_goals/migrations/0001_initial.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import openedx.core.djangoapps.xmodule_django.models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseGoal',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('course_key', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
|
||||
('goal_key', models.CharField(default=b'unsure', max_length=100, choices=[(b'certify', 'Earn a certificate.'), (b'complete', 'Complete the course.'), (b'explore', 'Explore the course.'), (b'unsure', 'Not sure yet.')])),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='coursegoal',
|
||||
unique_together=set([('user', 'course_key')]),
|
||||
),
|
||||
]
|
||||
0
lms/djangoapps/course_goals/migrations/__init__.py
Normal file
0
lms/djangoapps/course_goals/migrations/__init__.py
Normal file
35
lms/djangoapps/course_goals/models.py
Normal file
35
lms/djangoapps/course_goals/models.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Course Goals Models
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class CourseGoal(models.Model):
|
||||
"""
|
||||
Represents a course goal set by a user on the course home page.
|
||||
|
||||
The goal_key represents the goal key that maps to a translated
|
||||
string through using the CourseGoalOption class.
|
||||
"""
|
||||
GOAL_KEY_CHOICES = (
|
||||
('certify', 'Earn a certificate.'),
|
||||
('complete', 'Complete the course.'),
|
||||
('explore', 'Explore the course.'),
|
||||
('unsure', 'Not sure yet.'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(User, blank=False)
|
||||
course_key = CourseKeyField(max_length=255, db_index=True)
|
||||
goal_key = models.CharField(max_length=100, choices=GOAL_KEY_CHOICES, default='unsure')
|
||||
|
||||
def __unicode__(self):
|
||||
return 'CourseGoal: {user} set goal to {goal} for course {course}'.format(
|
||||
user=self.user.username,
|
||||
course=self.course_key,
|
||||
goal_key=self.goal_key,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "course_key")
|
||||
19
lms/djangoapps/course_goals/signals.py
Normal file
19
lms/djangoapps/course_goals/signals.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Course Goals Signals
|
||||
"""
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from eventtracking import tracker
|
||||
|
||||
from .models import CourseGoal
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseGoal, dispatch_uid="emit_course_goal_event")
|
||||
def emit_course_goal_event(sender, instance, **kwargs):
|
||||
name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated'
|
||||
tracker.emit(
|
||||
name,
|
||||
{
|
||||
'goal_key': instance.goal_key,
|
||||
}
|
||||
)
|
||||
0
lms/djangoapps/course_goals/tests/__init__.py
Normal file
0
lms/djangoapps/course_goals/tests/__init__.py
Normal file
62
lms/djangoapps/course_goals/tests/test_api.py
Normal file
62
lms/djangoapps/course_goals/tests/test_api.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Unit tests for course_goals.api methods.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from lms.djangoapps.course_goals.models import CourseGoal
|
||||
from rest_framework.test import APIClient
|
||||
from student.models import CourseEnrollment
|
||||
from track.tests import EventTrackingTestCase
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
|
||||
|
||||
class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Testing the Course Goals API.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# Create a course with a verified track
|
||||
super(TestCourseGoalsAPI, self).setUp()
|
||||
self.course = CourseFactory.create(emit_signals=True)
|
||||
|
||||
self.user = User.objects.create_user('john', 'lennon@thebeatles.com', 'password')
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
|
||||
self.client = APIClient(enforce_csrf_checks=True)
|
||||
self.client.login(username=self.user.username, password=self.user.password)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
self.apiUrl = reverse('course_goals_api:v0:course_goal-list')
|
||||
|
||||
def test_add_valid_goal(self):
|
||||
""" Ensures a correctly formatted post succeeds. """
|
||||
response = self.post_course_goal(valid=True)
|
||||
self.assert_events_emitted()
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(len(CourseGoal.objects.filter(user=self.user, course_key=self.course.id)), 1)
|
||||
|
||||
def test_add_invalid_goal(self):
|
||||
""" Ensures a correctly formatted post succeeds. """
|
||||
response = self.post_course_goal(valid=False)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(CourseGoal.objects.filter(user=self.user, course_key=self.course.id)), 0)
|
||||
|
||||
def post_course_goal(self, valid=True, goal_key='certify'):
|
||||
"""
|
||||
Sends a post request to set a course goal and returns the response.
|
||||
"""
|
||||
goal_key = goal_key if valid else 'invalid'
|
||||
response = self.client.post(
|
||||
self.apiUrl,
|
||||
{
|
||||
'goal_key': goal_key,
|
||||
'course_key': self.course.id,
|
||||
'user': self.user.username,
|
||||
},
|
||||
)
|
||||
return response
|
||||
15
lms/djangoapps/course_goals/urls.py
Normal file
15
lms/djangoapps/course_goals/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Course Goals URLs
|
||||
"""
|
||||
from django.conf.urls import include, patterns, url
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import CourseGoalViewSet
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'course_goals', CourseGoalViewSet, base_name='course_goal')
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^v0/', include(router.urls, namespace='v0')),
|
||||
)
|
||||
92
lms/djangoapps/course_goals/views.py
Normal file
92
lms/djangoapps/course_goals/views.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Course Goals Views - includes REST API
|
||||
"""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from edx_rest_framework_extensions.authentication import JwtAuthentication
|
||||
from eventtracking import tracker
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.lib.api.permissions import IsStaffOrOwner
|
||||
from rest_framework import permissions, serializers, viewsets
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
from .api import CourseGoalOption
|
||||
from .models import CourseGoal
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CourseGoalSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializes CourseGoal models.
|
||||
"""
|
||||
user = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = CourseGoal
|
||||
fields = ('user', 'course_key', 'goal_key')
|
||||
|
||||
def validate_goal_key(self, value):
|
||||
"""
|
||||
Ensure that the goal_key is valid.
|
||||
"""
|
||||
if value not in CourseGoalOption.get_course_goal_keys():
|
||||
raise serializers.ValidationError(
|
||||
'Provided goal key, {goal_key}, is not a valid goal key (options= {goal_options}).'.format(
|
||||
goal_key=value,
|
||||
goal_options=[option.value for option in CourseGoalOption],
|
||||
)
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_course_key(self, value):
|
||||
"""
|
||||
Ensure that the course_key is valid.
|
||||
"""
|
||||
course_key = CourseKey.from_string(value)
|
||||
if not course_key:
|
||||
raise serializers.ValidationError(
|
||||
'Provided course_key ({course_key}) does not map to a course.'.format(
|
||||
course_key=course_key
|
||||
)
|
||||
)
|
||||
return course_key
|
||||
|
||||
|
||||
class CourseGoalViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API calls to create and retrieve a course goal.
|
||||
|
||||
**Use Case**
|
||||
* Create a new goal for a user.
|
||||
|
||||
Http400 is returned if the format of the request is not correct,
|
||||
the course_id or goal is invalid or cannot be found.
|
||||
|
||||
* Retrieve goal for a user and a particular course.
|
||||
|
||||
Http400 is returned if the format of the request is not correct,
|
||||
or the course_id is invalid or cannot be found.
|
||||
|
||||
**Example Requests**
|
||||
GET /api/course_goals/v0/course_goals/
|
||||
POST /api/course_goals/v0/course_goals/
|
||||
Request data: {"course_key": <course-key>, "goal_key": "<goal-key>", "user": "<username>"}
|
||||
|
||||
"""
|
||||
authentication_classes = (JwtAuthentication, SessionAuthentication,)
|
||||
permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner,)
|
||||
queryset = CourseGoal.objects.all()
|
||||
serializer_class = CourseGoalSerializer
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseGoal, dispatch_uid="emit_course_goals_event")
|
||||
def emit_course_goal_event(sender, instance, **kwargs):
|
||||
name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated'
|
||||
tracker.emit(
|
||||
name,
|
||||
{
|
||||
'goal_key': instance.goal_key,
|
||||
}
|
||||
)
|
||||
@@ -401,6 +401,9 @@ FEATURES = {
|
||||
|
||||
# Whether the bulk enrollment view is enabled.
|
||||
'ENABLE_BULK_ENROLLMENT_VIEW': False,
|
||||
|
||||
# Whether course goals is enabled.
|
||||
'ENABLE_COURSE_GOALS': True,
|
||||
}
|
||||
|
||||
# Settings for the course reviews tool template and identification key, set either to None to disable course reviews
|
||||
@@ -2245,6 +2248,9 @@ INSTALLED_APPS = [
|
||||
'openedx.core.djangoapps.waffle_utils',
|
||||
'openedx.core.djangoapps.schedules.apps.SchedulesConfig',
|
||||
|
||||
# Course Goals
|
||||
'lms.djangoapps.course_goals',
|
||||
|
||||
# Features
|
||||
'openedx.features.course_bookmarks',
|
||||
'openedx.features.course_experience',
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@include margin(0, 0, $baseline, $baseline);
|
||||
position: relative;
|
||||
border: 1px solid $lms-border-color;
|
||||
margin: 0 $baseline $baseline/2;
|
||||
padding: $baseline/2 $baseline;
|
||||
padding: $baseline;
|
||||
border-radius: $baseline/4;
|
||||
width: calc(100% - 90px);
|
||||
|
||||
@media (max-width: $grid-breakpoints-md) {
|
||||
width: 100%;
|
||||
@@ -30,7 +31,7 @@
|
||||
&::before {
|
||||
@include left(0);
|
||||
|
||||
bottom: 35%;
|
||||
top: 25px;
|
||||
border: solid transparent;
|
||||
height: 0;
|
||||
width: 0;
|
||||
@@ -58,13 +59,49 @@
|
||||
|
||||
.message-header {
|
||||
font-weight: $font-semibold;
|
||||
margin-bottom: $baseline/4;
|
||||
margin-bottom: $baseline/2;
|
||||
width: calc(100% - 40px)
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: $font-semibold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dismiss {
|
||||
@include right($baseline/4);
|
||||
top: $baseline/4;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
color: $black-t3;
|
||||
|
||||
&:hover {
|
||||
color: $black-t2;
|
||||
}
|
||||
}
|
||||
// Course Goal Styling
|
||||
.goal-options-container {
|
||||
margin-top: $baseline;
|
||||
text-align: center;
|
||||
|
||||
.goal-option {
|
||||
text-decoration: none;
|
||||
font-size: font-size(x-small);
|
||||
padding: $baseline/2;
|
||||
|
||||
&.dismissible {
|
||||
@include right($baseline/4);
|
||||
position: absolute;
|
||||
top: $baseline/2;
|
||||
font-size: font-size(small);
|
||||
color: $uxpl-blue-base;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $black-t2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ site_status_msg = get_site_status_msg(course_id)
|
||||
</%block>
|
||||
|
||||
% if uses_bootstrap:
|
||||
<header class="navigation-container header-global ${"slim" if course else ""}">
|
||||
<header class="navigation-container header-global ${'slim' if course else ''}">
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<%include file="bootstrap/navbar-logo-header.html" args="online_help_token=online_help_token"/>
|
||||
<button class="navbar-toggler navbar-toggler-right mt-2" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
|
||||
@@ -829,6 +829,11 @@ urlpatterns += (
|
||||
url(r'^commerce/', include('commerce.urls', namespace='commerce')),
|
||||
)
|
||||
|
||||
# Course goals
|
||||
urlpatterns += (
|
||||
url(r'^api/course_goals/', include('lms.djangoapps.course_goals.urls', namespace='course_goals_api')),
|
||||
)
|
||||
|
||||
# Embargo
|
||||
if settings.FEATURES.get('EMBARGO'):
|
||||
urlpatterns += (
|
||||
|
||||
Reference in New Issue
Block a user