Commit d23ed5e2 by Nimisha Asthagiri Committed by GitHub

Merge pull request #15878 from edx/ret/foreign-key-course-key

Ret/foreign key course key
parents 0c204e35 ceb0814f
...@@ -59,6 +59,9 @@ class CourseModeForm(forms.ModelForm): ...@@ -59,6 +59,9 @@ class CourseModeForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CourseModeForm, self).__init__(*args, **kwargs) super(CourseModeForm, self).__init__(*args, **kwargs)
if self.data.get('course'):
self.data['course'] = CourseKey.from_string(self.data['course'])
default_tz = timezone(settings.TIME_ZONE) default_tz = timezone(settings.TIME_ZONE)
if self.instance._expiration_datetime: # pylint: disable=protected-access if self.instance._expiration_datetime: # pylint: disable=protected-access
...@@ -81,7 +84,7 @@ class CourseModeForm(forms.ModelForm): ...@@ -81,7 +84,7 @@ class CourseModeForm(forms.ModelForm):
) )
def clean_course_id(self): def clean_course_id(self):
course_id = self.cleaned_data['course_id'] course_id = self.cleaned_data['course']
try: try:
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
except InvalidKeyError: except InvalidKeyError:
...@@ -150,7 +153,7 @@ class CourseModeForm(forms.ModelForm): ...@@ -150,7 +153,7 @@ class CourseModeForm(forms.ModelForm):
""" """
# Trigger validation so we can access cleaned data # Trigger validation so we can access cleaned data
if self.is_valid(): if self.is_valid():
course_key = self.cleaned_data.get("course_id") course = self.cleaned_data.get("course")
verification_deadline = self.cleaned_data.get("verification_deadline") verification_deadline = self.cleaned_data.get("verification_deadline")
mode_slug = self.cleaned_data.get("mode_slug") mode_slug = self.cleaned_data.get("mode_slug")
...@@ -158,8 +161,11 @@ class CourseModeForm(forms.ModelForm): ...@@ -158,8 +161,11 @@ class CourseModeForm(forms.ModelForm):
# we need to handle saving this ourselves. # we need to handle saving this ourselves.
# Note that verification deadline can be `None` here if # Note that verification deadline can be `None` here if
# the deadline is being disabled. # the deadline is being disabled.
if course_key is not None and mode_slug in CourseMode.VERIFIED_MODES: if course is not None and mode_slug in CourseMode.VERIFIED_MODES:
verification_models.VerificationDeadline.set_deadline(course_key, verification_deadline) verification_models.VerificationDeadline.set_deadline(
course.id,
verification_deadline
)
return super(CourseModeForm, self).save(commit=commit) return super(CourseModeForm, self).save(commit=commit)
...@@ -169,7 +175,7 @@ class CourseModeAdmin(admin.ModelAdmin): ...@@ -169,7 +175,7 @@ class CourseModeAdmin(admin.ModelAdmin):
form = CourseModeForm form = CourseModeForm
fields = ( fields = (
'course_id', 'course',
'mode_slug', 'mode_slug',
'mode_display_name', 'mode_display_name',
'min_price', 'min_price',
...@@ -180,11 +186,11 @@ class CourseModeAdmin(admin.ModelAdmin): ...@@ -180,11 +186,11 @@ class CourseModeAdmin(admin.ModelAdmin):
'bulk_sku' 'bulk_sku'
) )
search_fields = ('course_id',) search_fields = ('course',)
list_display = ( list_display = (
'id', 'id',
'course_id', 'course',
'mode_slug', 'mode_slug',
'min_price', 'min_price',
'expiration_datetime_custom', 'expiration_datetime_custom',
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import openedx.core.djangoapps.xmodule_django.models
# This should only be used for migrations that have be verified to have a net-neutral sql
# change generated by Django
class NoSqlAlterField(migrations.AlterField):
def database_forwards(self, app_label, schema_editor, from_state, to_state):
return
def database_backwards(self, app_label, schema_editor, from_state, to_state):
return
class Migration(migrations.Migration):
dependencies = [
('course_overviews', '0013_courseoverview_language'),
('course_modes', '0007_coursemode_bulk_sku'),
]
operations = [
# Pin the name of the column in the database so that we can rename the field
# in Django without generating any sql changes
migrations.AlterField(
model_name='coursemode',
name='course_id',
field=openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True, verbose_name="Course", db_column='course_id'),
),
# Change the field name in Django to match our target field name
migrations.RenameField(
model_name='coursemode',
old_name='course_id',
new_name='course',
),
# Change the type of the field in Django to be a foreign key
# N.B. we don't need the db_column declaration because the default
# for Django is to use ${field_name}_id (which is what we pinned the column
# name to above).
# We deliberately leave db_constraint set to False because the column
# isn't currently constrained
NoSqlAlterField(
model_name='coursemode',
name='course',
field=models.ForeignKey(related_name='modes', db_constraint=False, default=None, to='course_overviews.CourseOverview'),
preserve_default=False,
),
# Change the Django unique-together constraint (this is Django-level only
# since the database column constraint already exists).
migrations.AlterUniqueTogether(
name='coursemode',
unique_together=set([('course', 'mode_slug', 'currency')]),
),
]
...@@ -13,7 +13,9 @@ from django.db.models import Q ...@@ -13,7 +13,9 @@ from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text from django.utils.encoding import force_text
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
from request_cache.middleware import RequestCache, ns_request_cached from request_cache.middleware import RequestCache, ns_request_cached
...@@ -39,8 +41,25 @@ class CourseMode(models.Model): ...@@ -39,8 +41,25 @@ class CourseMode(models.Model):
class Meta(object): class Meta(object):
app_label = "course_modes" app_label = "course_modes"
# the course that this mode is attached to course = models.ForeignKey(
course_id = CourseKeyField(max_length=255, db_index=True, verbose_name=_("Course")) CourseOverview,
db_constraint=False,
db_index=True,
related_name='modes',
)
# Django sets the `course_id` property in __init__ with the value from the database
# This pair of properties converts that into a proper CourseKey
@property
def course_id(self):
return self._course_id
@course_id.setter
def course_id(self, value):
if isinstance(value, basestring):
self._course_id = CourseKey.from_string(value)
else:
self._course_id = value
# the reference to this mode that can be used by Enrollments to generate # the reference to this mode that can be used by Enrollments to generate
# similar behavior for the same slug across courses # similar behavior for the same slug across courses
...@@ -164,7 +183,7 @@ class CourseMode(models.Model): ...@@ -164,7 +183,7 @@ class CourseMode(models.Model):
CACHE_NAMESPACE = u"course_modes.CourseMode.cache." CACHE_NAMESPACE = u"course_modes.CourseMode.cache."
class Meta(object): class Meta(object):
unique_together = ('course_id', 'mode_slug', 'currency') unique_together = ('course', 'mode_slug', 'currency')
def clean(self): def clean(self):
""" """
...@@ -738,8 +757,6 @@ def get_course_prices(course, verified_only=False): ...@@ -738,8 +757,6 @@ def get_course_prices(course, verified_only=False):
settings.PAID_COURSE_REGISTRATION_CURRENCY[0] settings.PAID_COURSE_REGISTRATION_CURRENCY[0]
) )
currency_symbol = settings.PAID_COURSE_REGISTRATION_CURRENCY[1]
if registration_price > 0: if registration_price > 0:
price = registration_price price = registration_price
# Handle course overview objects which have no cosmetic_display_price # Handle course overview objects which have no cosmetic_display_price
...@@ -748,6 +765,15 @@ def get_course_prices(course, verified_only=False): ...@@ -748,6 +765,15 @@ def get_course_prices(course, verified_only=False):
else: else:
price = None price = None
return registration_price, format_course_price(price)
def format_course_price(price):
"""
Return a formatted price for a course (a string preceded by correct currency, or 'Free').
"""
currency_symbol = settings.PAID_COURSE_REGISTRATION_CURRENCY[1]
if price: if price:
# Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a # Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a
# numerical amount in that currency. Adjust this display as needed for your language. # numerical amount in that currency. Adjust this display as needed for your language.
...@@ -756,7 +782,7 @@ def get_course_prices(course, verified_only=False): ...@@ -756,7 +782,7 @@ def get_course_prices(course, verified_only=False):
# Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free. # Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free.
cosmetic_display_price = _('Free') cosmetic_display_price = _('Free')
return registration_price, force_text(cosmetic_display_price) return cosmetic_display_price
class CourseModesArchive(models.Model): class CourseModesArchive(models.Model):
......
...@@ -12,6 +12,7 @@ from pytz import UTC, timezone ...@@ -12,6 +12,7 @@ from pytz import UTC, timezone
from course_modes.admin import CourseModeForm from course_modes.admin import CourseModeForm
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
# Technically, we shouldn't be importing verify_student, since it's # Technically, we shouldn't be importing verify_student, since it's
# defined in the LMS and course_modes is in common. However, the benefits # defined in the LMS and course_modes is in common. However, the benefits
# of putting all this configuration in one place outweigh the downsides. # of putting all this configuration in one place outweigh the downsides.
...@@ -40,16 +41,16 @@ class AdminCourseModePageTest(ModuleStoreTestCase): ...@@ -40,16 +41,16 @@ class AdminCourseModePageTest(ModuleStoreTestCase):
user.save() user.save()
course = CourseFactory.create() course = CourseFactory.create()
expiration = datetime(2015, 10, 20, 1, 10, 23, tzinfo=timezone(settings.TIME_ZONE)) expiration = datetime(2015, 10, 20, 1, 10, 23, tzinfo=timezone(settings.TIME_ZONE))
CourseOverview.load_from_module_store(course.id)
data = { data = {
'course_id': unicode(course.id), 'course': unicode(course.id),
'mode_slug': 'verified', 'mode_slug': 'verified',
'mode_display_name': 'verified', 'mode_display_name': 'verified',
'min_price': 10, 'min_price': 10,
'currency': 'usd', 'currency': 'usd',
'_expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as separate vals '_expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as separate vals
'_expiration_datetime_1': expiration.time(), '_expiration_datetime_1': expiration.time(),
} }
self.client.login(username=user.username, password='test') self.client.login(username=user.username, password='test')
...@@ -89,6 +90,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase): ...@@ -89,6 +90,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase):
""" """
super(AdminCourseModeFormTest, self).setUp() super(AdminCourseModeFormTest, self).setUp()
self.course = CourseFactory.create() self.course = CourseFactory.create()
CourseOverview.load_from_module_store(self.course.id)
@ddt.data( @ddt.data(
("honor", False), ("honor", False),
...@@ -197,7 +199,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase): ...@@ -197,7 +199,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase):
mode_slug=mode, mode_slug=mode,
) )
return CourseModeForm({ return CourseModeForm({
"course_id": unicode(self.course.id), "course": unicode(self.course.id),
"mode_slug": mode, "mode_slug": mode,
"mode_display_name": mode, "mode_display_name": mode,
"_expiration_datetime": upgrade_deadline, "_expiration_datetime": upgrade_deadline,
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import openedx.core.djangoapps.xmodule_django.models
# This should only be used for migrations that have be verified to have a net-neutral sql
# change generated by Django
class NoSqlAlterField(migrations.AlterField):
def database_forwards(self, app_label, schema_editor, from_state, to_state):
return
def database_backwards(self, app_label, schema_editor, from_state, to_state):
return
class Migration(migrations.Migration):
dependencies = [
('course_overviews', '0013_courseoverview_language'),
('student', '0010_auto_20170207_0458'),
]
operations = [
# Pin the db_columns to the names already in the database
migrations.AlterField(
model_name='courseenrollment',
name='course_id',
field=openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True, db_column='course_id'),
),
migrations.AlterField(
model_name='historicalcourseenrollment',
name='course_id',
field=openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True, db_column='course_id'),
),
# Rename the fields in Django to the new names that we want them to have
migrations.RenameField(
model_name='courseenrollment',
old_name='course_id',
new_name='course',
),
migrations.RenameField(
model_name='historicalcourseenrollment',
old_name='course_id',
new_name='course',
),
# Alter the fields to make them ForeignKeys (leaving off the db_constraint so
# that we don't create it at migration time). The db_column is left off because
# it defaults to ${field_name}_id, which we pinned it to up above.
NoSqlAlterField(
model_name='courseenrollment',
name='course',
field=models.ForeignKey(db_constraint=False, to='course_overviews.CourseOverview'),
preserve_default=True,
),
NoSqlAlterField(
model_name='historicalcourseenrollment',
name='course',
field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.DO_NOTHING, db_constraint=False, blank=True, to='course_overviews.CourseOverview', null=True),
preserve_default=True,
),
# Set the Django-side unique-together and ordering configuration (no SQL required)
migrations.AlterModelOptions(
name='courseenrollment',
options={'ordering': ('user', 'course')},
),
migrations.AlterUniqueTogether(
name='courseenrollment',
unique_together=set([('user', 'course')]),
),
]
...@@ -992,10 +992,26 @@ class CourseEnrollment(models.Model): ...@@ -992,10 +992,26 @@ class CourseEnrollment(models.Model):
checking course dates, user permissions, etc.) This logic is currently checking course dates, user permissions, etc.) This logic is currently
scattered across our views. scattered across our views.
""" """
MODEL_TAGS = ['course_id', 'is_active', 'mode'] MODEL_TAGS = ['course', 'is_active', 'mode']
user = models.ForeignKey(User) user = models.ForeignKey(User)
course_id = CourseKeyField(max_length=255, db_index=True)
course = models.ForeignKey(
CourseOverview,
db_constraint=False,
)
@property
def course_id(self):
return self._course_id
@course_id.setter
def course_id(self, value):
if isinstance(value, basestring):
self._course_id = CourseKey.from_string(value)
else:
self._course_id = value
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
# If is_active is False, then the student is not considered to be enrolled # If is_active is False, then the student is not considered to be enrolled
...@@ -1017,8 +1033,8 @@ class CourseEnrollment(models.Model): ...@@ -1017,8 +1033,8 @@ class CourseEnrollment(models.Model):
MODE_CACHE_NAMESPACE = u'CourseEnrollment.mode_and_active' MODE_CACHE_NAMESPACE = u'CourseEnrollment.mode_and_active'
class Meta(object): class Meta(object):
unique_together = (('user', 'course_id'),) unique_together = (('user', 'course'),)
ordering = ('user', 'course_id') ordering = ('user', 'course')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CourseEnrollment, self).__init__(*args, **kwargs) super(CourseEnrollment, self).__init__(*args, **kwargs)
...@@ -1181,7 +1197,7 @@ class CourseEnrollment(models.Model): ...@@ -1181,7 +1197,7 @@ class CourseEnrollment(models.Model):
cost=cost, currency=currency) cost=cost, currency=currency)
@classmethod @classmethod
def send_signal_full(cls, event, user=user, mode=mode, course_id=course_id, cost=None, currency=None): def send_signal_full(cls, event, user=user, mode=mode, course_id=None, cost=None, currency=None):
""" """
Sends a signal announcing changes in course enrollment status. Sends a signal announcing changes in course enrollment status.
This version should be used if you don't already have a CourseEnrollment object This version should be used if you don't already have a CourseEnrollment object
...@@ -1433,7 +1449,7 @@ class CourseEnrollment(models.Model): ...@@ -1433,7 +1449,7 @@ class CourseEnrollment(models.Model):
try: try:
return cls.objects.filter( return cls.objects.filter(
user=user, user=user,
course_id__startswith=querystring, course__id__startswith=querystring,
is_active=1 is_active=1
).exists() ).exists()
except cls.DoesNotExist: except cls.DoesNotExist:
...@@ -1655,11 +1671,6 @@ class CourseEnrollment(models.Model): ...@@ -1655,11 +1671,6 @@ class CourseEnrollment(models.Model):
return self.user.username return self.user.username
@property @property
def course(self):
# Deprecated. Please use the `course_overview` property instead.
return self.course_overview
@property
def course_overview(self): def course_overview(self):
""" """
Returns a CourseOverview of the course to which this enrollment refers. Returns a CourseOverview of the course to which this enrollment refers.
...@@ -1669,7 +1680,7 @@ class CourseEnrollment(models.Model): ...@@ -1669,7 +1680,7 @@ class CourseEnrollment(models.Model):
If the course is re-published within the lifetime of this If the course is re-published within the lifetime of this
CourseEnrollment object, then the value of this property will CourseEnrollment object, then the value of this property will
become stale. become stale.
""" """
if not self._course_overview: if not self._course_overview:
try: try:
self._course_overview = CourseOverview.get_from_id(self.course_id) self._course_overview = CourseOverview.get_from_id(self.course_id)
......
...@@ -88,11 +88,11 @@ def _listen_for_track_change(sender, user, **kwargs): # pylint: disable=unused- ...@@ -88,11 +88,11 @@ def _listen_for_track_change(sender, user, **kwargs): # pylint: disable=unused-
user_enrollments = CourseEnrollment.enrollments_for_user(user=user) user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
grade_factory = CourseGradeFactory() grade_factory = CourseGradeFactory()
for enrollment in user_enrollments: for enrollment in user_enrollments:
if grade_factory.read(user=user, course=enrollment.course).passed: if grade_factory.read(user=user, course=enrollment.course_overview).passed:
if fire_ungenerated_certificate_task(user, enrollment.course.id): if fire_ungenerated_certificate_task(user, enrollment.course_id):
log.info(u'Certificate generation task initiated for {user} : {course} via track change'.format( log.info(u'Certificate generation task initiated for {user} : {course} via track change'.format(
user=user.id, user=user.id,
course=enrollment.course.id course=enrollment.course_id
)) ))
......
...@@ -38,7 +38,7 @@ def get_experiment_user_metadata_context(course, user): ...@@ -38,7 +38,7 @@ def get_experiment_user_metadata_context(course, user):
return { return {
'upgrade_link': upgrade_data and upgrade_data.link, 'upgrade_link': upgrade_data and upgrade_data.link,
'upgrade_price': get_cosmetic_verified_display_price(course), 'upgrade_price': unicode(get_cosmetic_verified_display_price(course)),
'enrollment_mode': enrollment_mode, 'enrollment_mode': enrollment_mode,
'enrollment_time': enrollment_time, 'enrollment_time': enrollment_time,
'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced', 'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced',
......
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