forked from openedx/edx-platform
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request openedx#16008 from edx/HarryRein/LEARNER-2307-cour…
…se-goal-messaging LEARNER-2307: Course Goal Messaging
- Loading branch information
Showing
25 changed files
with
643 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')]), | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.