Commit 416dd82d by zubair-arbi

Merge pull request #8315 from edx/zub/story/credit-requirement-model-add-location-id-field

use 'name' field in 'CreditRequirement' model to save the locatio…
parents 0db8bac7 38b859fd
......@@ -6,7 +6,7 @@ from openedx.core.djangoapps.credit.exceptions import InvalidCreditCourse
def set_credit_requirements(course_key, requirements):
""" Add requirements to given course
"""Add requirements to given course.
Args:
course_key(CourseKey): The identifier for course
......@@ -17,23 +17,21 @@ def set_credit_requirements(course_key, requirements):
"course-v1-edX-DemoX-1T2015",
[
{
"namespace": "verification",
"name": "verification",
"criteria": {},
},
{
"namespace": "reverification",
"name": "midterm",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {},
},
{
"namespace": "proctored_exam",
"name": "final",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Final Exam",
"criteria": {},
},
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {"min_grade": 0.8},
},
])
......@@ -65,7 +63,7 @@ def set_credit_requirements(course_key, requirements):
def get_credit_requirements(course_key, namespace=None):
""" Returns the requirements of a given course and namespace
"""Get credit eligibility requirements of a given course and namespace.
Args:
course_key(CourseKey): The identifier for course
......@@ -77,23 +75,21 @@ def get_credit_requirements(course_key, namespace=None):
requirements =
[
{
"namespace": "verification",
"name": "verification",
"criteria": {},
},
{
"namespace": "reverification",
"name": "midterm",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {},
},
{
"namespace": "proctored_exam",
"name": "final",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Final Exam",
"criteria": {},
},
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {"min_grade": 0.8},
},
]
......@@ -108,6 +104,7 @@ def get_credit_requirements(course_key, namespace=None):
{
"namespace": requirement.namespace,
"name": requirement.name,
"display_name": requirement.display_name,
"criteria": requirement.criteria
}
for requirement in requirements
......@@ -115,7 +112,8 @@ def get_credit_requirements(course_key, namespace=None):
def _get_requirements_to_disable(old_requirements, new_requirements):
""" Returns the ids of CreditRequirement to be disabled that are deleted from the courseware
"""Get the ids of 'CreditRequirement' entries to be disabled that are
deleted from the courseware.
Args:
old_requirements(QuerySet): QuerySet of CreditRequirement
......@@ -128,6 +126,7 @@ def _get_requirements_to_disable(old_requirements, new_requirements):
for old_req in old_requirements:
found_flag = False
for req in new_requirements:
# check if an already added requirement is modified
if req["namespace"] == old_req.namespace and req["name"] == old_req.name:
found_flag = True
break
......@@ -137,7 +136,7 @@ def _get_requirements_to_disable(old_requirements, new_requirements):
def _validate_requirements(requirements):
""" Validate the requirements
"""Validate the requirements.
Args:
requirements(list): List of requirements
......@@ -152,6 +151,8 @@ def _validate_requirements(requirements):
invalid_params.append("namespace")
if not requirement.get("name"):
invalid_params.append("name")
if not requirement.get("display_name"):
invalid_params.append("display_name")
if "criteria" not in requirement:
invalid_params.append("criteria")
......
""" This module contains the exceptions raised in credit course requirements """
"""
This module contains the exceptions raised in credit course requirements.
"""
class InvalidCreditRequirements(Exception):
""" The exception occurs when the requirement dictionary has invalid format. """
"""
The exception occurs when the requirement dictionary has invalid format.
"""
pass
class InvalidCreditCourse(Exception):
""" The exception occurs when the the course is not marked as a Credit Course. """
"""
The exception occurs when the the course is not marked as a Credit Course.
"""
pass
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'CreditRequirement.display_name'
db.add_column('credit_creditrequirement', 'display_name',
self.gf('django.db.models.fields.CharField')(default='', max_length=255),
keep_default=False)
def backwards(self, orm):
# Deleting field 'CreditRequirement.display_name'
db.delete_column('credit_creditrequirement', 'display_name')
models = {
'credit.creditcourse': {
'Meta': {'object_name': 'CreditCourse'},
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'credit.crediteligibility': {
'Meta': {'unique_together': "(('username', 'course'),)", 'object_name': 'CreditEligibility'},
'course': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'eligibilities'", 'to': "orm['credit.CreditCourse']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'provider': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'eligibilities'", 'to': "orm['credit.CreditProvider']"}),
'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
},
'credit.creditprovider': {
'Meta': {'object_name': 'CreditProvider'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'provider_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
},
'credit.creditrequirement': {
'Meta': {'unique_together': "(('namespace', 'name', 'course'),)", 'object_name': 'CreditRequirement'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'course': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credit_requirements'", 'to': "orm['credit.CreditCourse']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'criteria': ('jsonfield.fields.JSONField', [], {}),
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'namespace': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'credit.creditrequirementstatus': {
'Meta': {'object_name': 'CreditRequirementStatus'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'reason': ('jsonfield.fields.JSONField', [], {'default': '{}'}),
'requirement': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'statuses'", 'to': "orm['credit.CreditRequirement']"}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
}
}
complete_apps = ['credit']
\ No newline at end of file
......@@ -19,14 +19,16 @@ log = logging.getLogger(__name__)
class CreditCourse(models.Model):
"""Model for tracking a credit course."""
"""
Model for tracking a credit course.
"""
course_key = CourseKeyField(max_length=255, db_index=True, unique=True)
enabled = models.BooleanField(default=False)
@classmethod
def is_credit_course(cls, course_key):
""" Check that given course is credit or not
"""Check that given course is credit or not.
Args:
course_key(CourseKey): The course identifier
......@@ -38,7 +40,7 @@ class CreditCourse(models.Model):
@classmethod
def get_credit_course(cls, course_key):
""" Get the credit course if exists for the given course_key
"""Get the credit course if exists for the given 'course_key'.
Args:
course_key(CourseKey): The course identifier
......@@ -65,28 +67,36 @@ class CreditProvider(TimeStampedModel):
class CreditRequirement(TimeStampedModel):
"""This model represents a credit requirement.
Each requirement is uniquely identified by a `namespace` and a `name`. CreditRequirements
also include a `criteria` dictionary, the format of which varies by the type of requirement.
The criteria dictionary provides additional information clients may need to determine
whether a user has satisfied the requirement.
Each requirement is uniquely identified by its 'namespace' and
'name' fields.
The 'name' field stores the unique name or location (in case of XBlock)
for a requirement, which serves as the unique identifier for that
requirement.
The 'display_name' field stores the display name of the requirement.
The 'criteria' field dictionary provides additional information, clients
may need to determine whether a user has satisfied the requirement.
"""
course = models.ForeignKey(CreditCourse, related_name="credit_requirements")
namespace = models.CharField(max_length=255)
name = models.CharField(max_length=255)
display_name = models.CharField(max_length=255)
criteria = JSONField()
active = models.BooleanField(default=True)
class Meta(object):
"""Model metadata"""
"""
Model metadata.
"""
unique_together = ('namespace', 'name', 'course')
@classmethod
def add_or_update_course_requirement(cls, credit_course, requirement):
""" Add requirement to a given course
"""Add requirement to a given course.
Args:
credit_course(CreditCourse): The identifier for credit course course
requirement(dict): requirement dict to be added
credit_course(CreditCourse): The identifier for credit course
requirement(dict): Requirement dict to be added
Returns:
(CreditRequirement, created) tuple
......@@ -96,6 +106,7 @@ class CreditRequirement(TimeStampedModel):
course=credit_course,
namespace=requirement["namespace"],
name=requirement["name"],
display_name=requirement["display_name"],
defaults={"criteria": requirement["criteria"], "active": True}
)
if not created:
......@@ -107,11 +118,11 @@ class CreditRequirement(TimeStampedModel):
@classmethod
def get_course_requirements(cls, course_key, namespace=None):
""" Get credit requirements of a given course
"""Get credit requirements of a given course.
Args:
course_key(CourseKey): The identifier for a course
namespace(str): namespace of credit course requirements
namespace(str): Namespace of credit course requirements
Returns:
QuerySet of CreditRequirement model
......@@ -123,7 +134,7 @@ class CreditRequirement(TimeStampedModel):
@classmethod
def disable_credit_requirements(cls, requirement_ids):
""" Mark the given requirements inactive
"""Mark the given requirements inactive.
Args:
requirement_ids(list): List of ids
......@@ -176,5 +187,7 @@ class CreditEligibility(TimeStampedModel):
provider = models.ForeignKey(CreditProvider, related_name="eligibilities")
class Meta(object):
"""Model metadata"""
"""
Model metadata.
"""
unique_together = ('username', 'course')
"""This file contains receivers of course publication signals."""
"""
This file contains receivers of course publication signals.
"""
from django.dispatch import receiver
......@@ -7,11 +9,12 @@ from xmodule.modulestore.django import SignalHandler
@receiver(SignalHandler.course_published)
def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
Receives signal and kicks off celery task to update the course requirements.
"""Receive 'course_published' signal and kick off a celery task to update
the credit course requirements.
"""
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
# Import here, because signal is registered at startup, but items in tasks
# are not yet able to be loaded
from .tasks import update_course_requirements
update_course_requirements.delay(unicode(course_key))
""" This file contains celery tasks for credit course views """
"""
This file contains celery tasks for credit course views.
"""
from django.conf import settings
......@@ -55,20 +57,20 @@ def _get_course_credit_requirements(course):
List of minimum_grade_credit and ICRV requirements
"""
icrv_requirements = _get_credit_course_requirement_xblocks(course)
credit_xblock_requirements = _get_credit_course_requirement_xblocks(course)
min_grade_requirement = _get_min_grade_requirement(course)
credit_requirements = icrv_requirements + min_grade_requirement
credit_requirements = credit_xblock_requirements + min_grade_requirement
return credit_requirements
def _get_min_grade_requirement(course):
"""Returns the list of minimum_grade_credit requirements for the given course.
"""Get list of 'minimum_grade_credit' requirement for the given course.
Args:
course(Course): The course object
Raises:
AttributeError if the course has not minimum_grade_credit attribute
AttributeError if the course has not 'minimum_grade_credit' attribute
Returns:
The list of minimum_grade_credit requirements
......@@ -80,6 +82,7 @@ def _get_min_grade_requirement(course):
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": getattr(course, "minimum_grade_credit")
}
......@@ -91,7 +94,7 @@ def _get_min_grade_requirement(course):
def _get_credit_course_requirement_xblocks(course): # pylint: disable=invalid-name
"""Generates a course structure dictionary for the specified course.
"""Generate a course structure dictionary for the specified course.
Args:
course(Course): The course object
......@@ -109,23 +112,25 @@ def _get_credit_course_requirement_xblocks(course): # pylint: disable=invalid-n
block = {
"namespace": curr_block.get_credit_requirement_namespace(),
"name": curr_block.get_credit_requirement_name(),
"display_name": curr_block.get_credit_requirement_display_name(),
"criteria": ""
}
requirements_blocks.append(block)
# Add this blocks children to the stack so that we can traverse them as well.
# Add the children of current block to the stack so that we can
# traverse them as well.
blocks_stack.extend(children)
return requirements_blocks
def _is_credit_requirement(xblock):
"""Check if the given xblock is a credit requirement.
"""Check if the given XBlock is a credit requirement.
Args:
xblock(XBlock): The given xblock object
xblock(XBlock): The given XBlock object
Returns:
True if xblock is a credit requirement else False
True if XBlock is a credit requirement else False
"""
is_credit_requirement = False
......
""" Tests for credit course api """
"""
Tests for credit course api.
"""
import ddt
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.credit.api import (
get_credit_requirements, set_credit_requirements, _get_requirements_to_disable
)
......@@ -12,7 +16,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@ddt.ddt
class ApiTestCases(ModuleStoreTestCase):
""" Tests for credit course api """
"""
Tests for credit course api.
"""
def setUp(self, **kwargs):
super(ApiTestCases, self).setUp()
......@@ -39,6 +45,7 @@ class ApiTestCases(ModuleStoreTestCase):
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade"
}
]
)
......@@ -48,25 +55,34 @@ class ApiTestCases(ModuleStoreTestCase):
set_credit_requirements(self.course_key, requirements)
def test_set_credit_requirements_invalid_course(self):
"""Test that 'InvalidCreditCourse' exception is raise if we try to
set credit requirements for a non credit course.
"""
requirements = [
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {}
}
]
with self.assertRaises(InvalidCreditCourse):
set_credit_requirements(self.course_key, requirements)
self.add_credit_course(enabled=False)
with self.assertRaises(InvalidCreditCourse):
set_credit_requirements(self.course_key, requirements)
def test_set_get_credit_requirements(self):
"""Test that if same requirement is added multiple times
then it is added only one time and update for next all iterations.
"""
self.add_credit_course()
requirements = [
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
}
......@@ -74,27 +90,26 @@ class ApiTestCases(ModuleStoreTestCase):
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
"min_grade": 0.9
}
}
]
set_credit_requirements(self.course_key, requirements)
self.assertEqual(len(get_credit_requirements(self.course_key)), 1)
# now verify that the saved requirement has values of last requirement
# from all same requirements
self.assertEqual(get_credit_requirements(self.course_key)[0], requirements[1])
def test_disable_credit_requirements(self):
self.add_credit_course()
requirements = [
{
"namespace": "grade",
"name": "grade",
"criteria": {
"min_grade": 0.8
}
},
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
}
......@@ -106,12 +121,14 @@ class ApiTestCases(ModuleStoreTestCase):
requirements = [
{
"namespace": "reverification",
"name": "midterm",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {}
}
]
set_credit_requirements(self.course_key, requirements)
self.assertEqual(len(get_credit_requirements(self.course_key)), 1)
grade_req = CreditRequirement.objects.filter(namespace="grade", name="grade")
self.assertEqual(len(grade_req), 1)
self.assertEqual(grade_req[0].active, False)
......@@ -122,13 +139,7 @@ class ApiTestCases(ModuleStoreTestCase):
{
"namespace": "grade",
"name": "grade",
"criteria": {
"min_grade": 0.8
}
},
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
}
......@@ -142,7 +153,8 @@ class ApiTestCases(ModuleStoreTestCase):
requirements = [
{
"namespace": "reverification",
"name": "midterm",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {}
}
]
......@@ -154,13 +166,15 @@ class ApiTestCases(ModuleStoreTestCase):
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
}
},
{
"namespace": "reverification",
"name": "midterm",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {}
}
]
......@@ -168,8 +182,9 @@ class ApiTestCases(ModuleStoreTestCase):
self.assertEqual(len(requirements_to_disabled), 0)
def add_credit_course(self, enabled=True):
""" Mark the course as a credit """
"""
Mark the course as a credit.
"""
credit_course = CreditCourse(course_key=self.course_key, enabled=enabled)
credit_course.save()
return credit_course
""" Tests for credit course models """
"""
Tests for credit course models.
"""
import ddt
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.credit.models import CreditCourse, CreditRequirement
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@ddt.ddt
class ModelTestCases(ModuleStoreTestCase):
""" Tests for credit course models """
"""
Tests for credit course models.
"""
def setUp(self, **kwargs):
super(ModelTestCases, self).setUp()
......@@ -28,6 +33,7 @@ class ModelTestCases(ModuleStoreTestCase):
requirement = {
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
}
......@@ -43,6 +49,7 @@ class ModelTestCases(ModuleStoreTestCase):
requirement = {
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
}
......@@ -52,9 +59,10 @@ class ModelTestCases(ModuleStoreTestCase):
self.assertEqual(created, True)
requirement = {
"namespace": "icrv",
"name": "midterm",
"criteria": ""
"namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {}
}
credit_req, created = CreditRequirement.add_or_update_course_requirement(credit_course, requirement)
self.assertEqual(credit_course, credit_req.course)
......@@ -62,6 +70,7 @@ class ModelTestCases(ModuleStoreTestCase):
requirements = CreditRequirement.get_course_requirements(self.course_key)
self.assertEqual(len(requirements), 2)
requirements = CreditRequirement.get_course_requirements(self.course_key, namespace="grade")
self.assertEqual(len(requirements), 1)
......
""" Tests for credit course tasks """
"""
Tests for credit course tasks.
"""
import mock
from datetime import datetime
......@@ -13,16 +15,17 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
class TestTaskExecution(ModuleStoreTestCase):
"""
Set of tests to ensure that the task code will do the right thing when
executed directly. The test course gets created without the listeners
being present, which allows us to ensure that when the listener is
executed, it is done as expected.
"""Set of tests to ensure that the task code will do the right thing when
executed directly.
The test course gets created without the listeners being present, which
allows us to ensure that when the listener is executed, it is done as
expected.
"""
def mocked_set_credit_requirements(course_key, requirements): # pylint: disable=no-self-argument, unused-argument
"""
Used as a side effect when mocking `verify_student.ssencrypt.has_valid_signature`.
"""Used as a side effect when mocking method credit api method
'set_credit_requirements'.
"""
raise InvalidCreditRequirements
......@@ -46,8 +49,7 @@ class TestTaskExecution(ModuleStoreTestCase):
def test_task_adding_requirements_invalid_course(self):
"""
Make sure that the receiver correctly fires off the task when
invoked by signal
Test that credit requirements cannot be added for non credit course.
"""
requirements = get_credit_requirements(self.course.id)
self.assertEqual(len(requirements), 0)
......@@ -57,9 +59,10 @@ class TestTaskExecution(ModuleStoreTestCase):
self.assertEqual(len(requirements), 0)
def test_task_adding_requirements(self):
"""
"""Test that credit requirements are added properly for credit course.
Make sure that the receiver correctly fires off the task when
invoked by signal
invoked by signal.
"""
self.add_credit_course(self.course.id)
requirements = get_credit_requirements(self.course.id)
......@@ -70,9 +73,8 @@ class TestTaskExecution(ModuleStoreTestCase):
self.assertEqual(len(requirements), 1)
def test_task_adding_icrv_requirements(self):
"""
Make sure that the receiver correctly fires off the task when
invoked by signal
"""Make sure that the receiver correctly fires off the task when
invoked by signal.
"""
self.add_credit_course(self.course.id)
self.add_icrv_xblock()
......@@ -90,7 +92,9 @@ class TestTaskExecution(ModuleStoreTestCase):
)
)
def test_retry(self):
"""
"""Test that adding credit requirements is retried when
'InvalidCreditRequirements' exception is raised.
Make sure that the receiver correctly fires off the task when
invoked by signal
"""
......@@ -103,10 +107,10 @@ class TestTaskExecution(ModuleStoreTestCase):
self.assertEqual(len(requirements), 0)
def add_credit_course(self, course_key):
""" Add the course as a credit
"""Add the course as a credit.
Args:
course_key(CourseKey): identifier for the course
course_key(CourseKey): Identifier for the course
Returns:
CreditCourse object added
......
......@@ -50,7 +50,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c
git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b#egg=edx_lint==0.2.3
-e git+https://github.com/edx/xblock-utils.git@db22bc40fd2a75458a3c66d057f88aff5a7383e6#egg=xblock-utils
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@03da85753d5f563a22c1282c0e89fcb2e828b8c1#egg=edx-reverification-block
-e git+https://github.com/edx/edx-reverification-block.git@6e2834c5f7e998ad9b81170e7ceb4d8a64900eb0#egg=edx-reverification-block
git+https://github.com/edx/ecommerce-api-client.git@1.0.0#egg=ecommerce-api-client==1.0.0
# Third Party XBlocks
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment