Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENT-211 add course key v2 #87

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

zubair-arbi
Copy link

@zubair-arbi zubair-arbi commented Mar 3, 2017

@asadiqbal08 @saleem-latif @mattdrayer
Add a new type CourseKeyV2 of OpaqueKey for handling course keys without run information.

Usage:

>>> from opaque_keys.edx.keys import CourseKeyV2
>>> from opaque_keys.edx.locator import CourseLocatorV2
>>> course_key_v2 = CourseKeyV2.from_string('course-v2:edX+DemoX')
>>> course_key_v2
CourseLocatorV2(org='edX', course='DemoX')
>>> course_key_v2._to_string()
'edX+DemoX'
>>> str(course_key_v2)
'course-v2:edX+DemoX'
>>> course_run_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
>>> course_run_key.make_course_key_v2()
CourseLocatorV2(u'edX', u'DemoX')
>>> course_locator_v2 = CourseLocatorV2.from_course_run_key(course_run_key)
>>> course_locator_v2
CourseLocatorV2(u'edX', u'DemoX')
>>> course_locator_v2.make_course_run_key('Spring_2017')
CourseLocator(u'edX', u'DemoX', 'Spring_2017', None, None)

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 96.081% when pulling f72a1db on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@zubair-arbi
Copy link
Author

@cpennington Please let me know if this implementation is what you have suggested.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 96.081% when pulling 11498a5 on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.07%) to 96.43% when pulling 5f565ee on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@zubair-arbi zubair-arbi force-pushed the zub/ENT-211-add-aggregate-course-key branch from 5f565ee to b824779 Compare March 6, 2017 10:45
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.03%) to 96.476% when pulling b824779 on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.03%) to 96.476% when pulling b824779 on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

"""
The serialized course key without run information.
"""
return '{org}+{course}'.format(org=self.org, course=self.course)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you've implement _to_string, you don't need serialize. If you override serialize like this, it's going to break the namespacing that's built into opaque keys, which will make it impossible to have new/alternate versions of AggregateCourseKeys .

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the method serialize

@@ -1330,3 +1330,67 @@ def to_deprecated_list_repr(self):

# Register AssetLocator as the deprecated fallback for AssetKey
AssetKey.set_deprecated_fallback(AssetLocator)


class AggregateCourseLocator(AggregateCourseKey): # pylint: disable=abstract-method
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What formats of key does this need to parse? Can you add some tests to make sure it parses/serializes the way you expect?

Also, should you be able to get an AggregateCourseKey from a CourseKey?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpennington @clintonb The only format need is org+course. I have added/update tests in opaque_keys/edx/tests/test_locators.py for the AggregateCourseLocator.

I was unable to directly get a AggregateCourseKey locator from a CourseKey locator mainly due to changed KEY_TYPE and OpaqueKey Hierarchy. To handle the conversion from CourseKey to AggregateCourseKey I have added a new method from_course_key in AggregateCourseKey which takes a course key object and returns a locator AggregateCourseLocator from it. Please let me know your suggestion on this approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a) I'm pretty skeptical about the claim that the only format needed is org+course. We've been down this road before, and being able to version the key formats is quite useful over the long term. Please don't neglect that.

b) My suggestion would be to add a method to CourseKey called get_aggregate_course_key which would construct an appropriate AggregateCourseKey object. That's consistent with how we've managed this in the past. You might also add something to AggregateCourseKey that let's you construct a particular run CourseKey by supplying the run_id. (This is all very much in parallel to the relationship between CourseKey and UsageKey).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a) Sorry I forgot to mention that the complete format of course key has version, e.g. course-v2:edX+DemoX
b) I have added the method make_course_key_v2 in CourseKey which will return CourseKeyV2 locator. Also added the method make_course_run_key in CourseKeyV2 which will return a CourseKey locator CourseLocator from the provided course run.
@cpennington thanks for the suggestions. Please have a look on the changes and let me know if I have missed anything.

…method to create aggregate course key from a course key object + add/update tests
@coveralls
Copy link

Coverage Status

Coverage increased (+0.03%) to 96.53% when pulling 1f24d58 on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@zubair-arbi zubair-arbi changed the title WIP: ENT-211 add aggregate course key ENT-211 add aggregate course key Mar 7, 2017
An :class:`opaque_keys.OpaqueKey` identifying a
serialized Course Key object.
"""
KEY_TYPE = 'aggregate_course_key'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpennington any reason (aside from confusion) not to call this course-v2 and use course-run-v1 to replace course-v1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No technical issues that I can think of.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we should do as part of this PR? We can certainly make the change but I don't want to introduce two new ideas at one time if we don't have to

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this course-v2 now. Add a ticket to your backlog to add course-run-v1.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpennington @clintonb I have to change the key type to course_key_v2 or just course_v2 because the CANONICAL_NAMESPACE of AggregateCourseLocator would be course-v2? Also should I change the name of aggregate course key and locator names too?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have change the CANONICAL_NAMESPACE of AggregateCourseLocator to course-v2. Now the aggregate course key format would be course-v2:org+course

AggregateCourseKey.from_string('course-v2:edX+DemoX')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we agreed to get rid of the word aggregate. I have no clue what it means in this context.


"""
super(AggregateCourseLocator, self).__init__(
org=org,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: one line

)

if self.org is None or self.course is None:
raise InvalidKeyError(self.__class__, 'Both org and course should be set.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/should/must

KEY_FIELDS = ('org', 'course')
__slots__ = KEY_FIELDS

URL_RE_SOURCE = r'^(?P<org>{ALLOWED_ID_CHARS}+)\+(?P<course>{ALLOWED_ID_CHARS}+)$'.format(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be a separate variable. Combine it with URL_RE.

ALLOWED_ID_CHARS=Locator.ALLOWED_ID_CHARS
)

URL_RE = re.compile(URL_RE_SOURCE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give this a more appropriate name, KEY_REGEX.

aggregate_course_locator = AggregateCourseLocator.from_course_key(course)

aggregate_course_key = 'aggregate-course:{}+{}'.format(course.org, course.course)
self.assertEqual(aggregate_course_locator, AggregateCourseKey.from_string(aggregate_course_key))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of AggregateCourseKey.from_string is confusing and unnecessary.

course_key = CourseKey.from_string(course_id)
expected = AggregateCourseKey(org=course_key.org, course=course_key.course)
actual = AggregateCourseKey.from_course_key(course_key)
assert expected == actual

aggregate_course_key = 'aggregate-course:{}+{}'.format(course.org, course.course)
self.assertEqual(aggregate_course_locator, AggregateCourseKey.from_string(aggregate_course_key))

def test_aggregate_course_locator_serialize(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_serialize_to_string


def test_aggregate_course_locator_serialize(self):
aggregate_course_key = 'aggregate-course:org+course'
aggregate_course_locator = AggregateCourseKey.from_string(aggregate_course_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not need to use AggregateCourseKey.from_string to instantiate a new instance of AggregateCourseKey. Fix this here, and in all other tests where it is used unnecessarily.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@clintonb @cpennington This seems to be the documented way if you try to initialize the AggregateCourseKey directly you get error:

>>> AggregateCourseKey(org=course_key.org, course=course_key.course)
TypeError: Can't instantiate abstract class AggregateCourseKey with abstract
methods _to_string, from_course_key

**kwargs
)

if self.org is None or self.course is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to account for empty string: if not (self.org and self.course)

"""
Return a string representing this location.
"""
return u'{org}+{course}'.format(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the u.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.03%) to 96.53% when pulling 12086b5 on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@zubair-arbi
Copy link
Author

@cpennington @clintonb please give another review.


def test_aggregate_course_locator(self):
"""
Verify that the method "from_string" of class "AggregateCourseKey"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ` instead of " for Python code (classes, methods).

An :class:`opaque_keys.OpaqueKey` identifying a
serialized Course Key object.
"""
KEY_TYPE = 'aggregate_course_key'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we agreed to get rid of the word aggregate. I have no clue what it means in this context.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.07%) to 96.575% when pulling b1b4a1b on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@zubair-arbi
Copy link
Author

@clintonb @cpennington I have updated the naming. Please review.

@zubair-arbi zubair-arbi changed the title ENT-211 add aggregate course key ENT-211 add course key v2 Mar 9, 2017
__slots__ = ()

@abstractproperty
def from_course_key(self, course_key): # pragma: no cover
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this from_course_run_key. Eventually, course-key-v1 will be replaced with course-run-key-v1.


def test_course_locator_v2(self):
"""
Verify that the method "from_string" of class "CourseKeyV2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ` instead of " to format Python entities. This should be fixed in all of your docstrings in this file.

self.assertEqual(expected_course_locator, course_locator_v2)

@ddt.data(
'org/course/run',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for ddt in this instance. Whether we use the old or new format for CourseKey doesn't matter. The method simply expects an instance of CourseKey.

course_key = CourseKey.from_string('course-v1:org+course+run')
course_locator_v2 = CourseLocatorV2(org=course_key.org, course=course_key.course)
expected_serialized_key = '{org}+{course}'.format(org=course_key.org, course=course_key.course)
# pylint: disable=protected-access
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to call the protected member. Use str(course_locator_v2).

Verify that the method "_to_string" of class "CourseLocatorV2"
serializes a course key v2 to a string with expected format.
"""
course_key = CourseKey.from_string('course-v1:org+course+run')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary.

Tests for :class:`.CourseLocatorV2`
"""

def test_course_locator_v2(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_from_string

'org/course/run',
'course-v1:org+course+run',
)
def test_course_locator_v2_from_course_key(self, course_id):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to prefix your test methods with course_locator_v2. The fact that CourseLocatorV2 is given by the name of the test class, CourseLocatorV2Tests. This should simply be test_from_course_key.

'org+course+run+foo',
'course-v2:org+course+run',
)
def test_invalid_format_course_key(self, course_key):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_from_string_with_invalid_input

@coveralls
Copy link

Coverage Status

Coverage increased (+0.03%) to 96.531% when pulling 7a7595a on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

…lementedError as library locators are course agnostic
@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 96.611% when pulling 0fd82da on zub/ENT-211-add-aggregate-course-key into ffb2ee1 on master.

@zubair-arbi
Copy link
Author

@clintonb Please have another look.
@cpennington I have added inter-conversion methods for course key and course key v2. I have updated the sample usage in the description of this PR. Since the LibraryLocator is course agnostic so the method make_course_key_v2 will just raise error NotImplementedError.

Copy link
Contributor

@clintonb clintonb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was ready to approve this if the issues highlighted yesterday had been resolved. Now the API has been expanded in a manner that is not necessarily in scope or desired.

It would be ideal to limit the co-mingling of key types to avoid deprecation/cleanup issues down the road. I advocate for removing these new methods. If we actually need them in the future, we can add them. Today, however, there is no use case for them that cannot be met by existing methods.

@@ -61,6 +61,13 @@ def make_asset_key(self, asset_type, path): # pragma: no cover
"""
raise NotImplementedError()

@abstractmethod
def make_course_key_v2(self): # pragma: no cover
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary given that you already have CourseKeyV2.from_course_run_key()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpennington @clintonb just to confirm, I have to remove this method only or do I need to remove the make_course_run_key in class CourseKeyV2 too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zubair-arbi: You should have some a method to go from CourseKeyV2 to CourseKey and back again, but only one of each. I would suggest using regular methods, rather than class methods, in general. So, probably CourseKeyV2.make_course_run_key, and CourseKey.make_course_key_v2.

raise NotImplementedError()

@abstractproperty
def make_course_run_key(self, course_run): # pragma: no cover
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? We already have existing methods of creating course run keys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were my suggestion. The existing keys typically are related by giving the more general key a way to create more specific keys by hydrating additional information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, I agree that it doesn't make much sense to have both methods. We should pick one, and stick to it.

@cpennington
Copy link
Contributor

I'm concerned about the names CourseKeyV2 and CourseKey. They are fundamentally different things, but that distinction isn't really captured in the names. I think it would be awesome to deprecate CourseKey and start encouraging the use of CourseRunKey and AggregateCourseKey (which are clearly different). "Course" is just too ambiguous right now, until a vast majority of our software switches to using CourseRun for course runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants